LSP 协议核心与常见请求:从文档同步到补全诊断

这一篇把 LSP 协议层面的几个关键部分拆开讲:基本消息结构、文档同步、常见语言特性请求,以及它们在一个编辑器里的调用顺序。

LSP 使用 JSON-RPC 2.0 作为消息格式,在传输层一般采用:

  • 标准输入输出(stdio)
  • TCP 套接字
  • WebSocket(在浏览器环境下常见)

消息分三类:

  • 请求(request):有 id,需要服务器返回结果,例如 textDocument/completion
  • 响应(response):对应请求的结果,包含 idresult/error
  • 通知(notification):无 id,不期望响应,例如 textDocument/didOpen

在 Theia 这类 IDE 场景中,前端 → 后端通常通过 JSON-RPC 通道,把这些消息包装后发送给运行中的语言服务器进程。

连接语言服务器后,第一件事是协商能力,形成双方对功能的共识。

  • 客户端发送 initialize 请求,带上:

    • 客户端信息(名称、版本);
    • 工作区根路径/URI;
    • 客户端支持的特性(如是否支持增量同步、代码操作、进度报告等)。
  • 服务器返回:

    • 服务器能力声明(ServerCapabilities):
      • 是否支持补全(completionProvider);
      • 是否支持 hover、definition、references 等;
      • 文档同步模式(textDocumentSync:none / full / incremental);
      • 代码操作、重命名、格式化等能力。

完成 initialize 后,客户端会再发送一个 initialized 通知,
之后服务器可以通过诸如 workspace/configuration 等请求向客户端获取更多配置。

这一步确定了“这条 LSP 连接上能做哪些事”,
后续请求是否发送、如何发送,都依赖于这次协商的结果。

让语言服务器提供正确结果的前提是:它对文档的内容有一致视图。
文档同步相关通知负责保持这种一致性。

常见模式(以增量同步为例):

  • textDocument/didOpen

    • 客户端告知服务器“打开了某个文本”,并附上完整内容;
    • 包含文档 URI、语言 ID、版本号、文本内容。
  • textDocument/didChange

    • 文档内容发生变化时发送;
    • 如果协商为增量同步,则只发送变动范围与新文本;
    • 每次变动都会带上递增的版本号,防止乱序更新。
  • textDocument/didSave

    • 文档保存时触发,视 didSave 能力配置,可带内容或仅通知保存事件。
  • textDocument/didClose

    • 文档关闭时通知服务器,可用于释放服务器端资源。

在配合 Monaco 时,编辑器侧通常会:

  • 监听模型打开/关闭事件,对应发送 didOpen/didClose;
  • 监听内容变更事件,根据 textDocumentSync 模式组织 didChange 数据;
  • 在保存动作发生时发送 didSave 通知。

补全是最直观的 LSP 能力之一,请求/响应形态大致如下:

  • 请求参数包括:

    • 文档 URI;
    • 光标位置(行列);
    • 触发上下文(是键入触发还是通过快捷键触发)。
  • 服务器返回:

    • 一组 CompletionItem
      • label:显示文本;
      • kind:类型(函数、变量、类等);
      • detail / documentation:额外信息;
      • insertText / textEdit:实际插入内容和范围。

编辑器侧会把这些结果适配到自身的补全模型,例如:

  • 在 Monaco 中映射为 monaco.languages.CompletionItem
  • 由 Monaco 的补全 UI 组件负责展示和插入。

除了补全,日常使用最频繁的还有 hover 和跳转相关请求。

  • textDocument/hover

    • 请求中带上文档 URI 与位置;
    • 服务器返回 Hover 对象,包含 Markdown/纯文本内容与可选范围;
    • 编辑器根据结果在光标处显示悬浮提示。
  • textDocument/definition

    • 服务器返回一个或多个位置(Location),包括目标文档 URI 和位置范围;
    • 编辑器根据结果跳转到目标位置(可能在同一文件,也可能是其它文件)。
  • textDocument/references

    • 返回所有引用位置;
    • 编辑器通常以列表形式展示,或以 inline 高亮标记。

这些请求的流程与补全类似,只是结果的呈现方式不同。
在 Theia + Monaco 的组合中,这些数据会被转换为 Monaco 能理解的编辑操作或 UI 展示形式。

与前面几种“请求-响应”不同,诊断发布是服务器主动发出的通知

  • textDocument/publishDiagnostics 通知包含:

    • 文档 URI;
    • 一组 diagnostics,包含范围、严重级别(Error/Warning/Info)、消息、代码等。

客户端收到后会:

  • 更新内部的诊断状态(例如 Theia 的 Markers 服务);
  • 在编辑器中通过装饰器标记错误位置;
  • 在问题面板中列出所有 diagnostics。

这就是前面 Monaco 装饰器文里提到的“Markers → 装饰器 → 视图高亮”这一条链路的上游来源。

从协议视角看,LSP 的几个核心片段可以归纳为:

  • 初始化阶段的能力协商(initialize / initialized);
  • 文档同步保障服务器端视图一致(didOpen / didChange / didSave / didClose);
  • 各类请求为编辑体验提供语言智能(completion / hover / definition / references 等);
  • 诊断发布为问题列表和编辑器标记提供数据(publishDiagnostics)。

这些消息本身与具体编辑器无关,
Theia 所做的工作是:在前端把它们接入 Monaco 和自己的 UI 系统,在后端管理语言服务器进程与通信,
从而把 LSP 这一协议层真正“落地”到完整的 IDE 使用体验中。**