【CodeMirror】如何构建自己的线上编辑器 - 自定义Lint语法校验

2,143 阅读3分钟

当我们构建好语法高亮后,我们发现当编辑器上的内容并没有命中我们匹配的规则,或者其语义并不能被正确解析。我们需要将异常的地方标示出来,并且给予信息提示。基于此,我们可以通过自定义Lint的方式去实现这个功能。如果需要实现以下功能,我们需要先集成CodeMirror中提供的lint插件,然后基于插件通过registerHelper API去定义我们的lint功能。

CodeMirror Lint插件集成

关于Lint插件的集成,我们需要两个步骤。

  • 引入codemirror/addon/lint/lint.js和lint.css文件。
  • 实例化配置项新增lint: true的选项

为什么要集成lint.js 和lint.css文件

  1. lint.js和lint.css文件内部实现了自定义配置项lint的功能
  2. 通过接口方式定义了外部和lint.js的交互,主要是返回以{message, severity, from, to}对象数组格式的数据来实现侧边栏的异常描述和具体位置异常提示
  3. 通过不同配置满足同步或者异步的处理

为什么要配置lint:true这个选项

lint这个选项是lint.js中通过defineOption的方式去扩展CodeMirror的配置项,通过初始化或者setOption时调用回调去启用lint。

CodeMirror.defineOption("lint", false, function(cm, val, old) {
    if (old && old != CodeMirror.Init) {
      clearMarks(cm);
      if (cm.state.lint.options.lintOnChange !== false)
        cm.off("change", onChange);
      CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
      clearTimeout(cm.state.lint.timeout);
      delete cm.state.lint;
    }

    if (val) {
      var gutters = cm.getOption("gutters"), hasLintGutter = false;
      for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
      var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
      if (state.options.lintOnChange)
        cm.on("change", onChange);
      if (state.options.tooltips != false && state.options.tooltips != "gutter")
        CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);

      startLinting(cm);
    }
  });

如何实现Lint

先上代码。

    CodeMirror.registerHelper("lint", MODE_NAME, (text: string, options: CodeMirror.EditorConfiguration, cm:CodeMirror) => {
      const lintList: CodeMirrorVerifyError[] = [];

      try {
        // 业务实现,存在异常通过Error抛出
        valid(text);
      } catch (e: unknown) {
        lintList.push(e as CodeMirrorVerifyError);
      }
      return lintList;
    });

这里,我们通过CodeMirror的registHelper方法去定义lint。其中这里包含了3个参数

  • type。CodeMirror的命名空间,由于我们定义的是lint,所以赋值为'lint'
  • name。这里需要注意的是,我们的name必须和我们在实例化使用的mode name必须一致,否则lint内部在调用getHelper时,会找不到对于的lint方法
  • 回调函数cb。其中参数为内容字符串、选项对象和编辑器实例。并且需要返回上述说的约定数组{message, severity, from, to}数组。其中message为异常信息、severity时异常级别,默认为error。from和to为异常的起始位置和结束位置,类型为CodeMirror.Pos。

我们可以通过getHelper源码可以看到getHelper的实现基于getHelper。而getHelpers首先会根据type获取列表,然后会获取当前mode,通过mode name去取出匹配的函数列表。

    getHelper: function(pos, type) {
      return this.getHelpers(pos, type)[0]
    },

    getHelpers: function(pos, type) {
      let found = []
      if (!helpers.hasOwnProperty(type)) return found
      let help = helpers[type], mode = this.getModeAt(pos)
      if (typeof mode[type] == "string") {
        if (help[mode[type]]) found.push(help[mode[type]])
      } else if (mode[type]) {
        for (let i = 0; i < mode[type].length; i++) {
          let val = help[mode[type][i]]
          if (val) found.push(val)
        }
      } else if (mode.helperType && help[mode.helperType]) {
        found.push(help[mode.helperType])
      } else if (help[mode.name]) {
        found.push(help[mode.name])
      }
      for (let i = 0; i < help._global.length; i++) {
        let cur = help._global[i]
        if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
          found.push(cur.val)
      }
      return found
    },

这样,我们就基本可以实现我们自定义的Lint校验了。当然,如果你想配置不同的图标和样式,可以通修改css文件覆盖CodeMirror-lint-markers相关的样式

总结

总的来说,配置lint是比较简单的,复杂的主要时你对lint的校验规则,其中涉及了词法分析、语法分析、语义分析等实现。当然,你可以可以通过babel插件去解析对应的语法来实现lint自定义。

2022.08.02

木更