Skip to content

第 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 的核心状态

text
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 建立三套坐标

一个字符至少可能有:

  1. 底层 Buffer offset/Point;
  2. Excerpt 内 offset;
  3. MultiBuffer 全局 offset/Point。

MultiBuffer Anchor 需要保存 excerpt 身份和底层 text Anchor,避免插入/删除 excerpt 后丢失语义。 坐标转换通过 Snapshot 和 SumTree Summary 完成。

4. 底层 Buffer 变化如何同步

MultiBuffer 订阅每个底层 Buffer。访问 snapshot 前会 sync

text
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
text transaction
  → language Buffer transaction
  → MultiBuffer transaction
  → ProjectTransaction(多文件)

6. DisplayMap 的六层管道

DisplayMap 模块注释给出了准确顺序:

text
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. 坐标转换为什么不能省

假设文件内容是:

text
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. 输入路径

一次普通文字输入大致经过:

text
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 三阶段中:

  1. 获取 EditorSnapshot;
  2. 根据 scroll/viewport 找可见 DisplayRow;
  3. 从 DisplaySnapshot 迭代带 style 的 chunks;
  4. 进行 text shaping、gutter/block/selection 布局;
  5. prepaint hitbox 和输入区域;
  6. paint background、glyph、cursor、diagnostic、scrollbar。

源码入口:crates/editor/src/editor.rs:1707crates/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 交给上层。

这把复杂度从:

text
每次编辑 × 全文 × 所有显示特性

变为更接近:

text
变化范围 × 受影响的显示层 + 可见 viewport

15. 可迁移的编辑器架构经验

  1. 把“文档内容”与“一个视图的摘录”分开;
  2. 每次坐标变换都使用 newtype,拒绝裸 usize 跨层;
  3. 每层都提供 Snapshot 和 Patch 变换,不共享可变内部状态;
  4. 非文本 UI 用 block/inlay 投影,不污染持久化文本;
  5. 让普通文件成为 MultiBuffer 的简单特例,而不是两条渲染路径。

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