monaco-editor 的 Language Services

cnblogs 2024-06-13 10:41:00 阅读 67

我们是袋鼠云数栈 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

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


声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。