Skip to content

第 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 压缩成一张依赖漏斗:

text
                       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边界
gpuiApp、Entity、Window、Element、Scene、input、keymap
gpui_platform当前平台选择与公共平台接口
gpui_macos/linux/windowsOS 窗口、输入、剪贴板、显示器、字体接入
gpui_wgpuGPU 渲染后端
gpui_tokioTokio runtime 与 GPUI executor 桥接
gpui_macrosRenderAppContext、测试等过程宏
uiZed 产品级组件库和设计 token

gpuiui 要分开理解:前者像应用运行时和渲染框架,后者是 Zed 的 Button、Label、 Popover、List 等产品组件。

4. 第二组:文本与编辑器家族

text
sum_tree
   └─ rope
       └─ text
           └─ language::Buffer
               └─ multi_buffer
                   └─ editor

sum_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 的组合:

text
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解决的问题
protoProtobuf 类型、typed envelope、消息宏
rpcPeer、request/response、stream、handler、WebSocket framing
client登录、连接状态机、重连、全局服务客户端
collab云端协作服务器、房间、项目转发、数据库
channel / call频道 buffer、语音房间、参与者状态
remoteSSH/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_modelLanguageModel / Provider trait 与 registry
language_modelsAnthropic/OpenAI/Google/Ollama 等 provider 适配
agentThread loop、工具、权限、沙箱、持久化、compaction
acp_threadAgent Client Protocol 会话与 UI 中立表示
agent_servers外部 ACP Agent 的启动和连接
agent_uiPanel、ConversationView、消息、diff、审批、模型选择
edit_prediction*行内预测的模型、上下文、指标、UI
context_serverMCP 风格 context server transport 与 protocol

这组边界说明 Zed 同时支持两种 Agent:内置 NativeAgent 直接使用 agent::Thread,外部 Agent 经 ACP 接入。acp_thread 把两者投影成 UI 能消费的共同会话模型。

8. 第六组:扩展家族

扩展系统有三层 API:

  1. extension_api:扩展作者编译到 Wasm 时看到的 Rust trait;
  2. extension:宿主内部的统一 Extension trait 和 proxy;
  3. extension_host:安装、索引、Wasmtime、WIT 版本兼容和 capability grant。

再由 language_extensiontheme_extensiondebug_adapter_extension 把扩展能力接回具体 registry。这样 Wasm host 不需要直接依赖所有产品子系统。

9. UI crate 为什么按功能拆

gitgit_uiterminalterminal_viewcollabcollab_ui 分开,是很有价值的 边界:

  • 逻辑层可被 remote server 和 headless test 使用;
  • UI 层依赖 GPUI、Workspace 和具体组件;
  • 协议、状态机和产品布局不会互相锁死。

同样原则也解释了 edit_prediction / edit_prediction_uiauto_update / auto_update_ui

10. 如何判断一个 crate 的真正边界

不要只看名字。按顺序检查:

  1. Cargo.toml 的本地依赖指向谁;
  2. src/lib.rs 或主文件 re-export 哪些类型;
  3. 是否有 init(cx),它注册了什么;
  4. 核心 state 是 plain struct、GPUI Entity 还是 service Arc;
  5. 是否有 local/remote enum 或 trait;
  6. test-support 暴露了哪些 fake。

11. 依赖边界的代价

241 个 crate 也带来成本:

  • 初始化顺序变长,组合根承担更多装配责任;
  • 跨 crate 类型需要 re-export 或协议转换;
  • feature/test-support 容易形成复杂条件编译矩阵;
  • 小修改可能触发较宽的增量编译;
  • “真正入口”可能从 UI crate 跳到 Project store 再跳到底层 crate。

因此阅读 Zed 的正确姿势不是追求“所有文件都看过”,而是知道当前问题落在哪条数据旅程, 再横跨相关 crate。下一章就从最显式的数据旅程——启动——开始。

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