LSP:语言服务器协议的前世今生与在 Theia 中的角色
这里单独开一块讲 LSP(Language Server Protocol),因为它基本是现代编辑器/IDE 里“语言智能”的标准接口,
对理解 Theia 如何把语言能力接到 Monaco 编辑器上很关键。
LSP 出现之前:每个编辑器各写一套语言支持
在 LSP 出现之前,典型的状况是:
- 每种编辑器/IDE 各自实现语法高亮、补全、跳转定义、重构等特性;
- 每加入一门语言,就要为每个编辑器分别写一遍语言插件;
- 语言作者需要同时维护多套插件(比如 VS、Eclipse、Vim、Emacs、Sublime、VS Code……)。
这带来的问题很明显:
- 工作量成倍放大;
- 各编辑器对同一语言的支持质量参差不齐;
- 更新语言特性往往需要多个编辑器插件同时升级。
LSP 的核心思想:把“语言智能”抽到一个独立进程
LSP 的基本想法可以简化成一句话:
把语言相关的分析能力(补全、诊断、跳转等)放到一个“语言服务器”里,让不同编辑器通过统一协议来调用。
协议的核心点包括:
- 语言服务器只关心“给我文本文档 / 工程结构,我返回语言特性结果”;
- 编辑器负责文本展示、用户交互,把用户操作转译为 LSP 消息;
- 两者之间通过 JSON-RPC 形式的消息通信。
这样一来:
- 语言团队只需实现一个 Language Server,就可以被多个编辑器/IDE 复用;
- 编辑器只要实现一遍 LSP 客户端,就能接入大量语言服务器。
协议的基本结构:客户端、服务端与消息
LSP 把参与方分成两端:
- Client(客户端):通常是编辑器或 IDE,比如 VS Code、Theia;
- Server(服务器):具体语言的 Language Server,比如 TypeScript LS、Pyright、gopls 等。
常见消息类型包括:
- 生命周期相关:
initialize、initialized、shutdown、exit; - 文档同步:
textDocument/didOpen、didChange、didSave、didClose; - 语言特性请求:
- 补全:
textDocument/completion - 悬停:
textDocument/hover - 跳转定义:
textDocument/definition - 诊断发布:
textDocument/publishDiagnostics(通知)
- 补全:
协议还定义了 capability 协商机制:
客户端和服务端在 initialize 阶段互相声明支持的能力,以便按需启用/关闭特性。
LSP 与 Monaco 的关系
LSP 只是一套协议规范,本身不关心具体编辑器实现。
Monaco 则提供了承载语言特性的前端 API,比如:
- 诊断(Markers)展示;
- 补全、hover、跳转定义等的 UI 呈现;
- 语法高亮与折叠等基础功能。
在使用 LSP + Monaco 的场景中,通常的链路是:
- 编辑器(基于 Monaco)监听用户行为(比如输入、光标位置变更);
- LSP 客户端将必要的信息(文档内容、位置等)通过协议发送给 Language Server;
- 服务器返回补全项、diagnostics 或跳转位置等结果;
- 客户端再把这些结果转换为 Monaco 可以识别的形式(Markers、CompletionItem 等)。
Theia 就是在这层之上,为不同语言统一实现了“LSP 客户端 + Monaco 适配”这一套逻辑。
在 Theia 中,LSP 处在什么位置?
结合前面 Lumino / InversifyJS / Monaco 的内容,可以大致画出这样一层关系:
- Lumino:负责窗口/布局;
- Monaco:负责编辑器 UI 和基础编辑行为;
- LSP:负责语言智能(补全、诊断、导航、重构等);
- InversifyJS:负责把这些组件(编辑器服务、LSP 客户端、各语言扩展)通过依赖注入方式组织起来。
在 Theia 中,LSP 相关代码主要集中在:
- 前端:语言客户端、与 Monaco 的桥接、Markers 更新等;
- 后端:Language Server 进程管理、与实际语言服务器进程通信。
编辑器侧的一个典型调用链会是:
- 用户在 Theia 的 Monaco 编辑器中操作;
- 通过 Theia 的前端服务触发 LSP 请求;
- LSP 客户端与后端语言服务器进行通信;
- 语言服务器返回结果;
- Theia 把结果映射回 Monaco(比如加装饰器、弹出补全列表)。