第 3 章:启动与组合根——一个产品如何被装配出来
crates/zed/src/main.rs的核心职责不是实现功能,而是决定进程模式、建立基础设施、按依赖 顺序注册能力,最后恢复或创建 Workspace。它是整套应用的 composition root。
1. 一个二进制,多种进程角色
main() 进入 GUI 前先检查特殊模式:
| 参数/条件 | 进程角色 | 为什么必须最先处理 |
|---|---|---|
| sandbox launcher | Linux 沙箱 helper | 后续参数属于被包装命令,不能被 Zed CLI 解析 |
--askpass | Git/SSH askpass bridge | 只做 socket I/O,不应启动 UI |
--crash-handler | minidump server | 独立监控主进程 |
| ETW trace 参数 | Windows trace recorder | 只负责采样与输出 |
--printenv | shell env probe | 被 CLI/安装逻辑调用 |
--dump-all-actions | 开发工具 | 输出 action catalog 后退出 |
证据:crates/zed/src/main.rs:200-269。
这个设计比为每个 helper 单独发一个二进制更方便打包,但要求分支尽早、依赖尽量少。
2. 启动分成六个阶段
阶段 1 进程分流与路径准备
↓
阶段 2 日志、trace、版本、线程池
↓
阶段 3 Application、DB、Session、单实例、Crash handler
↓
阶段 4 Fs / Settings / HTTP / Client / LanguageRegistry
↓
阶段 5 init 注册产品能力,构建 AppState
↓
阶段 6 恢复 session、处理 open request、创建 Workspace阶段 1:先保证最小可运行环境
init_paths() 创建配置、日志、扩展、数据库等目录。如果失败,Zed 仍尝试建立最小 GPUI Application,显示 critical prompt;连窗口也打不开才退到 stderr/系统通知。
这条失败路径很重要:错误 UI 不能依赖尚未成功初始化的 Settings、Theme 或 Workspace。
源码:crates/zed/src/main.rs:95-197、:1654-1678。
阶段 2:日志和并行计算先于产品对象
Zed 根据 stdout 是否为 TTY 选择日志文件或 stdout,然后初始化 tracing。全局 Rayon pool 使用 大约一半可用 CPU,并把 worker stack 设为 10 MiB。
源码:crates/zed/src/main.rs:293-328。
这里可以看出两类异步并存:
- GPUI executor/Tokio 处理有生命周期的 async I/O;
- Rayon 处理 CPU 密集并行计算。
阶段 3:先创建不会依赖 Workspace 的基础设施
build_application() 选择当前 GPUI platform,并根据实验环境变量决定是否开启 accessibility。 接着创建:
db::AppDatabase;- system/installation/session id 的后台任务;
OpenListener;- 单实例监听;
- crash handler;
- Git provider registry 与
RealFs; - keymap/config watcher;
- login shell environment 加载任务。
源码:crates/zed/src/main.rs:343-453。
阶段 4:进入 app.run,建立 App 全局世界
app.run(move |cx| { ... }) 是关键边界。闭包之前可以准备线程安全的 service;闭包内部拿到 &mut App,才能创建 Entity、设置 Global、注册 Action 和观察者。
最先注册的能力包括:
AppDatabase
→ TrustedWorktrees
→ menu / zed_actions
→ ReleaseChannel / gpui_tokio
→ SettingsStore + watcher
→ HttpClient / Fs / GitHostingRegistry
→ Extension proxy
→ Client / LanguageRegistry / NodeRuntime源码:crates/zed/src/main.rs:478-565。
顺序不是装饰。比如 languages::init 需要 LanguageRegistry、Fs 和 NodeRuntime; language_extension::init 又需要 ExtensionHostProxy 和 LanguageRegistry。
3. init(cx) 模式到底做什么
Zed 中 foo::init(cx) 通常不是构造一个大单例,而是做下列一项或多项:
- 注册 Action handler;
- 注册 Settings 类型;
- 把某个 registry 放进 Global;
- 注册 ProjectItem/Panel/Toolbar;
- 订阅全局状态变化;
- 把协议 handler 挂到 Client/RemoteClient;
- 注册语言、provider、debug adapter 或 extension proxy。
例如 zed::init(cx) 注册应用级 Hide/Quit/打开设置/打开任务等 Action,并观察 feature flag。 源码:crates/zed/src/zed.rs:191-300。
为什么不用一个自动依赖注入容器
显式 init 序列有三点优势:
- 编译器检查函数签名中的依赖;
- 启动顺序在一个文件里可审计;
- 平台和 feature 分支可以使用普通 Rust 条件编译。
代价是 main.rs 很长,而且遗漏某个 init 往往表现为“Action 不工作/类型没注册”,而非构造 阶段立刻报错。
4. AppState 是产品级依赖包
经过 Client、Fs、LanguageRegistry、NodeRuntime、Session 等准备后,启动代码构造 workspace::AppState。Workspace 和大量 UI 模块共享它,而不是各自从 Global 抓取全部依赖。
AppState 的典型成员包括:
- client / user store;
- language registry;
- fs;
- workspace store;
- session;
- node runtime;
- build/release 元数据。
AppState::set_global 同时保留了从任意应用上下文获取产品状态的入口。源码位置: crates/workspace/src/workspace.rs:1122-1193。
5. 为什么 Editor 的 init 靠后
主启动序列大致先初始化模型/Agent,再初始化 Editor、Workspace 和面板:
language_model / language_models
→ agent_ui
→ repl / recent_projects / dev_container
→ editor
→ diagnostics
→ workspace
→ file_finder / project_panel / outline_panel / tasks_ui
→ vim / terminal_view / collab_ui / git_ui / previews / settings_ui源码:crates/zed/src/main.rs:683-785。
原因不是 AI 比 Editor 更底层,而是许多 init 只是注册 provider 或类型;Workspace init 之后, 具体面板才能把自己挂到窗口/项目模型上。阅读 init 顺序时必须区分“注册定义”与“创建实例”。
6. 启动不是等所有工作完成
Zed 在 UI 可用前不会同步等待所有慢任务:
- system id、installation id、session 异步打开;
- login shell environment 后台加载;
- extension index 必要时异步重建;
- theme/language 目录 watcher 持续运行;
- 自动更新、认证、遥测在后台推进;
- worktree 初始扫描提供可等待 future,但 Workspace 可先显示。
这是一种“先建立最小一致状态,再增量补齐能力”的启动策略。
7. OpenListener 统一所有“打开”入口
文件可以从多条路径要求 Zed 打开:
- CLI 连接已有实例;
- OS
open URL; zed://深链;- 首次启动参数;
- diff path;
- remote connection request。
OpenListener 把它们归一成 RawOpenRequest / OpenRequest。app.on_open_urls 只负责投递, 后续 handle_open_request 再解析路径位置、remote options、workspace 复用策略。
源码:crates/zed/src/main.rs:357-378、:455-476、:1000-1363。
8. Session 恢复的边界
启动结尾必须在三种结果中选择:
- 明确 open request:按请求打开目标;
- 可恢复 session:重建 workspace、pane、item 和最近路径;
- 没有历史状态:创建新 workspace 或 onboarding。
恢复逻辑不会序列化任意 Rust 对象,而是通过 Workspace/Item registry 把持久化 model 转回 具体 item。这就是为什么可打开 item 要注册序列化/反序列化描述符。
9. 失败与取消路径
启动代码特别值得学习的不是 happy path,而是降级:
| 故障 | 行为 |
|---|---|
| 目录不可创建 | 最小 GPUI critical prompt |
| 日志文件失败 | 回退 stdout |
| 已有实例 | 投递 open request 或退出 |
| crash handler 不适用 | 强制 backtrace |
| shell env 尚未加载 | 通过 oneshot 在需要处等待 |
| session 数据损坏 | 记录错误,创建新 Workspace |
| 窗口创建失败 | stderr + 平台通知/退出 |
“启动成功”不是一个布尔值,而是一组能力逐步变为 available 的过程。
10. 可迁移的组合根模式
从 Zed 启动代码可以带走五条实践:
- helper 模式在解析主应用参数前分流;
- 错误展示路径只依赖最小基础设施;
- 在线程安全阶段准备 service,在 App 上下文中创建 UI entity;
- 用显式 init 顺序暴露依赖,而不是把装配分散到静态初始化;
- 慢能力异步补齐,但为依赖方提供明确的 wait/observe 接口。
下一章进入所有这些 cx、Entity、Task 背后的运行时:GPUI。