• monaco-editor 的 Language Services


    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

    本文作者:修能

    这是一段平平无奇的 SQL 语法

    SELECT id, sum(name) FROM student GROUP BY id ORDER BY id;
    

    如果把这段代码放到 monaco-editor(@0.49.0) 中,一切也显得非常普通。

    monaco.editor.create(ref.current!, {
      value: 'SELECT id, sum(name) FROM student GROUP BY id ORDER BY id;',
      language: "SparkSQL",
    });
    

    效果如下:

    file

    接下来我们通过 monaco-editor 提供的一些 Language Services 来针对 SparkSQL 的语言进行优化。

    本文旨在提供相关思路以及 Demo,不可将相关代码用于生产环境

    高亮

    const regex1 = /.../;
    const regex2 = /.../;
    const regex3 = /.../;
    const regex4 = /.../;
    
    // Register a new language
    monaco.languages.register({ id: "SparkSQL" });
    
    // Register a tokens provider for the language
    monaco.languages.setMonarchTokensProvider("SparkSQL", {
      tokenizer: {
        root: [
          [regex1, "keyword"],
          [regex2, "comment"],
          [regex3, "function"],
          [regex4, "string"],
        ],
      },
    });
    
    // Define a new theme that contains only rules that match this language
    monaco.editor.defineTheme("myCoolTheme", {
      base: "vs",
      inherit: false,
      rules: [
        { token: "keyword", foreground: "#0000ff" },
        { token: "function", foreground: "#795e26" },
        { token: "comment", foreground: "#008000" },
        { token: "string", foreground: "#a31515" },
      ],
      colors: {
        "editor.foreground": "#001080",
      },
    });
    

    不知道各位有没有疑惑,为什么 monaco-editor 的高亮和 VSCode 的高亮不太一样?
    为什么使用 Monarch 而不是 textmate 的原因?

    file

    折叠

    通过 registerFoldingRangeProvider可以自定义实现一些折叠代码块的逻辑

    monaco.languages.registerFoldingRangeProvider("SparkSQL", {
      provideFoldingRanges: function (model) {
        const ranges: monaco.languages.FoldingRange[] = [];
        for (let i = 0; i < model.getLineCount(); ) {
          const lineContent = model.getLineContent(i + 1);
    
          const isValidLine = (content: string) =>
            content && !content.trim().startsWith("--");
    
          // 整段折叠
          if (isValidLine(lineContent) && !isValidLine(model.getLineContent(i))) {
            const start = i + 1;
            let end = start;
            while (end < model.getLineCount() && model.getLineContent(end + 1)) {
              end++;
            }
            if (end <= model.getLineCount()) {
              ranges.push({
                start: start,
                end: end,
                kind: monaco.languages.FoldingRangeKind.Region,
              });
            }
          }
    
          i++;
        }
        return ranges;
      },
    });
    

    PS:如果不设置的话,monaco-editor 会根据缩紧注册默认的折叠块逻辑

    补全

    通过 registerCompletionItemProvider可以实现自定义补全代码

    monaco.languages.registerCompletionItemProvider("SparkSQL", {
      triggerCharacters: ["."],
      provideCompletionItems: function (model, position) {
        const word = model.getWordUntilPosition(position);
        const range: monaco.IRange = {
          startLineNumber: position.lineNumber,
          endLineNumber: position.lineNumber,
          startColumn: word.startColumn,
          endColumn: word.endColumn,
        };
    
        const offset = model.getOffsetAt(position);
        const prevIdentifier = model.getWordAtPosition(
          model.getPositionAt(offset - 1)
        );
        if (prevIdentifier?.word) {
          const regex = createRegExp(
            exactly("CREATE TABLE ")
              .and(exactly(`${prevIdentifier.word} `))
              .and(exactly("("))
              .and(oneOrMore(char).groupedAs("columns"))
              .and(exactly(")"))
          );
          const match = model.getValue().match(regex);
          if (match && match.groups.columns) {
            const columns = match.groups.columns;
            return {
              suggestions: columns.split(",").map((item) => {
                const [columnName, columnType] = item.trim().split(" ");
                return {
                  label: `${columnName.trim()}(${columnType.trim()})`,
                  kind: monaco.languages.CompletionItemKind.Field,
                  documentation: `${columnName.trim()} ${columnType.trim()}`,
                  insertText: columnName.trim(),
                  range: range,
                };
              }),
            };
          }
        }
    
        return {
          suggestions: createDependencyProposals(range),
        };
      },
    });
    
    

    悬浮提示

    通过 registerHoverProvider实现悬浮后提示相关信息

    import * as monaco from "monaco-editor";
    
    monaco.languages.registerHoverProvider("SparkSQL", {
      provideHover: function (model, position) {
        const word = model.getWordAtPosition(position);
        if (!word) return null;
        const fullText = model.getValue();
        const offset = fullText.indexOf(`CREATE TABLE ${word.word}`);
        if (offset !== -1) {
          const lineNumber = model.getPositionAt(offset);
          const lineContent = model.getLineContent(lineNumber.lineNumber);
          return {
            range: new monaco.Range(
              position.lineNumber,
              word.startColumn,
              position.lineNumber,
              word.endColumn
            ),
            contents: [
              {
                value: lineContent,
              },
            ],
          };
        }
      },
    });
    
    

    内嵌提示

    通过 registerInlayHintsProvider可以实现插入提示代码

    monaco.languages.registerInlayHintsProvider("SparkSQL", {
      provideInlayHints(model, range) {
        const hints: monaco.languages.InlayHint[] = [];
        for (let i = range.startLineNumber; i <= range.endLineNumber; i++) {
          const lineContent = model.getLineContent(i);
          if (lineContent.includes("sum")) {
            hints.push({
              label: "expr: ",
              position: {
                lineNumber: i,
                column: lineContent.indexOf("sum") + 5,
              },
              kind: monaco.languages.InlayHintKind.Parameter,
            });
          }
        }
        return {
          hints: hints,
          dispose: function () {},
        };
      },
    });
    

    跳转定义/引用

    跳转定义/引用是一对相辅相成的 API。如果实现了跳转定义而不实现跳转引用,会让用户感到困惑。
    这里我们分别registerDefinitionProviderregisterReferenceProvider两个 API 实现跳转定义和跳转引用。

    monaco.languages.registerDefinitionProvider("SparkSQL", {
      provideDefinition: function (model, position) {
        const lineContent = model.getLineContent(position.lineNumber);
        if (lineContent.startsWith("--")) return null;
        const word = model.getWordAtPosition(position);
        const fullText = model.getValue();
        const offset = fullText.indexOf(`CREATE TABLE ${word?.word}`);
        if (offset !== -1) {
          const pos = model.getPositionAt(offset + 13);
          return {
            uri: model.uri,
            range: new monaco.Range(
              pos.lineNumber,
              pos.column,
              pos.lineNumber,
              pos.column + word!.word.length
            ),
          };
        }
      },
    });
    
    
    monaco.languages.registerReferenceProvider("SparkSQL", {
      provideReferences: function (model, position) {
        const lineContent = model.getLineContent(position.lineNumber);
        if (!lineContent.startsWith("CREATE TABLE")) return null;
        const word = model.getWordAtPosition(position);
        if (word?.word) {
          const regex = createRegExp(
            exactly("SELECT").and(oneOrMore(char)).and(`FROM student`),
            ["g"]
          );
    
          const fullText = model.getValue();
          const array1: monaco.languages.Location[] = [];
          while (regex.exec(fullText) !== null) {
            console.log("regex:", regex.lastIndex);
            const pos = model.getPositionAt(regex.lastIndex);
            array1.push({
              uri: model.uri,
              range: new monaco.Range(
                pos.lineNumber,
                model.getLineMinColumn(pos.lineNumber),
                pos.lineNumber,
                model.getLineMaxColumn(pos.lineNumber)
              ),
            });
          }
    
          if (array1.length) return array1;
        }
    
        return null;
      },
    });
    
    

    CodeAction

    可以基于 CodeAction 实现如快速修复等功能。

    monaco.languages.registerCodeActionProvider("SparkSQL", {
      provideCodeActions: function (model, range, context) {
        const actions: monaco.languages.CodeAction[] = [];
        const diagnostics = context.markers;
    
        diagnostics.forEach((marker) => {
          if (marker.code === "no-function") {
            actions.push({
              title: "Correct function",
              diagnostics: [marker],
              kind: "quickfix",
              edit: {
                edits: [
                  {
                    resource: model.uri,
                    textEdit: {
                      range: marker,
                      text: "sum",
                    },
                    versionId: model.getVersionId(),
                  },
                ],
              },
              isPreferred: true,
            });
          }
        });
    
        return {
          actions: actions,
          dispose: function () {},
        };
      },
    });
    
    

    PS:需要配合 Markers 一起才能显示其效果

    instance.onDidChangeModelContent(() => {
      setModelMarkers(instance.getModel());
    });
    

    超链接

    众所周知,在 monaco-editor 中,如果一段文本能匹配 http(s?):的话,会自动加上超链接的标识。而通过 registerLinkProvider这个 API,我们可以自定义一些文案进行超链接的跳跃。

    monaco.languages.registerLinkProvider("SparkSQL", {
      provideLinks: function (model) {
        const links: monaco.languages.ILink[] = [];
        const lines = model.getLinesContent();
    
        lines.forEach((line, lineIndex) => {
          const idx = line.toLowerCase().indexOf("sum");
          if (line.startsWith("--") && idx !== -1) {
            links.push({
              range: new monaco.Range(
                lineIndex + 1,
                idx + 1,
                lineIndex + 1,
                idx + 4
              ),
              url: "https://spark.apache.org/docs/latest/api/sql/#sum",
            });
          }
        });
    
        return {
          links: links,
        };
      },
    });
    
    

    格式化

    通过registerDocumentFormattingEditProviderAPI 可以实现文档格式化的功能。

    import * as monaco from "monaco-editor";
    
    monaco.languages.registerDocumentFormattingEditProvider("SparkSQL", {
      provideDocumentFormattingEdits: function (model) {
        const edits: monaco.languages.TextEdit[] = [];
        const lines = model.getLinesContent();
    
        lines.forEach((line, lineNumber) => {
          const trimmedLine = line.trim();
          if (trimmedLine.length > 0) {
            const range = new monaco.Range(
              lineNumber + 1,
              1,
              lineNumber + 1,
              line.length + 1
            );
            edits.push({
              range: range,
              text: trimmedLine,
            });
          }
        });
    
        return edits;
      },
    });
    
    

    其他

    除了上述提到的这些 Language Services 的功能以外,还有很多其他的语言服务功能可以实现。这里只是抛砖引玉来提到一些 API,还有一些 API 可以关注 monaco-editor 的官方文档 API。

    最后

    欢迎关注【袋鼠云数栈UED团队】~
    袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

  • 相关阅读:
    十四天学会C++之第八天:文件操作
    STM32F4---PWM输出
    JAVA集合源码
    2022ICPC 网络赛第二场 F Infinity Tree
    基于SSM+MySQL+Vue前后端分离的在线问卷调查系统
    Shell
    CSS常用的长度单位
    upp(统一流程平台)项目,如果需要项目章程会怎么写
    从“一云多芯”支持,看多元算力的全栈云方案
    视频编解码 — 卡顿与花屏
  • 原文地址:https://www.cnblogs.com/dtux/p/18245298