Skip to content

第 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

rust
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 的思路是:

  1. 尽早建立 watcher(平台允许时);
  2. 后台 scanner 枚举目录并构造新 LocalSnapshot;
  3. watcher event 与 scan job 通过队列串行归并;
  4. 使用 scan id / completed scan id 表达进度;
  5. watcher 报 rescan 时重新扫描受影响范围;
  6. 每次发布 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 协调器

打开文件的高层链路:

text
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_localis_via_remote_serveris_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 都使用类似模式:

text
后台或远端产生变化
  → 前台 Entity 原子更新当前 Snapshot
  → emit 精确 Event / notify
  → 消费者读取新的不可变 Snapshot

Event 告诉“为什么醒来”,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 边界极多。

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