我們是袋鼠雲數棧 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
- 大資料分散式任務排程系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大資料領域的 SQL Parser 專案——dt-sql-parser
- 袋鼠雲數棧前端團隊程式碼評審工程實踐文件——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模組打包器——ko
- 一個針對 antd 的元件測試工具庫——ant-design-testing