第 10 章:Language 与 LSP——语义能力如何进入 Buffer
LanguageRegistry 负责“这个文件是什么语言、用什么 grammar/adapter”,LspStore 负责“哪些 server 应该运行、请求发给谁、结果如何映射回当前 Buffer 版本”。
1. Language、Grammar、LspAdapter 各自是什么
| 对象 | 职责 |
|---|---|
Language | 名称、后缀、注释、indent、queries、grammar 等静态语义 |
Grammar | Tree-sitter parser language 与 query 配置 |
LspAdapter | server binary、初始化参数、workspace config、能力定制 |
LanguageServer | 已启动子进程的 JSON-RPC 客户端 |
LspStore | 生命周期、路由、Buffer 同步、local/remote 桥接 |
把 Adapter 和 Server 分开,才能让同一 Adapter 在不同 worktree/toolchain 下启动多个 server。
2. LanguageRegistry 是延迟加载目录
Registry 保存已知 Language、available grammar、LSP adapter 和加载状态。文件打开时按:
- 显式 language override;
- 文件名/后缀;
- shebang/内容匹配;
- plain text fallback;
选择 Language。
Wasm grammar 可以处于 Unloaded → Loading(waiters) → Loaded / LoadFailed 状态,多次并发请求共享 一次加载。源码:crates/language/src/language_registry.rs:71-78、:724-762。
3. Tree-sitter 与 LSP 不重复
Tree-sitter 擅长本地、增量、结构稳定的能力
- syntax highlight;
- bracket/indent;
- outline/runnable query;
- injection;
- local text object。
LSP 擅长需要项目语义的能力
- completion/hover/signature;
- definition/reference/rename;
- diagnostics/code action;
- semantic token/inlay hint;
- format。
Zed 让两者在 language::Buffer 与 Editor 上汇合,而不是让 LSP 负责所有高亮。
4. Server 身份不是只有名字
LspStore 使用 LanguageServerSeed,包含:
- worktree id;
- server name;
- toolchain;
- 会影响身份的 binary/init options settings。
动态 workspace settings 不放进 seed,因为可通过 didChangeConfiguration 更新,无需重启。
源码:crates/project/src/lsp_store.rs:266-282。
这避免“同名 rust-analyzer”在不同根目录或工具链下被错误复用。
5. 启动 server 的完整链路
Buffer 注册到 LspStore
→ 语言 + worktree + settings 计算 LaunchDisposition
→ seed 查找已存在 server
→ 检查 worktree trust
→ adapter 获取/下载 LanguageServerBinary
→ spawn process + stdio transport
→ initialize request / initialized notification
→ 注册动态 capability 和 workspace folders
→ didOpen 当前 Buffer入口:crates/project/src/lsp_store.rs:367-520。
启动本身是 LanguageServerState::Starting,完成后转 Running;请求方需要等待或在当前没有运行 server 时返回可解释的空结果。
6. Buffer 与 LSP 版本桥
LSP 文档版本通常是递增整数,Zed Buffer 使用版本向量。LspStore 为每个 buffer_id → server_id 保存 LspBufferSnapshot 历史:
- 发送 didChange 时记录对应 BufferSnapshot;
- response/diagnostic 带 LSP version 回来时找到当时 snapshot;
- 把 UTF-16 range 先解析到旧 snapshot Anchor;
- 再在当前 snapshot 上 resolve。
这解决“请求发出后用户继续输入”的常态,而不是把输入冻结到 response 返回。
7. 一个 completion 请求如何走
Editor selection(DisplayPoint)
→ MultiBuffer 映射到底层 Buffer Anchor
→ Project.completions
→ LspStore 选择该 Buffer 的 server(s)
→ Anchor 在请求 snapshot 转 PointUtf16
→ LanguageServer.request(textDocument/completion)
→ provider-specific transform / resolve
→ Completion edit range 转回 Anchor
→ Editor 过滤、排序、显示如果多个 server 都提供 completion,LspStore/Project 会保留来源 server id,后续 resolve 或 code action 才能发回正确 server。
8. request_lsp 如何统一 local/remote
Project 的各种 semantic API 最终使用 LspStore。Store 的 public request 路径判断:
- local:直接选 LanguageServer,执行 Rust typed LSP request;
- remote:把 command、buffer/version/position 转为 proto,请 host 执行;
- response 再转换为 Project 的统一类型。
所以 Editor 不直接持有 lsp::LanguageServer,否则 remote 模式无法复用。
9. Diagnostics 的两种来源
Push diagnostics
server 主动 publishDiagnostics,LspStore 关联 document/version,更新 Buffer 的 DiagnosticSet。
Pull diagnostics
客户端请求 document/workspace diagnostics,维护 result id,按 progress 或刷新信号重新拉取。
LspStore 同时追踪 disk-based source、registration id、result id 和 workspace progress,代码体量因此 远大于一个 JSON-RPC wrapper。
10. Dynamic registration
Server 可以运行时注册:
- watched files;
- formatting;
- semantic tokens;
- inlay hints;
- workspace folders 等。
LspStore 保存 registration,并把 watched glob 接到 Worktree watcher。注销时必须同时撤掉行为, 不能只更新 capability flag。
11. Format 是一个小型编排器
Project format 可能组合:
- code actions on save;
- language server formatting/range formatting;
- external formatter / Prettier;
- whitespace/line-ending处理;
- 将多个 WorkspaceEdit 反序列化成 Buffer transaction。
请求期间 Buffer 仍可编辑,所以每个 TextEdit 都要经旧 snapshot 和 Anchor 重新定位;多文件 edit 最终形成 ProjectTransaction。
12. Server 故障与重启
必须处理:
- binary 下载失败;
- initialize 超时;
- process crash;
- stderr 捕获;
- 用户 stop/restart;
- settings/toolchain 改变导致 seed 失效;
- worktree trust 撤销或 remote disconnect;
- response 晚于 server replacement。
LanguageServerId 与 state map 防止旧 server response 写入新实例状态。
13. 进度与取消
LSP work-done progress 使用 number/string token。LspStore 记录 pending work、percentage、message、 是否可取消,并通过事件投影到 status bar。完成时移除 token;用户取消则发 window/workDoneProgress/cancel。
源码:crates/project/src/lsp_store.rs:10748-10952。
14. 设计经验
- 用 Adapter 描述“如何启动”,Server 描述“已经启动的连接”;
- server identity 包含会影响进程行为的配置;
- 所有跨 await 的 position 都经 Snapshot/Anchor;
- UI 只依赖 Project 语义 API,不直接依赖本地 server;
- LSP capability 是动态状态,不是 initialize 后永远不变的常量。