首页 > 其他分享 >monaco-editor 的 Language Services

monaco-editor 的 Language Services

时间:2024-06-13 10:15:35浏览次数:33  
标签:const Language lineNumber languages editor monaco Services word model

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

标签:const,Language,lineNumber,languages,editor,monaco,Services,word,model
From: https://www.cnblogs.com/dtux/p/18245298

相关文章

  • 论文解读——AAMAS2024《OPEx: A Large Language Model-Powered Framework for Embodi
    一、研究背景  具身指令执行(EmbodiedInstructionFollowing,EIF)是指在一个特定的物理或虚拟环境中,使能自主代理(如机器人或虚拟代理)根据自然语言指令来执行复杂的任务。这种研究领域集中于探索自然语言理解与机器执行能力的结合,尤其是在模拟家庭或日常环境中,如何使代理......
  • Hungry Hungry Hippos Towards Language Modeling with State Space Models
    目录概H3代码FuD.Y.,DaoT.,SaabK.K.,ThomasA.W.,RudraA.andReC.Hungryhungryhippos:towardslanguagemodelingwithstatespacemodels.2022.概Mamba系列第五作:H3.H3感觉H3是之前的linearattention和SSM的一个结合,它所做的只是把line......
  • LISA: Reasoning Segmentation via Large Language Model
    Motivation&Abs现有的感知系统依赖人类的指示,难以主动推理以理解人类意图。新任务:reasoningsegmentation,模型需要根据给定的复杂/具有隐含意义的文本输出相应的segmask。新的benchmark:包含1000张左右图像的数据集(image-instruction-mask)。模型:LISA,既有LLM的语言生成能力......
  • CBT-LLM: A Chinese Large Language Model for Cognitive Behavioral Therapy-based M
    本文是LLM系列文章,针对《CBT-LLM:AChineseLargeLanguageModelforCognitiveBehavioralTherapy-basedMentalHealthQuestionAnswering》的翻译。CBT-LLM:一个基于认知行为治疗的心理健康问答的中文大语言模型摘要1引言2相关工作3方法4实验5结论和未......
  • Arcee’s MergeKit: A Toolkit for Merging Large Language Models
    本文是LLM系列文章,针对《Arcee’sMergeKit:AToolkitforMergingLargeLanguageModels》的翻译。Arcee的MergeKit:一个用于合并大型语言模型的工具包摘要1引言2背景和相关工作3库设计:关键设计原则4MergeKit的可扩展性5MergeKit的普及性和有效性6结论和......
  • CoLLEGe: Concept Embedding Generation for Large Language Models
    本文是LLM系列文章,针对《CoLLEGe:ConceptEmbeddingGenerationforLargeLanguageModels》的翻译。CoLLEGe:大型语言模型的概念嵌入生成摘要1引言2相关工作3CoLLEGe:概念学习与语言嵌入生成4用于训练CoLLEGe的数据集5实验6结论和讨论摘要当前的语言......
  • Vision-Language Models are Zero-Shot Reward Models for Reinforcement Learning
    发表时间:2024(ICLR2024)文章要点:文章提出用预训练的视觉语言模型作为zero-shot的rewardmodel(VLM-RMs)。好处在于可以通过自然语言来给定一个具体的任务,通过VLM-RMs让强化学习基于reward学习这个任务(usingpretrainedvision-languagemodels(VLMs)aszeroshotrewardmodels......
  • monaco-editor 实现SQL编辑器
    原文链接:https://www.yuque.com/sxd_panda/antv/editor安装yarnaddmonaco-editor或npminstallmonaco-editor配置看网上的教程需要添加vite配置,但是我的项目没有对vite进行配置,打包出来的也是可以用的,具体看你们的场景vite.config.js配置安装vite-plugin-monaco-ed......
  • BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and
    Motivation&Abs端到端大规模视觉语言预训练的开销极大。为此,本文提出了BLIP2,利用现成的冻住的imageencoder以及LLM引导视觉语言预训练。模态差距:通过两阶段训练的轻量级的QueryTransformer(Q-Former)弥补。第一阶段:从冻结的imageencoder引导VL学习;第二阶段:从冻结的LLM引导视......
  • HttpContext探究之RequestServices
    HttpContext探究之RequestServices在一篇随笔中提到了中间件的构造方式,主要有两种,第一种是直接从容器里面获取,第二种是构造函数的参数从容器里面获取,这两者都离不开容器,也就是serviceprovide,而RequestService则是里面重要的内容RequestServices是什么HttpContext.RequestServi......