第 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 的真实状态
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_text和deleted_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 的主路径非常紧凑:
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:
- 先放入 history;
can_apply_op检查因果依赖;- 能应用则
apply_op; - 否则记录 deferred replica 并插入 OperationQueue;
- 最后不断 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_text 和 deleted_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|XB或AX|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::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:
- 获取稳定 BufferSnapshot;
- 后台使用 Tree-sitter 和旧 SyntaxMap 增量解析;
- 新 edit 到来时可以取消/替换 reparse task;
- 结果回到实体时检查版本并更新 syntax snapshot;
- 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 的价值。