LSP 协议核心与常见请求:从文档同步到补全诊断
这一篇把 LSP 协议层面的几个关键部分拆开讲:基本消息结构、文档同步、常见语言特性请求,以及它们在一个编辑器里的调用顺序。
消息结构与传输方式概览
LSP 使用 JSON-RPC 2.0 作为消息格式,在传输层一般采用:
- 标准输入输出(stdio)
- TCP 套接字
- WebSocket(在浏览器环境下常见)
消息分三类:
- 请求(request):有
id,需要服务器返回结果,例如textDocument/completion; - 响应(response):对应请求的结果,包含
id与result/error; - 通知(notification):无
id,不期望响应,例如textDocument/didOpen。
在 Theia 这类 IDE 场景中,前端 → 后端通常通过 JSON-RPC 通道,把这些消息包装后发送给运行中的语言服务器进程。
初始化阶段:initialize / initialized
连接语言服务器后,第一件事是协商能力,形成双方对功能的共识。
客户端发送
initialize请求,带上:- 客户端信息(名称、版本);
- 工作区根路径/URI;
- 客户端支持的特性(如是否支持增量同步、代码操作、进度报告等)。
服务器返回:
- 服务器能力声明(
ServerCapabilities):- 是否支持补全(
completionProvider); - 是否支持 hover、definition、references 等;
- 文档同步模式(
textDocumentSync:none / full / incremental); - 代码操作、重命名、格式化等能力。
- 是否支持补全(
- 服务器能力声明(
完成 initialize 后,客户端会再发送一个 initialized 通知,
之后服务器可以通过诸如 workspace/configuration 等请求向客户端获取更多配置。
这一步确定了“这条 LSP 连接上能做哪些事”,
后续请求是否发送、如何发送,都依赖于这次协商的结果。
文档同步:didOpen / didChange / didSave / didClose
让语言服务器提供正确结果的前提是:它对文档的内容有一致视图。
文档同步相关通知负责保持这种一致性。
常见模式(以增量同步为例):
textDocument/didOpen:- 客户端告知服务器“打开了某个文本”,并附上完整内容;
- 包含文档 URI、语言 ID、版本号、文本内容。
textDocument/didChange:- 文档内容发生变化时发送;
- 如果协商为增量同步,则只发送变动范围与新文本;
- 每次变动都会带上递增的版本号,防止乱序更新。
textDocument/didSave:- 文档保存时触发,视
didSave能力配置,可带内容或仅通知保存事件。
- 文档保存时触发,视
textDocument/didClose:- 文档关闭时通知服务器,可用于释放服务器端资源。
在配合 Monaco 时,编辑器侧通常会:
- 监听模型打开/关闭事件,对应发送 didOpen/didClose;
- 监听内容变更事件,根据 textDocumentSync 模式组织 didChange 数据;
- 在保存动作发生时发送 didSave 通知。
补全:textDocument/completion
补全是最直观的 LSP 能力之一,请求/响应形态大致如下:
请求参数包括:
- 文档 URI;
- 光标位置(行列);
- 触发上下文(是键入触发还是通过快捷键触发)。
服务器返回:
- 一组
CompletionItem:label:显示文本;kind:类型(函数、变量、类等);detail/documentation:额外信息;insertText/textEdit:实际插入内容和范围。
- 一组
编辑器侧会把这些结果适配到自身的补全模型,例如:
- 在 Monaco 中映射为
monaco.languages.CompletionItem; - 由 Monaco 的补全 UI 组件负责展示和插入。
悬停与跳转:hover / definition / references
除了补全,日常使用最频繁的还有 hover 和跳转相关请求。
textDocument/hover:- 请求中带上文档 URI 与位置;
- 服务器返回
Hover对象,包含 Markdown/纯文本内容与可选范围; - 编辑器根据结果在光标处显示悬浮提示。
textDocument/definition:- 服务器返回一个或多个位置(
Location),包括目标文档 URI 和位置范围; - 编辑器根据结果跳转到目标位置(可能在同一文件,也可能是其它文件)。
- 服务器返回一个或多个位置(
textDocument/references:- 返回所有引用位置;
- 编辑器通常以列表形式展示,或以 inline 高亮标记。
这些请求的流程与补全类似,只是结果的呈现方式不同。
在 Theia + Monaco 的组合中,这些数据会被转换为 Monaco 能理解的编辑操作或 UI 展示形式。
诊断与问题列表:publishDiagnostics
与前面几种“请求-响应”不同,诊断发布是服务器主动发出的通知:
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 使用体验中。**