LSP 与 Monaco 的适配:诊断、补全与跳转在编辑器中的落地

这一篇把前面讲过的 LSP 消息,和 Monaco 以及 Theia 编辑器层的实际行为对起来:
publishDiagnostics 如何变成红波浪线和问题面板,completion/hover/definition 又是怎样映射到补全列表、悬浮提示和跳转动作。

LSP 诊断发布的起点是服务器的 textDocument/publishDiagnostics 通知,
其中包含文档 URI 和一组 diagnostics(范围、严重级别、消息等)。

在 Theia 中,大致会经历三层转换:

  1. LSP 客户端层

    • 收到 publishDiagnostics 消息;
    • 将 diagnostics 转换成内部的 Marker 结构,写入 Markers/诊断服务。
  2. Markers 服务层

    • 按文档 URI 维护当前最新的一组 diagnostics;
    • 对外提供订阅/查询接口,供问题面板、状态栏等使用。
  3. 编辑器适配层(Monaco)

    • 监听 Markers 变化事件;
    • 将 diagnostics 转换为 Monaco 模型上的装饰器:
      • 范围 → monaco.Range
      • 严重级别 → 不同的 className/glyphMarginClassName
      • 同时在 overview ruler/minimap 上打颜色标记。

最终效果是:

  • 在编辑器中看到红波浪线、行首红点、右侧 ruler 里的红条;
  • 在问题面板里看到按文件/行号列出的错误/警告列表;
  • 点击问题面板条目时,编辑器跳转到对应位置。

这一条链路将 LSP 的 diagnostics 与 Monaco 的装饰器系统紧密结合在一起。

当用户在编辑器里触发补全(键入点号、按快捷键等)时,
前端会根据当前文档 URI 与光标位置发出 textDocument/completion 请求。

处理过程通常是:

  1. LSP 客户端发出请求

    • 请求参数包括:文档 URI、位置、触发上下文(键入字符或显式触发)。
  2. Language Server 返回补全结果

    • 返回一组 CompletionItemCompletionList,包含 label、kind、detail、documentation、insertText/textEdit 等。
  3. 适配到 Monaco 补全模型

    • 前端将这些 CompletionItem 转换为 monaco.languages.CompletionItem
      • label → label;
      • kind → monaco.languages.CompletionItemKind
      • documentation → documentation 字段(支持 Markdown);
      • insertText/textEdit → 对应 Monaco 的插入/替换规则。
  4. 交给 Monaco 补全 UI 展示

    • 注册一个 CompletionItemProvider,在其中调用上述 LSP 客户端逻辑并返回转换后的 items;
    • Monaco 负责弹出补全窗口、处理选择与插入。

这样,无论服务器端是 TypeScript LS、Pyright 还是其它实现,
只要遵守 LSP 的 completion 协议,前端都能用统一方式将其结果呈现在 Monaco 中。

Hover 的适配链路类似:

  1. 用户在代码上悬停或通过快捷键请求悬浮信息;
  2. 前端发出 textDocument/hover 请求(文档 URI + 位置);
  3. Language Server 返回 Hover 对象,包含 Markdown 或纯文本内容和可选范围;
  4. 前端将 Hover 结果转换为 Monaco 能理解的 hover 提供者返回值:
    • 使用 monaco.languages.registerHoverProvider
    • provideHover 实现中调用 LSP 客户端并将结果包装为 monaco.languages.Hover

Monaco 随后会在合适位置显示悬浮框,渲染 Markdown 内容,并根据范围控制展示位置。

跳转相关的适配多了一步“导航”的动作:

  • textDocument/definition 请求 → 一组 Location(URI + Range);
  • textDocument/references 请求 → 一组引用位置。

前端收到这些结果后,会:

  • 决定是直接跳转(只有一个结果时),还是打开一个列表供用户选择(多个结果时);
  • 调用 Theia 的编辑器/导航服务:
    • 打开相应文档(如果还未打开);
    • 将编辑器光标移动到指定 Range 的起始位置;
    • 视需要对结果位置进行短暂高亮。

在这一过程中,Monaco 仍负责渲染与光标、视口控制,
而 Theia 的命令/导航服务负责“跨文件/跨编辑器”的跳转行为。

很多 LSP 驱动的行为最终会暴露为 Theia 的命令,例如:

  • “转到定义”(Go to Definition);
  • “查找所有引用”(Find All References);
  • “快速修复”(Quick Fix);
  • “重命名符号”(Rename Symbol)。

这一层通常按这样的结构组织:

  • 命令注册在 CommandContribution 中,绑定到某个命令 ID;
  • 在命令的 execute 函数里,通过当前编辑器/光标上下文构造 LSP 请求;
  • 将 LSP 响应转换为编辑器行为(跳转、打开列表、应用代码编辑等);
  • 与 Monaco 的操作 API(编辑、选择、视图控制)或 UI 组件协作完成最终效果。

通过这种做法,语言智能特性可以自然融入整个 IDE 的命令/快捷键/菜单体系。

LSP 提供了一套独立于具体编辑器的语言智能接口,
Monaco 提供了承载这些智能的编辑器 UI 和交互 API,
Theia 则在中间完成两者的“对接”:

  • diagnostics → Markers 服务 → Monaco 装饰器与问题面板;
  • completion/hover/definition 等 → LSP 请求 → Monaco 语言提供者与导航操作;
  • 所有这些行为再通过 Theia 命令系统统一编排,成为用户日常使用的各种编辑功能。

理解这层适配关系,有助于在扩展中合理地利用 LSP 与 Monaco 的能力,
并在需要时自定义或扩展特定语言特性在编辑器中的呈现方式。**