第 9 章:Project、Worktree 与文件系统——工程语义的统一门面
Worktree 回答“项目里有哪些路径”,BufferStore 回答“哪些文件已经成为活的文档”,Project 则把 Worktree、Buffer、LSP、Git、Task、Debugger 和远端协议组合成一个语义门面。
1. 三个容易混淆的对象
Fs:对操作系统 I/O 的可替换接口
dyn Fs 抽象读写、metadata、canonicalize、watch、trash、open 等能力。生产使用 RealFs,测试 使用 FakeFs。上层不直接散落 std::fs,因此 worktree scan、settings watch、extension install 都能在确定性测试环境运行。
Worktree:一个根目录的索引化快照
Worktree 维护相对路径、entry id、inode/mtime、ignore、Git repository、scan id 和 watcher 状态。 它不是文件浏览器 UI,而是所有工程能力共享的路径索引。
Project:一个或多个 Worktree 的语义集合
Project 源码注释把职责说得很清楚:tasks、LSP、collab query 与 worktree state synchronization; 它可以 local,也可以 remote。源码:crates/project/src/project.rs:210-215。
2. Worktree 为什么用 EntryId,不只用路径
路径会 rename,inode/handle 可能保持不变;symlink 可能让一个目标出现多个可见路径;remote update 需要稳定引用。ProjectEntryId 允许 UI selection、expanded state、Git repo root 和 watcher 在路径变化时继续识别同一 entry。
Worktree Snapshot 维护多个索引,以便按 id、path 和树顺序查询。
3. Local 与 Remote 是同一个 Worktree enum
pub enum Worktree {
Local(LocalWorktree),
Remote(RemoteWorktree),
}源码:crates/worktree/src/worktree.rs:95-98。
公共方法如 snapshot()、entry_for_id()、wait_for_snapshot() 在 enum 上分派:
- Local 从文件系统 scanner/watcher 更新;
- Remote 从 proto update 更新;
- 上层 Project Panel 和 path resolver 只消费 Snapshot。
这是 Zed 分布式同构最直接的例子。
4. 初始扫描与实时 watcher 如何衔接
扫描文件树有经典竞态:如果先扫描后注册 watcher,两者之间创建的文件可能永远漏掉;如果先 watch 又不去重,可能重复处理。
Zed 的思路是:
- 尽早建立 watcher(平台允许时);
- 后台 scanner 枚举目录并构造新 LocalSnapshot;
- watcher event 与 scan job 通过队列串行归并;
- 使用 scan id / completed scan id 表达进度;
- watcher 报 rescan 时重新扫描受影响范围;
- 每次发布 Snapshot + UpdatedEntriesSet。
Linux 的非递归 watcher 还要单独 watch Git refs/reftable 子目录。源码说明: crates/worktree/src/worktree.rs:3628-3669。
5. Ignore 不是 UI 过滤器
Worktree 同时处理:
.gitignore;- 全局 gitignore;
- Zed exclude settings;
- 外部 symlink target;
- hidden/ignored/private 等可见性。
这些状态影响扫描深度、搜索候选、file finder、LSP workspace folder 和 Git 展示。因此 ignore 必须 进入 Entry/Snapshot 模型,而不是 Project Panel 最后 filter 一下。
6. Worktree rename 的难点
Watcher 可能只报告 remove + create。Zed 暂存 RemovedEntries,同时按精确 path 与 inode 建索引:
- exact path 重建优先,避免 symlink alias 误判;
- inode 匹配可推断 rename,复用 EntryId;
- 找不到匹配才分配新 EntryId。
源码:crates/worktree/src/worktree.rs:283-300。
7. BufferStore:文件进入内存后的身份层
BufferStore 维护:
- BufferId → Entitylanguage::Buffer;
- ProjectPath → BufferId;
- 正在加载的 Buffer future;
- incomplete remote buffer;
- local/remote 实现;
- shared buffer 和 peer subscription。
源码:crates/project/src/buffer_store.rs:33-100。
为什么要合并并发 open
两个 UI 动作同时打开同一路径时,不应从磁盘加载两次并创建两个 Buffer。Store 把 loading task 作为共享结果缓存,后续请求等待同一 Entity。
Incomplete remote buffer
远端创建 Buffer 的 metadata、初始 state 和后续 operation 可能分批到达。Store 先保留 incomplete 对象,等版本同步完成再交给普通 open path,避免 UI 读到半初始化文档。
8. ProjectPath 为什么不是 PathBuf
ProjectPath 是 (WorktreeId, Arc<RelPath>)。它有两个重要作用:
- 防止多个 worktree 中同名相对路径冲突;
- 让 remote project 不需要伪造本地绝对路径。
只有在确定 local worktree 时,Project 才把它解析成绝对 PathBuf。
9. Project 是 Store 协调器
打开文件的高层链路:
Workspace.open_path(ProjectPath)
→ Project.open_buffer
→ BufferStore.open_buffer
├─ local: Worktree entry → Fs.load → language::Buffer
└─ remote: ProtoClient request → incomplete Buffer → sync
→ LspStore.register_buffer
→ Workspace registry 选择 Editor item保存、rename、format、references、Git diff 也从 Project 门面进入相关 Store。
10. Project 的 Local/Remote 构造差异
Local Project
持有真实 Fs、Local WorktreeStore、Local BufferStore、Local LspStore、Local GitStore、NodeRuntime, 可启动进程和语言服务器。
Remote Project
持有 RemoteClient/ProtoClient,各 Store 以 remote mode 构造。它仍在本地创建 Buffer 副本和 UI Entity,但文件、LSP、Git、terminal 副作用在 host 执行。
Collab Project
也是 remote 语义,但连接经协作 Client 和 room/project id,带 collaborator role/capability。
Project 提供 is_local、is_via_remote_server、is_via_collab 等查询,但绝大多数上层功能不需 用它们分支。
11. Trust 是工程能力的门禁
未信任 worktree 不能立即启动会执行项目代码的 LSP、task 或 environment discovery。TrustedWorktrees 维护 trust 状态;LspStore 甚至为未信任 worktree 保留 watch receiver,等 trust 事件后再继续下载/ 启动 server。
证据:crates/project/src/lsp_store.rs:440-489。
这比在每个按钮上做 UI 提醒更可靠:门禁位于真正执行副作用的 Store。
12. Snapshot + Event 的一致性模型
Worktree、Buffer、Git repository 都使用类似模式:
后台或远端产生变化
→ 前台 Entity 原子更新当前 Snapshot
→ emit 精确 Event / notify
→ 消费者读取新的不可变 SnapshotEvent 告诉“为什么醒来”,Snapshot 给“现在的完整事实”。消费者错过中间 notify 也可以从最新 Snapshot 恢复,而不必重放全部 UI event。
13. 设计取舍
收益
- local/remote 共享 Project API;
- 文件系统可 fake;
- path 身份在 rename 和多 worktree 中稳定;
- scanner、watcher、UI 用不可变 Snapshot 解耦。
复杂度
- ID、ProjectPath、absolute path、proto path 转换很多;
- store 之间存在同步顺序;
- remote incomplete state 和 reconnect 需要额外状态机;
- symlink/ignore/platform watcher 边界极多。