经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » SQL语言 » 查看文章
monaco-editor 的 Language Services
来源:cnblogs  作者:袋鼠云数栈前端  时间:2024/6/13 10:05:39  对本文有异议

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

效果如下:

file

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

file

折叠

通过 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. });

PS:如果不设置的话,monaco-editor 会根据缩紧注册默认的折叠块逻辑

补全

通过 registerCompletionItemProvider可以实现自定义补全代码

  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. });

悬浮提示

通过 registerHoverProvider实现悬浮后提示相关信息

  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. });

内嵌提示

通过 registerInlayHintsProvider可以实现插入提示代码

  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. });

跳转定义/引用

跳转定义/引用是一对相辅相成的 API。如果实现了跳转定义而不实现跳转引用,会让用户感到困惑。
这里我们分别registerDefinitionProviderregisterReferenceProvider两个 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. });
  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. });

CodeAction

可以基于 CodeAction 实现如快速修复等功能。

  1. monaco.languages.registerCodeActionProvider("SparkSQL", {
  2. provideCodeActions: function (model, range, context) {
  3. const actions: monaco.languages.CodeAction[] = [];
  4. const diagnostics = context.markers;
  5. diagnostics.forEach((marker) => {
  6. if (marker.code === "no-function") {
  7. actions.push({
  8. title: "Correct function",
  9. diagnostics: [marker],
  10. kind: "quickfix",
  11. edit: {
  12. edits: [
  13. {
  14. resource: model.uri,
  15. textEdit: {
  16. range: marker,
  17. text: "sum",
  18. },
  19. versionId: model.getVersionId(),
  20. },
  21. ],
  22. },
  23. isPreferred: true,
  24. });
  25. }
  26. });
  27. return {
  28. actions: actions,
  29. dispose: function () {},
  30. };
  31. },
  32. });

PS:需要配合 Markers 一起才能显示其效果

  1. instance.onDidChangeModelContent(() => {
  2. setModelMarkers(instance.getModel());
  3. });

超链接

众所周知,在 monaco-editor 中,如果一段文本能匹配 http(s?):的话,会自动加上超链接的标识。而通过 registerLinkProvider这个 API,我们可以自定义一些文案进行超链接的跳跃。

  1. monaco.languages.registerLinkProvider("SparkSQL", {
  2. provideLinks: function (model) {
  3. const links: monaco.languages.ILink[] = [];
  4. const lines = model.getLinesContent();
  5. lines.forEach((line, lineIndex) => {
  6. const idx = line.toLowerCase().indexOf("sum");
  7. if (line.startsWith("--") && idx !== -1) {
  8. links.push({
  9. range: new monaco.Range(
  10. lineIndex + 1,
  11. idx + 1,
  12. lineIndex + 1,
  13. idx + 4
  14. ),
  15. url: "https://spark.apache.org/docs/latest/api/sql/#sum",
  16. });
  17. }
  18. });
  19. return {
  20. links: links,
  21. };
  22. },
  23. });

格式化

通过registerDocumentFormattingEditProviderAPI 可以实现文档格式化的功能。

  1. import * as monaco from "monaco-editor";
  2. monaco.languages.registerDocumentFormattingEditProvider("SparkSQL", {
  3. provideDocumentFormattingEdits: function (model) {
  4. const edits: monaco.languages.TextEdit[] = [];
  5. const lines = model.getLinesContent();
  6. lines.forEach((line, lineNumber) => {
  7. const trimmedLine = line.trim();
  8. if (trimmedLine.length > 0) {
  9. const range = new monaco.Range(
  10. lineNumber + 1,
  11. 1,
  12. lineNumber + 1,
  13. line.length + 1
  14. );
  15. edits.push({
  16. range: range,
  17. text: trimmedLine,
  18. });
  19. }
  20. });
  21. return edits;
  22. },
  23. });

其他

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

最后

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

原文链接:https://www.cnblogs.com/dtux/p/18245298

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号