4 步为 Monaco Editor 添加自定义语言支持

在很多需要为用户提供编写代码能力的服务中,我们需要给用户提供一下能力足够强大的编辑器,这样可以使得用户在编写代码时更顺畅,减少查询文档次数,降低出错概率,提升编码效率。

而想要提供这些能力,就需要一个足够强大的代码编辑器,并且为它添加我们自定义语言的支持,这样用户可以在语法高亮、自动完成等方面得到足够的支持。

为什么用 Monaco Editor?

Visual Studio Code 是世界一个非常流行的代码编辑器,而 Monaco Editor 是用于构建 VSCode 核心功能的代码编辑器,它提供了相当多的功能,用于实现各种代码编辑能力。并且微软为 Monaco Editor 提供了单独的项目,单独的打包脚本,因此我们可以轻易的将 Monaco Editor 集成到我们自己的 Web 应用中。

Monaco Editor 已经提供了一系列的基础设施,用于完成对自定义语言的支持,只需要通过很小的步骤,我们就可以搭建一个属于自己语言的代码编辑器。

那么,这就开始吧!

Step 1. 注册一个语言

这里将不再赘述如何将 Monaco Editor 引入 Web 应用,在 Monaco Editor 的仓库提供了使用各种方式集成 Monaco Editor 的说明。

为了让 Monaco Editor 知道我们将要添加一种自定义语言的支持,首先需要注册一个自定义语言标识,这里我们选择 mylang 作为我们自定义语言的标识符。

注册一个自定义语言对于 Monaco Editor 来说非常简单,只需一行代码即可完成:

monaco.languages.register({ id: 'mylang' });

全部完成!

当然,这只是添加自定义语言支持的第一步,这里只是让 Monaco Editor 知道了我们需要添加一个名为 mylang 的自定义语言。后面还有更多工作来让这个自定义语言在 Monaco Editor 中更像一个得到完全支持的语言。

Step 2. 添加关键字

接下来,我们需要给 mylang 这个自定义语言来添加关键字的高亮支持。

通常,我们要在一个代码编辑器中添加自定义语言的支持,最重要的就是获得在编写代码时,可以将各种关键字、字符串、注释等来内容具体语法高亮显示,从而可以更清晰地来阅读代码。

在这一步中,需要涉及到的内容会稍多一些,因为关键字、字符串、注释的高亮会涉及到关键字的定义、字符串的定义,以及注释的定义等,并且这些内容在被识别出来之后,使用什么颜色来显示它,又会涉及到 Monaco Editor 中的主题,它将会定义每一个自定义语言元素的颜色、风格等。

我们先来列出 mylang 包含的关键字列表,通过这个列表就是一系列单词的数组:

let keywords = [ 'class', 'new', 'string', 'number', 'boolean', 'private', 'public' ];

在匹配关键字时,需要注意通常关键字的前后都不会与其他字符相邻,或者在行首,或者紧跟着一个单词分隔符,例如空格、Tab 等,因此这里可以通过一个正则表达式来获取这样的单词,并且将它与上面列出的 keywords 列表进行匹配,与 keywords 匹配中的,将识别为关键字,而同样格式但不在关键字列表中的标识符,将它识别为变量:

/@?[a-zA-Z][\w$]*/

当然,我们还需要定义字符串和注释是什么样的格式,而这个同样可以使用正则表达式来完成,例如,一个通过双引号包含的字符串通常可以用以下正则表达式来表示:

/".*?"/

以及使用双斜杠表示单行注释可以用以下正则表达式来匹配:

/\/\//

最终,就可以通过 Monaco Editor 的 setMonarchTokensProvider 方法,来将整个语法高亮添加到我们的自定义语言中:

monaco.languages.setMonarchTokensProvider('mylang', {
  keywords,
  tokenizer: {
    root: [
      [ /@?[a-zA-Z][\w$]*/, {
        cases: {
          '@keywords': 'keyword',
          '@default': 'variable',
        }
      }],
      [/".*?"/, 'string'],
      [/\/\//, 'comment'],
    ]
  }
});

现在来看看我们获得了什么:

铛铛,一个具备了关键字、字符串和注释的语法高亮支持!

这里似乎漏掉了什么?对!这里的颜色都是 Monaco Editor 自带主题对于关键字、字符串和注释颜色的定义,如果我们需要定义不同的颜色该如何去做?

同样的,Monaco Editor 提供了很简便的方法,用来自定义一个主题:

monaco.editor.defineTheme('mylang-theme', {
    base: 'vs',
    rules: [
        { token: 'keyword', foreground: '#FF6600', fontStyle: 'bold' },
        { token: 'comment', foreground: '#999999' },
        { token: 'string', foreground: '#009966' },
        { token: 'variable', foreground: '#006699' },
    ],
    colors: {}
});

啊哈,看我们得到了一个完全不一样的语法高亮结果:

到这里,我们已经完成了基本了自定义语言的关键字语法高亮功能,这个编辑器在自定义语言这个领域里,已经具备初步可用的能力了。

Step 3. 添加自动完成

在使用一个代码编辑器时,还有一个重要的功能是什么?对,就是自动完成!

很多时候我们并不想去记忆那么多的关键字,只想通过敲击关键字的前几个字符,剩下的请编辑器来帮我自动补全吧。而这个功能,Monaco Editor 同样提供了良好的支持。

通过 Monaco Editor 的注册自动完成提供者功能,就可以很轻松的为我们的自定义语言添加自动完成能力。

在这里,我们将为所有关键字,提供自动补全能力,用户在编辑器中输入关键字列表中所有关键字的前几个字符时,将得到一个自动完成列表来提示完整的关键字是什么,并且可以通过 Tab 键来完成自动输入。

monaco.languages.registerCompletionItemProvider('mylang', {
    provideCompletionItems: (model, position) => {
        const suggestions = [
            ...keywords.map(k => {
                return {
                    label: k,
                    kind: monacoEditor.languages.CompletionItemKind.Keyword,
                    insertText: k,
                };
            })
        ];
        return { suggestions: suggestions };
    }
});

是不是很简单?来来看效果怎么样:

可以看到,只输入了 st 两个字符,编辑器便为我们提示了完整的关键字为 string,并且可以 Tab 来一键完成输入。

Step 4. 添加错误提示

最后,如果我们的自定义语言,背后也有相应的编译器来支持的,那么,我们需要在用户输入了不符合我们预期的内容时,给于提示,便于用户检测代码中哪里有错误,从而可以快速定位问题并修正问题。

例如在 mylang 中,string 是一个关键字,但是 strig 并不是,如果在代码中输入了 strig,并且我们的编译器可以指出这个错误的话,那么我们就可以通过 Monaco Editor 的 Marker 功能,将这个问题标识出来,从而用户可以很快知道代码错误在哪里,可以更加迅速地去修复这个问题。

例如后端编译器可以给出这样一个错误信息的结构:

let err = {
  message: 'unknow type',
  line: 4,
  column: 5,
  length: 5
};

那么,就可以通过 Monaco Editor 去在代码编辑器提示这个错误:

let markers = [];

if (err) {
  markers.push({
    startLineNumber: err.line,
    endLineNumber: err.line,
    startColumn: err.column,
    endColumn: err.column + err.length,
    message: err.message,
    severity: monaco.MarkerSeverity.Error,
  });
}

monaco.editor.setModelMarkers(editor.getModel(), "owner", markers);

请注意,这里的 editor 为创建 Monaco Editor 的实例对象。

最终,我们得到了一个有用的错误信息提示,用户可以将鼠标放在这个错误上并得到它对应的错误信息:

至此,我们通过 Monaco Editor 提供的各项能力,完成了一个自定义语言 mylang 的完整的开发支持能力,包括了关键字语法高亮、关键字自动完成、编译错误提示这些必备功能。

在最后

当然,不要忘记在初始化 Monaco Editor 实例的时候,将它的语言指定为我们的自定义语言 mylang,否则它将不会按我们的预期去渲染和提示代码。

const container = document.getElementById('container');
editor = monaco.editor.create(container, {
    theme: 'mylang-theme',
    language: 'mylang',
    fontFamily: 'Menlo',
    fontSize: 12,
    value: `// A demo class
class ABC {
private:
    string field1 = "static string";
    string field2;
public:
    string method1();
    string method2();
}`,
});

另外,Monaco Editor 对于自定义语言的支持不仅限于以上这些功能,还有更多强大的能力可以深入到 Monaco Editor 的 API 中去发掘。

希望此文对你有所帮助。

发表评论?

2 条评论。

  1. “monaco-editor”: “^0.44.0”

    在自定义主题方面,defineTheme 方法中 ,color 属性务必要填写,否则 error

    monaco.editor.defineTheme(‘mylang-theme’, {
    base: ‘vs’,
    // here
    colors: {},
    rules: [
    { token: ‘keyword’, foreground: ‘#FF6600’, fontStyle: ‘bold’ },
    { token: ‘comment’, foreground: ‘#999999’ },
    { token: ‘string’, foreground: ‘#009966’ },
    { token: ‘variable’, foreground: ‘#006699’ },
    ],
    });

发表评论


注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>