Zhengrenzhe

(°∀°)ノ 老打杂了

用户工具

站点工具


前端:monaco-editor:monaco-editor指南-自定义语言高亮

monaco-editor指南-自定义语言高亮

monaco支持目前绝大部分的语言高亮,但只是高亮,不涉及任何语法提示或补全,只有前端系的语言才支持语法提示与补全,就像VS Code那样。在大多数情况下,monaco支持的语言高亮已经可以满足基本需求了,但如果你有一门特殊的语言,那就需要自己实现高亮。

在语法高亮前,首先想象如果没有monaco,你自己要对一段语言字符串做高亮,该怎么做?语法高亮,实际上高亮的是字符串中的token,例如let x = 12;中包含letx=12;这5个token,他们属于不同的类型,例如keyword、identifier、symbol、number,所以只要对这几个类型预定义高亮样式,tokenizer过程中将字符串切割为不同类型的token并附加对应样式即可。

所以语法高亮,实际上是tokenizer的过程。最简单的办法是直接根据空白字符切割字符串进行tokenizer,但缺点是不直观,可维护性低,并且可能要处理很多语言导致的歧义。monaco并没有使用这种办法,而是使用Monarch进行tokenizer过程的描述,它是声明式的tokenizer过程,比直接切割字符串更加直观。

Monarch文档页面,可以看到monaco是怎么定义语言的,Monarch配置是一个JSON对象,里面配置了tokenizer的规则,接着我们用它来实现自己的语言高亮。

import { languages } from "monaco-editor";

languages.register({
    id: "test-lang",
});

interface ITestLanguage extends languages.IMonarchLanguage {
    keywords: string[];
    typeKeywords: string[];
}

const languageConfig: ITestLanguage = {
    defaultToken: "",
    ignoreCase: false,

    typeKeywords: ["int", "char", "float", "bool"],
    keywords: ["if", "else", "function"],

    brackets: [
        { open: "{", close: "}", token: "delimiter.curly" },
        { open: "[", close: "]", token: "delimiter.square" },
        { open: "(", close: ")", token: "delimiter.parenthesis" },
    ],

    tokenizer: {
        root: [
            [/\d+/, "number"],

            [/[{}()\[\]]/, "@brackets"],

            [/[;,.]/, "delimiter"],

            [
                /[a-z_$][\w$]*/,
                {
                    cases: {
                        "@typeKeywords": "typeKeywords",
                        "@keywords": "keywords",
                        "@default": "identifier",
                    },
                },
            ],

            { include: "@comment" },
        ],

        comment: [
            [/\/\/.*$/, "comment"],
            [
                /\/\*/,
                {
                    token: "comment",
                    log: "$# comment push",
                },
                "@comment",
            ],
            [
                /[^\/*]/,
                {
                    token: "comment.content",
                    log: "$# in comment",
                },
            ],
            [
                /\*\//,
                {
                    token: "comment",
                    log: "$# comment pop",
                },
                "@pop",
            ],
        ],
    },
};

languages.setMonarchTokensProvider("test-lang", languageConfig);

console.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对应的样式,下面是根据上面配置简单配置的语言:

import { editor } from "monaco-editor";

const themeConfig: editor.IStandaloneThemeData = {
    colors: {},
    base: "vs",
    inherit: false,
    rules: [
        {
            token: "delimiter.curly",
            foreground: "#68217a",
        },
        {
            token: "delimiter.square",
            foreground: "#008800",
        },
        {
            token: "delimiter.parenthesis",
            foreground: "#ff0000",
        },
        {
            token: "number",
            foreground: "#2ecc71",
            fontStyle: "italic",
        },
        {
            token: "comment",
            foreground: "#adadad",
            fontStyle: "italic",
        },
        {
            token: "typeKeywords",
            foreground: "#e67e22",
        },
        {
            token: "keywords",
            foreground: "#2980b9",
        },
        {
            token: "identifier",
            foreground: "#8e44ad",
        },
        {
            token: "comment.content",
            foreground: "#ff0000",
        },
    ],
};

editor.defineTheme("test-theme", themeConfig);

最后一步,在editor.create中配置语言我们定义的test-lang及主题:

const editorInstance = editor.create(ele, {
    ...,
    language: "test-lang",
    theme: "test-theme",
});

最终运行起来就是这个效果

前端/monaco-editor/monaco-editor指南-自定义语言高亮.txt · 最后更改: 2020/03/23 14:58 由 zhengrenzhe