Skip to content

第 7 章:协同 Buffer 与 Anchor——编辑如何变成可合并操作

text::Buffer 不是“Rope + undo”。它保存可见/已删除文本、fragment 拓扑、版本向量、 Lamport 时钟、operation 队列和 Anchor 解析信息,使本地编辑能够乱序到达后仍确定性合并。

1. 两层 Buffer

Zed 有两个关键 Buffer

类型职责
text::Buffer协同纯文本、operation、版本、undo、anchor
language::Buffer文件、语言、Tree-sitter、diagnostics、remote selection

后者字段中直接包含前者 text: TextBuffer。源码: crates/language/src/buffer.rs:100-145

这种分层让协同文本算法不依赖 Tree-sitter、LSP、文件系统或 GPUI。

2. text::Buffer 的真实状态

text
Buffer
├─ snapshot: BufferSnapshot
├─ history: operations + undo/redo + transaction
├─ deferred_ops
├─ deferred_replicas
├─ lamport_clock
├─ subscriptions
└─ waiters(等待 edit/version/anchor)

源码:crates/text/src/text.rs:59-68

Snapshot 又包含:

  • visible_textdeleted_text 两棵 Rope;
  • fragment SumTree;
  • insertion 索引;
  • undo map;
  • version vector;
  • replica id / remote buffer id / line ending。

源码:crates/text/src/text.rs:112-124

3. Fragment:文本身份不等于当前位置

初始文本会被拆成 Fragment。每个 fragment 保存:

  • 可排序 locator id;
  • 插入时间戳;
  • 在某次 insertion 中的相对 offset 和 length;
  • visible 标记;
  • deletion/undo 元数据。

初始构造过程见 crates/text/src/text.rs:755-826

为什么需要 fragment?因为并发编辑下,“第 10 个字节”会不断变化,但“某次 insertion 的第 3~6 个字节”拥有稳定身份。显示文本只是当前可见 fragment 的投影。

4. 本地编辑的五步

Buffer::edit 的主路径非常紧凑:

text
start_transaction
  → lamport_clock.tick 得到 operation timestamp
  → apply_local_edit 生成 EditOperation + Patch
  → history 保存 operation,并加入 undo
  → version.observe(timestamp)
  → end_transaction

源码:crates/text/src/text.rs:870-903

输出不是“已经改好了”这一事实而已,而是可广播的 Operation::Edit

5. Lamport 与 Global version 分工

Lamport timestamp

单个 (sequence, replica_id),为 operation 提供全序打破并发冲突。每次本地 edit tick;观察到 远端 timestamp 后推进本地 clock。

Global version

版本向量记录每个 replica 已观察到的进度,用来判断:

  • 某 operation 的依赖是否已经到齐;
  • 某次 LSP/parse 结果针对哪个版本;
  • 是否已经应用过该 operation;
  • waiter 何时可以完成。

一个决定顺序,一个表达因果知识,不能只保留其一。

6. 远端 operation 为什么可能 defer

apply_ops 对每个 op:

  1. 先放入 history;
  2. can_apply_op 检查因果依赖;
  3. 能应用则 apply_op
  4. 否则记录 deferred replica 并插入 OperationQueue;
  5. 最后不断 flush 当前已满足依赖的 deferred op。

源码:crates/text/src/text.rs:909-955

网络可以乱序,Buffer 自己必须成为因果屏障,不能指望 WebSocket 顺序替所有 replica 解决问题。

7. 并发插入如何确定顺序

应用远端 edit 时,代码会跳过与当前 edit 并发、但 Lamport timestamp 更高的 fragment,再插入 当前 fragment。源码关键分支:crates/text/src/text.rs:1027-1034

这提供确定性:两个 replica 各自在同一位置插入,所有参与者最终用同一 timestamp 规则得到同一 顺序,而不是按“谁先收到网络包”决定。

8. 删除不是立刻丢掉文本

Snapshot 分开保存 visible_textdeleted_text。Fragment 的 visibility/undo map 决定它是否 出现在当前投影。

保留已删除内容的收益:

  • 远端迟到 operation 仍能引用旧 insertion;
  • undo 可以恢复逻辑内容;
  • Anchor 能解析穿过删除区域的位置;
  • 历史版本和 edited range 可以计算。

代价是需要额外索引与压缩策略,不能把删除等同于从 String 擦除。

9. Anchor:会随编辑移动的位置

Anchor 包含 timestamp、相对 offset、Bias 和 BufferId。源码: crates/text/src/anchor.rs:8-28

假设 cursor 在 A|B

  • left-biased anchor 倾向附着 A;
  • right-biased anchor 倾向附着 B;
  • 别人在边界插入 X 后,两种 anchor 可分别解析到 A|XBAX|B 的语义位置。

为什么 UI 不应长期保存裸 offset

补全请求、diagnostic、selection、Agent edit、Git hunk 都可能跨越 await。裸 offset 在用户继续输入 后可能指向另一段文字;Anchor 能在新 Snapshot 上重新 resolve。

10. Undo 也是 operation

Operation 有 Edit 和 Undo。Undo 不把本地状态强行回滚到旧快照,而是产生一个新的、可同步的 UndoOperation,更新 UndoMap 对相关 edit 的可见性。

这意味着多人协作中的 undo 是“撤销我这组 operation 的效果”,而不是让所有人回到历史时间点。

11. Transaction 解决用户意图分组

History 保存 transaction id、包含的 edit ids、开始版本和时间范围。默认约 300ms group interval 会把连续输入合并为更自然的一次 undo;显式 transaction depth 支持复合命令原子化。

源码:crates/text/src/text.rs:126-160:217-230

上层 language::Buffer 还追踪 transaction depth 和 edit source(User/Agent/Remote),方便 UI、 格式化和 telemetry 理解变化来源。

12. language::Buffer 加了什么

text
text::Buffer
  + File / mtime / saved_version / encoding
  + Language / SyntaxMap / TreeSitterData
  + diagnostics by LanguageServerId
  + remote selections
  + completion triggers
  + capability / conflict / dirty state

源码:crates/language/src/buffer.rs:100-145

它还把非文本变化表示成 language::Operation:UpdateDiagnostics、UpdateSelections、 UpdateCompletionTriggers、UpdateLineEnding。源码::252-298

13. Parse 如何避免阻塞编辑

Buffer edit 后安排增量 parse:

  1. 获取稳定 BufferSnapshot;
  2. 后台使用 Tree-sitter 和旧 SyntaxMap 增量解析;
  3. 新 edit 到来时可以取消/替换 reparse task;
  4. 结果回到实体时检查版本并更新 syntax snapshot;
  5. emit Reparsed,下游刷新 outline/highlight/runnables。

核心是 parser 消费快照,而不是持有可变 Buffer 锁。

14. 协同 Buffer 的不变量

任何操作序列后至少要保持:

  • visible fragment 拼接等于 visible Rope;
  • deleted fragment 拼接等于 deleted Rope;
  • SumTree Summary 与子节点一致;
  • version 只前进;
  • 已观察 operation 不重复应用;
  • 所有 replica 对相同 operation 集合收敛;
  • Anchor resolve 保持顺序和 bias 语义。

这就是 text crate 大量随机网络、断连、重连、undo/redo property tests 的价值。

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