找回密码
 立即注册
首页 业界区 业界 monaco-editor 的 Language Services

monaco-editor 的 Language Services

染悄 2025-6-6 15:09:07
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:修能
这是一段平平无奇的 SQL 语法
  1. SELECT id, sum(name) FROM student GROUP BY id ORDER BY id;
复制代码
如果把这段代码放到 monaco-editor(@0.49.0) 中,一切也显得非常普通。
  1. monaco.editor.create(ref.current!, {
  2.   value: 'SELECT id, sum(name) FROM student GROUP BY id ORDER BY id;',
  3.   language: "SparkSQL",
  4. });
复制代码
效果如下:
1.png

接下来我们通过 monaco-editor 提供的一些 Language Services 来针对 SparkSQL 的语言进行优化。
本文旨在提供相关思路以及 Demo,不可将相关代码用于生产环境
高亮
  1. const regex1 = /.../;
  2. const regex2 = /.../;
  3. const regex3 = /.../;
  4. const regex4 = /.../;
  5. // Register a new language
  6. monaco.languages.register({ id: "SparkSQL" });
  7. // Register a tokens provider for the language
  8. monaco.languages.setMonarchTokensProvider("SparkSQL", {
  9.   tokenizer: {
  10.     root: [
  11.       [regex1, "keyword"],
  12.       [regex2, "comment"],
  13.       [regex3, "function"],
  14.       [regex4, "string"],
  15.     ],
  16.   },
  17. });
  18. // Define a new theme that contains only rules that match this language
  19. monaco.editor.defineTheme("myCoolTheme", {
  20.   base: "vs",
  21.   inherit: false,
  22.   rules: [
  23.     { token: "keyword", foreground: "#0000ff" },
  24.     { token: "function", foreground: "#795e26" },
  25.     { token: "comment", foreground: "#008000" },
  26.     { token: "string", foreground: "#a31515" },
  27.   ],
  28.   colors: {
  29.     "editor.foreground": "#001080",
  30.   },
  31. });
复制代码
不知道各位有没有疑惑,为什么 monaco-editor 的高亮和 VSCode 的高亮不太一样?
为什么使用 Monarch 而不是 textmate 的原因?
2.png

折叠

通过 registerFoldingRangeProvider可以自定义实现一些折叠代码块的逻辑
  1. monaco.languages.registerFoldingRangeProvider("SparkSQL", {
  2.   provideFoldingRanges: function (model) {
  3.     const ranges: monaco.languages.FoldingRange[] = [];
  4.     for (let i = 0; i < model.getLineCount(); ) {
  5.       const lineContent = model.getLineContent(i + 1);
  6.       const isValidLine = (content: string) =>
  7.         content && !content.trim().startsWith("--");
  8.       // 整段折叠
  9.       if (isValidLine(lineContent) && !isValidLine(model.getLineContent(i))) {
  10.         const start = i + 1;
  11.         let end = start;
  12.         while (end < model.getLineCount() && model.getLineContent(end + 1)) {
  13.           end++;
  14.         }
  15.         if (end <= model.getLineCount()) {
  16.           ranges.push({
  17.             start: start,
  18.             end: end,
  19.             kind: monaco.languages.FoldingRangeKind.Region,
  20.           });
  21.         }
  22.       }
  23.       i++;
  24.     }
  25.     return ranges;
  26.   },
  27. });
复制代码
悬浮提示

通过 registerHoverProvider实现悬浮后提示相关信息
  1. monaco.languages.registerCompletionItemProvider("SparkSQL", {
  2.   triggerCharacters: ["."],
  3.   provideCompletionItems: function (model, position) {
  4.     const word = model.getWordUntilPosition(position);
  5.     const range: monaco.IRange = {
  6.       startLineNumber: position.lineNumber,
  7.       endLineNumber: position.lineNumber,
  8.       startColumn: word.startColumn,
  9.       endColumn: word.endColumn,
  10.     };
  11.     const offset = model.getOffsetAt(position);
  12.     const prevIdentifier = model.getWordAtPosition(
  13.       model.getPositionAt(offset - 1)
  14.     );
  15.     if (prevIdentifier?.word) {
  16.       const regex = createRegExp(
  17.         exactly("CREATE TABLE ")
  18.           .and(exactly(`${prevIdentifier.word} `))
  19.           .and(exactly("("))
  20.           .and(oneOrMore(char).groupedAs("columns"))
  21.           .and(exactly(")"))
  22.       );
  23.       const match = model.getValue().match(regex);
  24.       if (match && match.groups.columns) {
  25.         const columns = match.groups.columns;
  26.         return {
  27.           suggestions: columns.split(",").map((item) => {
  28.             const [columnName, columnType] = item.trim().split(" ");
  29.             return {
  30.               label: `${columnName.trim()}(${columnType.trim()})`,
  31.               kind: monaco.languages.CompletionItemKind.Field,
  32.               documentation: `${columnName.trim()} ${columnType.trim()}`,
  33.               insertText: columnName.trim(),
  34.               range: range,
  35.             };
  36.           }),
  37.         };
  38.       }
  39.     }
  40.     return {
  41.       suggestions: createDependencyProposals(range),
  42.     };
  43.   },
  44. });
复制代码
内嵌提示

通过 registerInlayHintsProvider可以实现插入提示代码
  1. import * as monaco from "monaco-editor";
  2. monaco.languages.registerHoverProvider("SparkSQL", {
  3.   provideHover: function (model, position) {
  4.     const word = model.getWordAtPosition(position);
  5.     if (!word) return null;
  6.     const fullText = model.getValue();
  7.     const offset = fullText.indexOf(`CREATE TABLE ${word.word}`);
  8.     if (offset !== -1) {
  9.       const lineNumber = model.getPositionAt(offset);
  10.       const lineContent = model.getLineContent(lineNumber.lineNumber);
  11.       return {
  12.         range: new monaco.Range(
  13.           position.lineNumber,
  14.           word.startColumn,
  15.           position.lineNumber,
  16.           word.endColumn
  17.         ),
  18.         contents: [
  19.           {
  20.             value: lineContent,
  21.           },
  22.         ],
  23.       };
  24.     }
  25.   },
  26. });
复制代码
PS:需要配合 Markers 一起才能显示其效果
  1. monaco.languages.registerInlayHintsProvider("SparkSQL", {
  2.   provideInlayHints(model, range) {
  3.     const hints: monaco.languages.InlayHint[] = [];
  4.     for (let i = range.startLineNumber; i <= range.endLineNumber; i++) {
  5.       const lineContent = model.getLineContent(i);
  6.       if (lineContent.includes("sum")) {
  7.         hints.push({
  8.           label: "expr: ",
  9.           position: {
  10.             lineNumber: i,
  11.             column: lineContent.indexOf("sum") + 5,
  12.           },
  13.           kind: monaco.languages.InlayHintKind.Parameter,
  14.         });
  15.       }
  16.     }
  17.     return {
  18.       hints: hints,
  19.       dispose: function () {},
  20.     };
  21.   },
  22. });
复制代码
超链接

众所周知,在 monaco-editor 中,如果一段文本能匹配 http(s?):的话,会自动加上超链接的标识。而通过 registerLinkProvider这个 API,我们可以自定义一些文案进行超链接的跳跃。
  1. monaco.languages.registerDefinitionProvider("SparkSQL", {
  2.   provideDefinition: function (model, position) {
  3.     const lineContent = model.getLineContent(position.lineNumber);
  4.     if (lineContent.startsWith("--")) return null;
  5.     const word = model.getWordAtPosition(position);
  6.     const fullText = model.getValue();
  7.     const offset = fullText.indexOf(`CREATE TABLE ${word?.word}`);
  8.     if (offset !== -1) {
  9.       const pos = model.getPositionAt(offset + 13);
  10.       return {
  11.         uri: model.uri,
  12.         range: new monaco.Range(
  13.           pos.lineNumber,
  14.           pos.column,
  15.           pos.lineNumber,
  16.           pos.column + word!.word.length
  17.         ),
  18.       };
  19.     }
  20.   },
  21. });
复制代码
格式化

通过registerDocumentFormattingEditProviderAPI 可以实现文档格式化的功能。
  1. monaco.languages.registerReferenceProvider("SparkSQL", {
  2.   provideReferences: function (model, position) {
  3.     const lineContent = model.getLineContent(position.lineNumber);
  4.     if (!lineContent.startsWith("CREATE TABLE")) return null;
  5.     const word = model.getWordAtPosition(position);
  6.     if (word?.word) {
  7.       const regex = createRegExp(
  8.         exactly("SELECT").and(oneOrMore(char)).and(`FROM student`),
  9.         ["g"]
  10.       );
  11.       const fullText = model.getValue();
  12.       const array1: monaco.languages.Location[] = [];
  13.       while (regex.exec(fullText) !== null) {
  14.         console.log("regex:", regex.lastIndex);
  15.         const pos = model.getPositionAt(regex.lastIndex);
  16.         array1.push({
  17.           uri: model.uri,
  18.           range: new monaco.Range(
  19.             pos.lineNumber,
  20.             model.getLineMinColumn(pos.lineNumber),
  21.             pos.lineNumber,
  22.             model.getLineMaxColumn(pos.lineNumber)
  23.           ),
  24.         });
  25.       }
  26.       if (array1.length) return array1;
  27.     }
  28.     return null;
  29.   },
  30. });
复制代码
其他

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

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

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
  • 一个针对 antd 的组件测试工具库——ant-design-testing

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册