第 8 章:MultiBuffer 与 DisplayMap——从源码坐标到屏幕坐标
Editor 的关键抽象不是“一个文件一个文本框”。
MultiBuffer允许一个视图拼接多个 Buffer 片段,DisplayMap再用六层变换把语义文本投影成最终可见行。
1. 为什么需要 MultiBuffer
下列视图看起来不同,却都需要选择、复制、跳转、语法高亮和可编辑能力:
- 普通文件;
- project search results;
- diagnostics 集合;
- Git diff;
- Agent 生成的多个 edit;
- references/definitions 多文件结果。
如果每类视图写专用文本控件,Editor 能力会碎片化。MultiBuffer 把它们统一成:
一组有顺序的 Excerpt;每个 Excerpt 指向某个底层 Buffer 的一个 Anchor range,并可带上下文。
2. MultiBuffer 的核心状态
MultiBuffer
├─ snapshot: MultiBufferSnapshot
├─ buffers: BufferId → BufferState
├─ diffs: BufferId → DiffState
├─ subscriptions
├─ singleton
├─ history
├─ title / capability
└─ buffer_changed_since_sync源码:crates/multi_buffer/src/multi_buffer.rs:70-92。
普通文件通常是 singleton:一个 Buffer、一个覆盖全文的 Excerpt。搜索结果则有很多 Buffer 和 Excerpt,但上层 Editor 使用同一接口。
3. Excerpt 建立三套坐标
一个字符至少可能有:
- 底层 Buffer offset/Point;
- Excerpt 内 offset;
- MultiBuffer 全局 offset/Point。
MultiBuffer Anchor 需要保存 excerpt 身份和底层 text Anchor,避免插入/删除 excerpt 后丢失语义。 坐标转换通过 Snapshot 和 SumTree Summary 完成。
4. 底层 Buffer 变化如何同步
MultiBuffer 订阅每个底层 Buffer。访问 snapshot 前会 sync:
consume Buffer subscription Patch
→ 找出该 Buffer 对应的 Excerpt
→ 把底层 edit 裁剪到 excerpt range
→ 转成 MultiBuffer coordinate Patch
→ 更新 Excerpt Summary / snapshot
→ publish 给 DisplayMap源码定位:crates/multi_buffer/src/multi_buffer.rs:2431-2520。
同一个 Buffer 可以出现多个 excerpt;一次底层 edit 可能映射成多段 MultiBuffer patch。
5. MultiBuffer 也有 transaction
跨多个 Buffer 的操作,例如 rename 或 Agent 批量 edit,需要在 UI 中成为一次 undo。MultiBuffer History 记录底层 transaction,ProjectTransaction 又能聚合多个 Buffer transaction。
因此原子性有层次:
text transaction
→ language Buffer transaction
→ MultiBuffer transaction
→ ProjectTransaction(多文件)6. DisplayMap 的六层管道
DisplayMap 模块注释给出了准确顺序:
MultiBuffer
→ InlayMap 插入 inlay / inlay hint
→ FoldMap 折叠范围变成 placeholder
→ TabMap hard tab 展开成显示列
→ WrapMap soft wrap 生成视觉行
→ BlockMap 插入 diagnostics 等自定义 block
→ DisplayMap 叠加文本 highlight源码:crates/editor/src/display_map.rs:1-65。
顺序非常重要:先插入 inlay 再 fold,决定折叠是否吞掉 inlay;tab 展开后 wrap,决定视觉列宽; block 最后插入,避免把非文本 block 当成文档内容换行。
7. 每一层都有相同的设计语法
模块注释总结了每层结构:
Transform:某段输入怎样变成输出;TransformSummary:输入/输出的 TextSummary;- 本层 coordinate newtype;
Snapshot;- coordinate conversion API;
- row/chunk iterator;
sync(snapshot, edits) -> (new_snapshot, transformed_edits)。
源码:crates/editor/src/display_map.rs:17-57。
这是第 6 章 SumTree 语言的直接应用。
8. 坐标转换为什么不能省
假设文件内容是:
let result = very_long_function(argument)显示层可能同时发生:
result后插入类型 inlay;very_long_function被 fold;- hard tab 展开;
- 窗口窄导致两次 soft wrap;
- 上方插入一个 diagnostic block。
鼠标点击第 4 个视觉行第 7 列时,必须逆向穿过 Block → Wrap → Tab → Fold → Inlay → MultiBuffer,才能得到真正 Buffer Anchor。任何一层直接使用下一层 Point 都会错。
9. Editor 保存什么状态
Editor 包含:
Entity<MultiBuffer>与Entity<DisplayMap>;- selections / newest selection;
- scroll position、autoscroll、visible line cache;
- completion/hover/context menu;
- inline completion、code action、rename 状态;
- blink/focus/input composition;
- Project 引用和 item/persistence 状态。
源码入口:crates/editor/src/editor.rs:924-1207。
EditorSnapshot 则固定一次渲染/命令所需的 Buffer、Display 和 selection 观察点。
10. 输入路径
一次普通文字输入大致经过:
platform text input / IME
→ Editor input handler
→ 当前 Display selection 逆映射到 MultiBuffer Anchor
→ Editor transaction
→ MultiBuffer.edit
→ 每个底层 language::Buffer.edit
→ text::Buffer Operation
→ selection 根据 Anchor 在新 snapshot 重解析
→ DisplayMap consume Patch
→ cx.notify 请求重绘自动缩进、pair insertion、snippet placeholder 和 linked edit 会在 Editor transaction 中扩展或 修正 edit 集合,但最终仍落到同一 Buffer operation 路径。
11. 显示路径
Editor::render 返回 EditorElement。后者在 GPUI 三阶段中:
- 获取 EditorSnapshot;
- 根据 scroll/viewport 找可见 DisplayRow;
- 从 DisplaySnapshot 迭代带 style 的 chunks;
- 进行 text shaping、gutter/block/selection 布局;
- prepaint hitbox 和输入区域;
- paint background、glyph、cursor、diagnostic、scrollbar。
源码入口:crates/editor/src/editor.rs:1707、crates/editor/src/element.rs:244。
12. Highlight 为什么是有优先级的 Key
DisplayMap 的 HighlightKey 顺序决定覆盖优先级:SemanticToken、matching bracket、search、 selection、rename、inline assist 等不能简单“后写覆盖前写”。
源码:crates/editor/src/display_map.rs:157-190。
显式 key 让不同功能独立插入/移除自己的高亮,不必共同维护一张最终 style 数组。
13. Blocks 让 Editor 超越纯文本
BlockMap 可以在文本行之间插入自定义内容,例如:
- diagnostic detail;
- Agent diff 控件;
- fold/crease 附加 UI;
- companion/split view 对齐 spacer;
- notebook 输出。
Block 不写回文件,也不污染 LSP position,却参与视觉行、滚动和 hit testing。
14. 为什么 DisplayMap 是增量链
如果每次输入都从头计算所有 inlay、fold、tab、wrap 和 block,GPU 再快也无济于事。每层 sync 接收下层 Patch,更新局部 transform,再把本层 Patch 交给上层。
这把复杂度从:
每次编辑 × 全文 × 所有显示特性变为更接近:
变化范围 × 受影响的显示层 + 可见 viewport15. 可迁移的编辑器架构经验
- 把“文档内容”与“一个视图的摘录”分开;
- 每次坐标变换都使用 newtype,拒绝裸
usize跨层; - 每层都提供 Snapshot 和 Patch 变换,不共享可变内部状态;
- 非文本 UI 用 block/inlay 投影,不污染持久化文本;
- 让普通文件成为 MultiBuffer 的简单特例,而不是两条渲染路径。