Skip to content

第 10 章:Language 与 LSP——语义能力如何进入 Buffer

LanguageRegistry 负责“这个文件是什么语言、用什么 grammar/adapter”,LspStore 负责“哪些 server 应该运行、请求发给谁、结果如何映射回当前 Buffer 版本”。

1. Language、Grammar、LspAdapter 各自是什么

对象职责
Language名称、后缀、注释、indent、queries、grammar 等静态语义
GrammarTree-sitter parser language 与 query 配置
LspAdapterserver binary、初始化参数、workspace config、能力定制
LanguageServer已启动子进程的 JSON-RPC 客户端
LspStore生命周期、路由、Buffer 同步、local/remote 桥接

把 Adapter 和 Server 分开,才能让同一 Adapter 在不同 worktree/toolchain 下启动多个 server。

2. LanguageRegistry 是延迟加载目录

Registry 保存已知 Language、available grammar、LSP adapter 和加载状态。文件打开时按:

  1. 显式 language override;
  2. 文件名/后缀;
  3. shebang/内容匹配;
  4. 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 的完整链路

text
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 请求如何走

text
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 可能组合:

  1. code actions on save;
  2. language server formatting/range formatting;
  3. external formatter / Prettier;
  4. whitespace/line-ending处理;
  5. 将多个 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. 设计经验

  1. 用 Adapter 描述“如何启动”,Server 描述“已经启动的连接”;
  2. server identity 包含会影响进程行为的配置;
  3. 所有跨 await 的 position 都经 Snapshot/Anchor;
  4. UI 只依赖 Project 语义 API,不直接依赖本地 server;
  5. LSP capability 是动态状态,不是 initialize 后永远不变的常量。

独立源码学习笔记 · 文档采用 CC BY-SA 4.0