monaco-editor 的 Language Services

  • monaco-editor 的 Language Services已关闭评论
  • 15 次浏览
  • A+
所属分类:Web前端
摘要

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


我们是袋鼠云数栈 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", }); 

效果如下:

monaco-editor 的 Language Services

接下来我们通过 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 的原因?

monaco-editor 的 Language Services

折叠

通过 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