第 2 章:241 个 crate 如何分层
读大型 Rust workspace 的第一原则:Cargo package 是依赖治理单元,不一定是运行时进程, 更不一定对应一个用户功能。
1. 为什么会有这么多 crate
Zed 的 workspace 把下列变化原因分开:
- 平台变化:macOS、Linux、Windows、Web/WGPU;
- 产品变化:Editor、Workspace、Agent、Git UI;
- 协议变化:LSP、DAP、ACP、内部 Proto;
- 可替换后端:HTTP client、语言模型 provider、远程 transport;
- 编译边界:宏、schema generator、extension API;
- 测试与 benchmark:fake fs、test app、eval CLI、bench crates。
crate 多的收益不是“目录整齐”,而是可以让底层在不依赖产品 UI 的情况下被复用和测试。
2. 依赖漏斗
把 241 个 crate 压缩成一张依赖漏斗:
zed (binary / composition root)
┌────────────────────────────────────────┐
│ workspace / editor / agent_ui / git_ui │
└──────────────────┬─────────────────────┘
┌────────────────────────┴──────────────────────────┐
│ project / agent / extension_host / remote / client│
└────────────────────────┬──────────────────────────┘
┌───────────────────────┴────────────────────────┐
│ language / multi_buffer / lsp / git / task / rpc│
└───────────────────────┬────────────────────────┘
┌────────────────────┴─────────────────────┐
│ text / rope / sum_tree / clock / fs / gpui│
└──────────────────────────────────────────┘并非每条 Cargo 依赖都严格服从这张简化图,但它足以判断阅读方向:如果底层 crate 开始依赖 具体 Panel 或 Workspace,通常意味着边界正在泄漏。
3. 第一组:GPUI 家族
| crate | 边界 |
|---|---|
gpui | App、Entity、Window、Element、Scene、input、keymap |
gpui_platform | 当前平台选择与公共平台接口 |
gpui_macos/linux/windows | OS 窗口、输入、剪贴板、显示器、字体接入 |
gpui_wgpu | GPU 渲染后端 |
gpui_tokio | Tokio runtime 与 GPUI executor 桥接 |
gpui_macros | Render、AppContext、测试等过程宏 |
ui | Zed 产品级组件库和设计 token |
gpui 与 ui 要分开理解:前者像应用运行时和渲染框架,后者是 Zed 的 Button、Label、 Popover、List 等产品组件。
4. 第二组:文本与编辑器家族
sum_tree
└─ rope
└─ text
└─ language::Buffer
└─ multi_buffer
└─ editorsum_tree
持久化 B+ tree。叶子存 Item,内部节点存 Summary;任何 Dimension 都能借 Summary 定位。
rope
把字符串切成 Chunk 存入 SumTree,同时摘要 byte、UTF-16、行列等维度。
text
在 Rope 上增加协同 operation、Lamport/版本向量、Anchor、transaction、undo/redo。
language
把文本 Buffer 包装成源码 Buffer:文件状态、语言、Tree-sitter、diagnostics、remote selections。
multi_buffer
把一个或多个 Buffer 的 excerpt 拼成一份可编辑视图,是 project search、diff、diagnostics 和 普通文件共享 Editor 的关键。
editor
选择、输入、DisplayMap、补全、hover、code action、导航和最终 EditorElement。
5. 第三组:Project 家族
project 很大,因为它本质上是多个 store 的组合:
Project
├─ WorktreeStore 文件树与扫描
├─ BufferStore 打开、创建、保存、同步 Buffer
├─ LspStore server 生命周期与请求路由
├─ GitStore repo、status、diff、stage/commit
├─ TaskStore task source、resolve、spawn
├─ DapStore debug adapter 与 session
├─ ToolchainStore 语言工具链选择
├─ ContextServerStore MCP/context server
└─ AgentServerStore 外部 Agent server为什么不拆成完全独立的顶级 service?因为这些 store 共享 worktree id、project path、buffer 版本和 remote client。Project 负责稳定地提供跨 store 的高层操作。
6. 第四组:协议与分布式家族
| crate | 解决的问题 |
|---|---|
proto | Protobuf 类型、typed envelope、消息宏 |
rpc | Peer、request/response、stream、handler、WebSocket framing |
client | 登录、连接状态机、重连、全局服务客户端 |
collab | 云端协作服务器、房间、项目转发、数据库 |
channel / call | 频道 buffer、语音房间、参与者状态 |
remote | SSH/WSL/Docker transport、远端 server 获取与连接池 |
remote_server | 无头远端进程,承载 Project/LSP/Git/terminal |
Collab 与 Remote 的共同点是“UI 在本地,工程能力在别处”;差别是 trust/domain:Collab 经云端 房间转发并带多人角色,Remote 是用户控制的主机,通常点对点启动 remote server。
7. 第五组:AI 家族
| crate | 角色 |
|---|---|
language_model_core | 与 GPUI 无关的消息、usage、错误等核心类型 |
language_model | LanguageModel / Provider trait 与 registry |
language_models | Anthropic/OpenAI/Google/Ollama 等 provider 适配 |
agent | Thread loop、工具、权限、沙箱、持久化、compaction |
acp_thread | Agent Client Protocol 会话与 UI 中立表示 |
agent_servers | 外部 ACP Agent 的启动和连接 |
agent_ui | Panel、ConversationView、消息、diff、审批、模型选择 |
edit_prediction* | 行内预测的模型、上下文、指标、UI |
context_server | MCP 风格 context server transport 与 protocol |
这组边界说明 Zed 同时支持两种 Agent:内置 NativeAgent 直接使用 agent::Thread,外部 Agent 经 ACP 接入。acp_thread 把两者投影成 UI 能消费的共同会话模型。
8. 第六组:扩展家族
扩展系统有三层 API:
extension_api:扩展作者编译到 Wasm 时看到的 Rust trait;extension:宿主内部的统一 Extension trait 和 proxy;extension_host:安装、索引、Wasmtime、WIT 版本兼容和 capability grant。
再由 language_extension、theme_extension、debug_adapter_extension 把扩展能力接回具体 registry。这样 Wasm host 不需要直接依赖所有产品子系统。
9. UI crate 为什么按功能拆
git 与 git_ui、terminal 与 terminal_view、collab 与 collab_ui 分开,是很有价值的 边界:
- 逻辑层可被 remote server 和 headless test 使用;
- UI 层依赖 GPUI、Workspace 和具体组件;
- 协议、状态机和产品布局不会互相锁死。
同样原则也解释了 edit_prediction / edit_prediction_ui 和 auto_update / auto_update_ui。
10. 如何判断一个 crate 的真正边界
不要只看名字。按顺序检查:
Cargo.toml的本地依赖指向谁;src/lib.rs或主文件 re-export 哪些类型;- 是否有
init(cx),它注册了什么; - 核心 state 是 plain struct、GPUI Entity 还是 service Arc;
- 是否有 local/remote enum 或 trait;
- test-support 暴露了哪些 fake。
11. 依赖边界的代价
241 个 crate 也带来成本:
- 初始化顺序变长,组合根承担更多装配责任;
- 跨 crate 类型需要 re-export 或协议转换;
- feature/test-support 容易形成复杂条件编译矩阵;
- 小修改可能触发较宽的增量编译;
- “真正入口”可能从 UI crate 跳到 Project store 再跳到底层 crate。
因此阅读 Zed 的正确姿势不是追求“所有文件都看过”,而是知道当前问题落在哪条数据旅程, 再横跨相关 crate。下一章就从最显式的数据旅程——启动——开始。