- A+
我们是袋鼠云数栈 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 来针对 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 的原因?
折叠
通过 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。如果实现了跳转定义而不实现跳转引用,会让用户感到困惑。
这里我们分别registerDefinitionProvider
和 registerReferenceProvider
两个 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, }; }, });
格式化
通过registerDocumentFormattingEditProvider
API 可以实现文档格式化的功能。
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