monaco-editor指南-自定义语言高亮
monaco支持目前绝大部分的语言高亮,但只是高亮,不涉及任何语法提示或补全,只有前端系的语言才支持语法提示与补全,就像VS Code那样。在大多数情况下,monaco支持的语言高亮已经可以满足基本需求了,但如果你有一门特殊的语言,那就需要自己实现高亮。
在语法高亮前,首先想象如果没有monaco,你自己要对一段语言字符串做高亮,该怎么做?语法高亮,实际上高亮的是字符串中的token,例如let x = 12;
中包含let
、x
、=
、12
、;
这5个token,他们属于不同的类型,例如keyword、identifier、symbol、number,所以只要对这几个类型预定义高亮样式,tokenizer过程中将字符串切割为不同类型的token并附加对应样式即可。
所以语法高亮,实际上是tokenizer的过程。最简单的办法是直接根据空白字符切割字符串进行tokenizer,但缺点是不直观,可维护性低,并且可能要处理很多语言导致的歧义。monaco并没有使用这种办法,而是使用Monarch进行tokenizer过程的描述,它是声明式的tokenizer过程,比直接切割字符串更加直观。
在Monarch文档页面,可以看到monaco是怎么定义语言的,Monarch配置是一个JSON对象,里面配置了tokenizer的规则,接着我们用它来实现自己的语言高亮。
1import { languages } from "monaco-editor";
2
3languages.register({
4 id: "test-lang",
5});
6
7interface ITestLanguage extends languages.IMonarchLanguage {
8 keywords: string[];
9 typeKeywords: string[];
10}
11
12const languageConfig: ITestLanguage = {
13 defaultToken: "",
14 ignoreCase: false,
15
16 typeKeywords: ["int", "char", "float", "bool"],
17 keywords: ["if", "else", "function"],
18
19 brackets: [
20 { open: "{", close: "}", token: "delimiter.curly" },
21 { open: "[", close: "]", token: "delimiter.square" },
22 { open: "(", close: ")", token: "delimiter.parenthesis" },
23 ],
24
25 tokenizer: {
26 root: [
27 [/\d+/, "number"],
28
29 [/[{}()\[\]]/, "@brackets"],
30
31 [/[;,.]/, "delimiter"],
32
33 [
34 /[a-z_$][\w$]*/,
35 {
36 cases: {
37 "@typeKeywords": "typeKeywords",
38 "@keywords": "keywords",
39 "@default": "identifier",
40 },
41 },
42 ],
43
44 { include: "@comment" },
45 ],
46
47 comment: [
48 [/\/\/.*$/, "comment"],
49 [
50 /\/\*/,
51 {
52 token: "comment",
53 log: "$# comment push",
54 },
55 "@comment",
56 ],
57 [
58 /[^\/*]/,
59 {
60 token: "comment.content",
61 log: "$# in comment",
62 },
63 ],
64 [
65 /\*\//,
66 {
67 token: "comment",
68 log: "$# comment pop",
69 },
70 "@pop",
71 ],
72 ],
73 },
74};
75
76languages.setMonarchTokensProvider("test-lang", languageConfig);
77
78console.log(languages.setMonarchTokensProvider);
这是一个简单的tokenizer,它包含了字符串,数字,括号,注释的匹配。下面来详细解读一下它。首先是两个属性配置defaultToken与ignoreCase,用于配置默认token为空,并且是大小写敏感的。
接着是两个特殊字符串枚举,表示当遇到数组中某个字符时,当前token为这个key。例如遇到了int,那么他就是一个typeKeywords,在主题设置中配置了该类型,就能对int进行高亮显示了。但只配置了这个还不够,特殊字符串枚举必须通过@include
配置在tokenizer中,具体配置稍后再看。
接着是brackets,用于配置括号,并标记其类型。这里支持了圆括号、方括号、花括号三种类型,并为其配置了不同了token class。
下面就到了tokenizer的核心:tokenizer属性,它是tokenizer过程的具体配置,上面的只是预定义了一些常量规则,并没有实际配置起来,只有在tokenizer属性中配置了,才会真正发生作用。
tokenizer的类型是[name: string]: IMonarchLanguageRule[]
,其中IMonarchLanguageRule是一个联合类型,由三种子类型组成:
- IShortMonarchLanguageRule1
- IShortMonarchLanguageRule2
- IExpandedMonarchLanguageRule
这三种也只是类型别名,具体类型分别是:
- [RegExp, IMonarchLanguageAction]
- [RegExp, IMonarchLanguageAction, string]
- {regex?: string | RegExp, action?: IMonarchLanguageAction, include?: string}
其中IMonarchLanguageAction也是联合类型,其子类型分别为:
- IShortMonarchLanguageAction
- IExpandedMonarchLanguageAction
- IShortMonarchLanguageAction[]
- IExpandedMonarchLanguageAction[]
虽然类型多,但核心配置思路只有一个,就是定义匹配该类型的正则表达式,定义该token的class,如果该类型还有进一步的状态,则配置它进入的下一个状态。
那么IShortMonarchLanguageRule1只是IShortMonarchLanguageRule2的简写形式,只是忽略的下一个状态配置,IShortMonarchLanguageRule2也只是IExpandedMonarchLanguageRule的简写形式,IMonarchLanguageAction同理。最简单的情况下你可以将IMonarchLanguageAction写为字符串,表示该token的class,如果还有更加详细的配置,则是IExpandedMonarchLanguageAction。
接着我们回到tokenizer配置中,monaco会根据tokenizer配置从上到下依次对正则进行匹配,并执行第一个匹配到的正则对应的规则。注意,这一点非常重要,这意味着你写的规则顺序也会影响最终的匹配结果。
当匹配到/\d+/
时,表示该token是一个number,匹配到/[{}()\[\]]/
时,@开头表示引用一个规则,这里我们直接使用前面配置好的brackets,下面的/[;,.]/
同理。
当匹配到/[a-z_$][\w$]*/
时,这种情况可能有多种规则,他可能是一个keywords、typeKeywords或者identifier,那么我们可以用case来做条件匹配,当匹配到的结果在keywords中时(使用@keywords引用前面定义过的),表示它是一个keywords,@default表示case的默认情况,当@typeKeywords与@keywords均未匹配到时,该token处于默认情况,即identifier。
{ include: "@comment" }
表示直接引用下面comment状态作为规则,该形式是为了组织定义规则,在monaco编译规则阶段时,它会被提前展开,不会对性能有影响。
comment是一个较复杂的状态,comment不仅包含单行注释,还包含多行注释,并且多行注释/**/之间的所有字符均属于注释内容,这就需要将规则配置为有状态的。
数组的第一行用于匹配单行注释,这个很简单。接着是/\/\*/
,当匹配到它时,则进入comment状态,也就是@comment表示的。这里/\/\*/
规则的第二个元素不是一个字符串了,而是一个对象,其实字符串就是该对象的简写形式,当对象只配置了token时,则可以直接写成字符串,这里我们增加了log属性,monaco在匹配到该规则时会在控制台打印"$# in comment"
,其中$#表示该规则匹配到的内容。
进入到comment状态后,遇到的非/\*
字符均为comment,这条规则确保了/**/
之间的所有字符均为注释。
最后遇到/\*\//
时,规则的下一个状态是@pop
,表示pop出当前状态,也就是从comment状态中退出。
现在我们已经实现了一个简单的语言高亮配置了,当然实际情况可能比这复杂得多,比如数字要支持16进制,更多关键字配置,字符串配置,我们可参照Monarch示例中的语言定义进行配置。
语言高亮只定义token切割是不够的,还需要定义token class对应的样式,下面是根据上面配置简单配置的语言:
1import { editor } from "monaco-editor";
2
3const themeConfig: editor.IStandaloneThemeData = {
4 colors: {},
5 base: "vs",
6 inherit: false,
7 rules: [
8 {
9 token: "delimiter.curly",
10 foreground: "#68217a",
11 },
12 {
13 token: "delimiter.square",
14 foreground: "#008800",
15 },
16 {
17 token: "delimiter.parenthesis",
18 foreground: "#ff0000",
19 },
20 {
21 token: "number",
22 foreground: "#2ecc71",
23 fontStyle: "italic",
24 },
25 {
26 token: "comment",
27 foreground: "#adadad",
28 fontStyle: "italic",
29 },
30 {
31 token: "typeKeywords",
32 foreground: "#e67e22",
33 },
34 {
35 token: "keywords",
36 foreground: "#2980b9",
37 },
38 {
39 token: "identifier",
40 foreground: "#8e44ad",
41 },
42 {
43 token: "comment.content",
44 foreground: "#ff0000",
45 },
46 ],
47};
48
49editor.defineTheme("test-theme", themeConfig);
最后一步,在editor.create中配置语言我们定义的test-lang及主题:
1const editorInstance = editor.create(ele, {
2 ...,
3 language: "test-lang",
4 theme: "test-theme",
5});
最终运行起来就是这个效果