Diff-Aware Buffer Redesign
Problem Statement
Split diff (side-by-side diff view) in Zed is currently built as a coordination problem. Two entirely separate editors, each with their own multibuffer, display pipeline, scroll state, and selections, are kept in sync by an elaborate synchronization layer:
-
Two multibuffers: The RHS editor has an editable
MultiBufferwith excerpts from the working buffer. The LHS editor has a read-onlyMultiBufferwith excerpts from the base text buffer (owned byBufferDiff). The sameBufferDiffentity is registered on the LHS in "inverted" mode. -
Companionstruct (display_map.rs~L232): Maintains bidirectional hash maps mapping excerpt IDs, buffer IDs, and row conversion functions between the two sides. -
Spacer blocks (
block_map.rs,spacer_blocks()~L1207): The block map inserts invisible spacer blocks on the shorter side of each hunk to keep rows aligned. This requires threading points through the full display pipeline (InlayMap → FoldMap → TabMap → WrapMap) on both sides to compute wrap counts, then computing deltas, then inserting spacers at the right positions. -
Balancing blocks: Custom blocks (diagnostics, etc.) on one side trigger mirrored "balancing" blocks on the other side.
-
SharedScrollAnchor(scroll.rs~L79): A shared scroll anchor entity is shared between both editors, with display-map-aware resolution. -
Cursor syncing (
split.rs~L732):sync_cursor_to_other_sidetranslates cursor positions between the two editors. -
Action interception: Actions on the LHS (stage, restore, open file) are intercepted and translated to RHS coordinates before execution.
-
Excerpt mirroring (
split.rs~L1871):LhsEditor::sync_path_excerptsmirrors excerpt additions, removals, and expansions from the RHS to the LHS. -
DiffTransformtree (multi_buffer.rs~L638): A secondarySumTree<DiffTransform>inMultiBufferSnapshotinterleavesBufferContentandDeletedHunkitems to inject base text into the multibuffer's coordinate space. This tree must be walked in lockstep with theexcerptstree byMultiBufferCursor. -
O(n) coordinate translation:
BufferDiffSnapshot::patch_for_buffer_rangeandpatch_for_base_text_range(buffer_diff.rs~L412-510) buildPatch<Point>(a flatVec<Edit<T>>) for coordinate translation, requiring linear traversal.
This architecture has six fundamental problems:
-
Every new editor feature must be taught about the companion system. Diagnostics, inlay hints, folds, breakpoints — anything that adds blocks or affects layout must consider how it interacts with the two-editor coordination.
-
Spacer block alignment is fragile. It reverse-engineers wrap counts through the entire display pipeline of a companion editor. Changes to any display pipeline stage can break alignment.
-
The architecture is asymmetric. The RHS is a real editor operating on a real buffer. The LHS is a synthetic read-only mirror that exists only to show the "other side." This asymmetry creates special cases throughout the codebase.
-
Coordinate translation is O(n) through
Patch<Point>, rather than O(log n) through a SumTree. -
The
DiffTransformtree and excerpt tree must be walked in lockstep, creating complex cursor logic (MultiBufferCursor~200 lines) that is a frequent source of bugs. -
sync_diff_transforms(multi_buffer.rs~L3127) is approximately 300 lines of the most complex code in the multibuffer, rebuilding theDiffTransformtree in response to buffer edits and diff recomputations.
Overview
This document describes the redesign that eliminates these problems.
The fundamental shift: Diff awareness moves from being bolted onto the multibuffer via a separate DiffTransform tree and Companion synchronization system, to being a native property of the content — baked into the summaries that flow through the SumTree, threaded as metadata through every display pipeline layer, and rendered by a single diff-aware editor.
Three architectural pillars:
-
The MultiBuffer is composed on
Entity<BufferDiff>.BufferDiffwraps a buffer and optionally holds a diff base. A buffer with no diff is the degenerate case. Different views of the same buffer can have independent diffs. -
A
DiffMappipeline stage produces a unified diff text stream. It sits between the MultiBuffer and InlayMap, introducing Removed text from the base and tagging every chunk withdiff_status. Downstream layers carry this metadata through, and some make behavioral decisions based on it (editability, fold boundaries, etc.). -
One diff-aware editor renders everything. Split view is a rendering mode of the
EditorElement, not a structural concept. There are no two synchronized editors — one editor, one multibuffer, one display pipeline, one cursor. The renderer draws two panels from the same display snapshot, routing content to left/right based ondiff_status.
Architecture Decision: Separate BufferDiff Entity
The BufferDiff entity is the fundamental unit that the MultiBuffer is composed on. It wraps an Entity<language::Buffer> and optionally holds a diff base (and secondary diff base for staging).
Rationale
Different views of the same live buffer legitimately need different diffs simultaneously:
- The regular editor shows a git diff (HEAD → working copy) for gutter indicators.
- The agent diff pane shows an action-log diff (pre-agent state → working copy) for the same buffer.
- During agent review, the action-log diff replaces the git diff on the regular editor's multibuffer (
agent_diff.rs~L1489 adds the action log's diff to the regular editor's multibuffer).
If diff state lived on language::Buffer itself, these scenarios would require mutating the buffer's diff base — clobbering the git state for every other view. The separate entity makes the diff-to-buffer association a property of the view (multibuffer), not the data (buffer).
Many current BufferDiff constructions are artifacts of the current architecture. The git UI views (commit_view, file_diff_view, multi_diff_view), the edit prediction rating modal, and similar callers all create purpose-built buffers that aren't shared with the editor. For these, setting a diff base directly on the buffer would work fine. But the action log and agent diff cases are genuine multi-diff scenarios that require independent diff entities wrapping the same live buffer.
What Stays the Same
language::Bufferis unchanged. It does not gain diff fields.text::Bufferandtext::BufferSnapshotare unchanged.- The rope, CRDT, fragment tree, and all text operations are unmodified.
What Changes
BufferDiffis restructured: base text becomes aRope(notEntity<language::Buffer>), and the hunk tree becomesSumTree<DiffRegion>. This eliminates one entity per diff compared to today.MultiBufferis composed onEntity<BufferDiff>. Itsbuffersmap becomesbuffer_diffs. Excerpt snapshots carry diff region information.- The
DiffTransformtree,Companionsystem,LhsEditor, spacer blocks, and excerpt mirroring are all eliminated. - A new
DiffMapdisplay pipeline stage handles the unified diff text stream. - The editor and
EditorElementbecome natively diff-aware. Split view is a rendering mode, not a structural concept.
Data Model
DiffRegion and the Region Tree
The relationship between buffer text and base text is described by a SumTree<DiffRegion>. Each region is classified:
- Unchanged — text present in both buffer and base (identical content).
buffer_len > 0,base_len > 0, and they are equal. - Added — text present in buffer only.
buffer_len > 0,base_len == 0. - Removed — text present in base only.
buffer_len == 0,base_len > 0.
The tree uses length-based boundaries, not anchors. Base text is a Rope with no CRDT, so anchor-based boundaries don't work for the base side.
Key invariants:
- The cumulative
buffer_lenacross all regions equals the buffer's text length. - The cumulative
base_lenacross all regions equals the base text length. - Adjacent regions never have the same
kind(they would be merged).
// In the `text` crate:
pub enum DiffRegionKind {
Unchanged,
Added,
Removed,
}
pub struct DiffRegion {
pub kind: DiffRegionKind,
pub buffer_len: usize,
pub base_len: usize,
}
Three-Dimensional Summary
DiffRegionSummary carries three TextSummary dimensions plus DiffStats:
pub struct DiffRegionSummary {
/// Cumulative buffer text extent (Unchanged + Added regions).
pub buffer: TextSummary,
/// Cumulative base text extent (Unchanged + Removed regions).
pub base: TextSummary,
/// Cumulative unified diff extent (all regions — the total vertical space).
pub diff: TextSummary,
/// Accumulated diff statistics.
pub stats: DiffStats,
}
pub struct DiffStats {
pub lines_added: u32,
pub lines_removed: u32,
pub bytes_added: usize,
pub bytes_removed: usize,
}
For each region kind:
| Kind | buffer |
base |
diff |
stats |
|---|---|---|---|---|
| Unchanged | buffer text summary | base text summary (= buffer) | buffer text summary | all zeros |
| Added | buffer text summary | zero | buffer text summary | lines/bytes from buffer |
| Removed | zero | base text summary | base text summary | lines/bytes from base |
The diff dimension for each region is whichever side has text: for Unchanged and Added it's the buffer summary, for Removed it's the base summary. The total diff extent equals buffer.lines + stats.lines_removed (equivalently base.lines + stats.lines_added). This is the unified diff height — the total vertical space needed to display the complete diff.
All three dimensions are seekable in O(log n) through the SumTree. The diff dimension is what the display pipeline operates in for vertical layout.
DiffStats in the Summary Algebra
DiffStats fields accumulate through SumTree addition (each field is simply added). This means:
- Total diff stats for a multibuffer:
snapshot.excerpts.summary().diff_stats— O(1). - Diff stats for a range: SumTree range query — O(log n).
- Diff stats per buffer in a multibuffer: walk relevant excerpts — O(log n) per excerpt.
This replaces the current O(n)-in-hunks computation used by CommitView::calculate_changed_lines and various diff stats displays.
rope::TextSummary Is Unchanged
The three-dimensional summary (DiffRegionSummary) lives at the buffer-diff level, not the rope level. Adding diff fields to rope::TextSummary would bloat every chunk summary in every rope by ~50% for a feature most ropes don't use. The rope stays lean.
BufferDiff Entity Structure
pub struct BufferDiff {
pub buffer_id: BufferId,
buffer: Entity<language::Buffer>,
diff_base: Option<DiffBaseState>,
secondary_diff_base: Option<DiffBaseState>,
diff_version: usize,
_buffer_subscription: gpui::Subscription,
}
struct DiffBaseState {
text: Rope,
regions: SumTree<DiffRegion>,
word_diffs: Vec<WordDiffHunk>,
}
Compared to today:
SumTree<InternalDiffHunk>is replaced bySumTree<DiffRegion>.Entity<language::Buffer>for base text is replaced byRope. This eliminates one entity per diff and removes the need for CRDT machinery on base text.- The
BufferDiffsubscribes to the buffer for edit-time region updates (via_buffer_subscription). - The secondary diff (for staging status) is inlined as a
DiffBaseStaterather than being a separateEntity<BufferDiff>.
Snapshot Structure
pub struct BufferDiffSnapshot {
pub buffer: language::BufferSnapshot,
diff_base: Option<DiffBaseSnapshot>,
secondary_diff_base: Option<DiffBaseSnapshot>,
diff_version: usize,
}
struct DiffBaseSnapshot {
text: Rope,
regions: SumTree<DiffRegion>,
word_diffs: Vec<WordDiffHunk>,
}
The snapshot carries the buffer snapshot alongside the diff information. When there's no diff, diff_base is None — the degenerate case. The MultiBuffer works with this snapshot type for all excerpt operations.
Degenerate Case (No Diff)
When a BufferDiff has no base text (diff_base: None):
- The region tree is empty.
diff_text_summary_for_rangereturnsbuffer: TextSummary, base: zero, diff: buffer, stats: zero.- The excerpt's
diff_base_summaryis zero anddiff_statsis zero. - The
diffdimension equals thetextdimension. - The
DiffMappipeline stage is a passthrough (zero cost).
This means every buffer can flow through the same diff-aware pipeline with no overhead when there's no diff.
MultiBuffer Composition on BufferDiff
Structural Changes
MultiBuffer's fundamental unit changes from Entity<Buffer> to Entity<BufferDiff>:
pub struct MultiBuffer {
snapshot: RefCell<MultiBufferSnapshot>,
buffer_diffs: BTreeMap<BufferId, BufferDiffState>,
// ... other fields unchanged ...
}
struct BufferDiffState {
diff: Entity<BufferDiff>,
last_version: RefCell<clock::Global>,
last_diff_version: Cell<usize>,
last_non_text_state_update_count: Cell<usize>,
excerpts: Vec<Locator>,
_subscriptions: [gpui::Subscription; 2], // buffer events + diff events
}
The diffs: TreeMap<BufferId, DiffState> map and the DiffTransform SumTree are eliminated. The add_diff() / add_inverted_diff() methods are eliminated.
Excerpt Changes
The Excerpt carries the BufferDiffSnapshot and three summary dimensions:
struct Excerpt {
id: ExcerptId,
locator: Locator,
buffer_id: BufferId,
buffer_diff: BufferDiffSnapshot,
range: Range<text::Anchor>,
max_buffer_row: BufferRow,
text_summary: TextSummary,
diff_base_summary: TextSummary,
diff_summary: TextSummary, // unified diff extent
diff_stats: DiffStats,
has_trailing_newline: bool,
}
ExcerptSummary carries all four accumulable fields:
pub struct ExcerptSummary {
excerpt_id: ExcerptId,
excerpt_locator: Option<Locator>,
widest_line_number: u32,
text: MBTextSummary,
diff_base: MBTextSummary,
diff: MBTextSummary, // unified diff extent
diff_stats: DiffStats,
}
The diff field serves as the primary vertical dimension for the display pipeline. In the degenerate case (no diff), diff == text.
API Surface
impl MultiBuffer {
/// Primary constructor — takes a BufferDiff.
pub fn singleton(buffer_diff: Entity<BufferDiff>, cx: &mut Context<Self>) -> Self;
/// Convenience — wraps a buffer in a no-diff BufferDiff.
pub fn singleton_buffer(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self;
pub fn push_excerpts(
&mut self,
buffer_diff: Entity<BufferDiff>,
ranges: impl IntoIterator<Item = ExcerptRange<Point>>,
cx: &mut Context<Self>,
) -> Vec<ExcerptId>;
/// Convenience — wraps buffer in a no-diff BufferDiff.
pub fn push_buffer_excerpts(
&mut self,
buffer: Entity<Buffer>,
ranges: impl IntoIterator<Item = ExcerptRange<Point>>,
cx: &mut Context<Self>,
) -> Vec<ExcerptId>;
}
Callers that work with diffs (editor, agent diff, git UI) use the primary Entity<BufferDiff> API. Callers that don't care about diffs (search results, scratch buffers, completion menus) use the convenience methods.
MultiBufferCursor Simplification
The current MultiBufferCursor walks two SumTrees in lockstep: the excerpt tree and the DiffTransform tree. This is ~200 lines of complex cursor logic.
In the new model, the cursor walks a single excerpt tree. The diff information is a native property of each excerpt's summary. The DiffTransform cursor, lockstep logic, DiffTransformSummary, and all associated dimension types are eliminated.
struct MultiBufferCursor<'a, MBD, BD> {
excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension<MBD>>,
cached_region: OnceCell<Option<MultiBufferRegion<'a, MBD, BD>>>,
}
Sync Path
When the buffer is edited or the diff changes, the sync path rebuilds the affected excerpt with a fresh BufferDiffSnapshot. The three summary dimensions (text, diff_base, diff) and diff_stats are recomputed from the snapshot's region tree. The sync_diff_transforms function (~300 lines) and recompute_diff_transforms_for_edit (~200 lines) are eliminated entirely.
The Unified Diff Coordinate Space
The three-dimensional summary on DiffRegionSummary provides a unified diff coordinate space that is the key to the entire design.
Why Three Dimensions
buffer— the working copy text. Offsets, points, and anchors refer to this space. This is the coordinate space of the CRDT buffer.base— the diff base text. Used for reading Removed text, coordinate translation, and staging operations.diff— the unified diff extent. This is the total vertical space needed to display the complete diff. Both sides' text contributes: Unchanged lines appear once, Added lines appear once, Removed lines appear once. The total isunchanged_lines + added_lines + removed_lines.
Vertical Space Accounting
The diff dimension answers the question: "how tall is this diff?" For a concrete example:
Unchanged: "hello\nworld\n" → 2 buffer lines, 2 base lines, 2 diff lines
Removed: "old line\n" → 0 buffer lines, 1 base line, 1 diff line
Added: "new line 1\nnew 2\n" → 2 buffer lines, 0 base lines, 2 diff lines
Unchanged: "goodbye\n" → 1 buffer line, 1 base line, 1 diff line
Total: buffer = 5 lines, base = 4 lines, diff = 6 lines.
In the unified diff stream (what the display pipeline processes):
- Row 0: "hello\n" — diff_status: Unchanged
- Row 1: "world\n" — diff_status: Unchanged
- Row 2: "old line\n" — diff_status: Removed (from base text)
- Row 3: "new line 1\n" — diff_status: Added (from buffer text)
- Row 4: "new 2\n" — diff_status: Added (from buffer text)
- Row 5: "goodbye\n" — diff_status: Unchanged
In split view rendering, each panel sees 6 rows:
- Left panel: rows 0-1 text, row 2 text (Removed, red bg), rows 3-4 gap (Added), row 5 text.
- Right panel: rows 0-1 text, row 2 gap (Removed), rows 3-4 text (Added, green bg), row 5 text.
The alignment is free — both panels have 6 rows because both are rendering from the same unified diff stream. Scroll position is one value. No synchronization logic.
Coordinate Translation
All coordinate translations are O(log n) via the region tree:
to_diff_base_offset(buffer_offset) -> base_offset— seek by buffer dimension, read base dimension.from_diff_base_offset(base_offset) -> buffer_offset— seek by base dimension, read buffer dimension.to_diff_offset(buffer_offset) -> diff_offset— seek by buffer dimension, read diff dimension.from_diff_offset(diff_offset) -> buffer_offset— seek by diff dimension, read buffer dimension.- Analogous
Pointvariants for all of the above.
How the Dimensions Flow Through the Architecture
DiffRegion SumTree
└── DiffRegionSummary { buffer, base, diff, stats }
BufferDiffSnapshot
└── Contains the region tree, provides O(log n) queries in all three dimensions
Excerpt
└── text_summary (buffer), diff_base_summary (base), diff_summary (diff), diff_stats
ExcerptSummary
└── text (buffer), diff_base (base), diff (diff), diff_stats
MultiBufferSnapshot
└── excerpts: SumTree<Excerpt> — seekable by all four accumulated dimensions
DiffMap (new pipeline stage)
└── Operates in the `diff` dimension
└── Produces unified text stream with diff_status on every chunk
InlayMap → FoldMap → TabMap → WrapMap → BlockMap
└── All carry diff_status through their Chunk types
└── Some make behavioral decisions (editability, fold boundaries)
EditorElement
└── In split mode: draws two panels from the same display snapshot
└── Routes content to left/right based on diff_status
Display Pipeline: The DiffMap
Position in the Pipeline
MultiBuffer(BufferDiff)
→ DiffMap ← NEW: produces unified diff text stream
→ InlayMap
→ FoldMap
→ TabMap
→ WrapMap
→ BlockMap
→ display rows
What the DiffMap Does
The DiffMap takes the MultiBufferSnapshot (which carries diff region information in its excerpts) and produces a unified text stream in the diff coordinate space. Each chunk in this stream carries a diff_status annotation.
For a buffer with no diff (degenerate case), the DiffMap is a zero-cost passthrough: diff coordinates equal buffer coordinates, and all chunks have diff_status: None.
For a buffer with a diff, the DiffMap:
- Iterates through excerpt diff regions in order.
- For Unchanged regions: yields the buffer text with
diff_status: Unchanged. - For Added regions: yields the buffer text with
diff_status: Added. - For Removed regions (when expanded): yields the base text (read from the diff base
Rope) withdiff_status: Removed. - For Removed regions (when collapsed in inline mode): yields nothing (the region is elided from the stream). A gutter indicator marks the hunk boundary.
DiffMap Transforms
Like other pipeline stages, the DiffMap maintains a SumTree<DiffMapTransform> with input/output summary pairs:
enum DiffMapTransform {
/// Pass-through: input excerpt text maps directly to output.
/// Covers Unchanged and Added regions (their text is in the buffer).
Isomorphic(TextSummary),
/// Inserted content from the diff base that doesn't exist in the
/// MultiBuffer's buffer coordinate space.
DiffBaseInsertion {
output: TextSummary,
buffer_id: BufferId,
base_byte_range: Range<usize>,
},
/// Elided content: a collapsed Removed region that produces no output.
/// In split view, this variant is never used (all hunks are expanded).
Elided,
}
Expand/Collapse State
The DiffMap holds the expand/collapse state for inline diff hunks:
- Inline view, collapsed hunk (default): The Removed region produces an
Elidedtransform. No base text appears in the stream. The gutter shows a diff indicator. - Inline view, expanded hunk: The Removed region produces a
DiffBaseInsertiontransform. The base text appears in the stream withdiff_status: Removed. - Split view: All hunks are always expanded. Every Removed region produces a
DiffBaseInsertion.
The editor tracks which hunks are expanded via a set of hunk identifiers (buffer anchors that survive edits). The DiffMap consults this set when building its transforms.
The sync Method
The DiffMap's sync follows the same pattern as InlayMap::sync (see inlay_map.rs ~L555):
- Takes a new
MultiBufferSnapshotand a set ofEdit<MultiBufferOffset>edits. - Rebuilds the affected portion of its transform tree.
- Produces transformed
Edit<DiffOffset>edits for the layer above (InlayMap).
When the diff base changes (new diff computation completes), the DiffMap receives this as an excerpt rebuild from the MultiBuffer. The affected regions get new transforms. In the worst case (full diff recomputation changes many regions), the DiffMap rebuild touches all transforms for that excerpt — but this is bounded by the number of diff regions, not the text size.
Diff Awareness Through the Pipeline
Lines 1–500 of 999. Load more
# Diff-Aware Buffer Redesign ## Problem Statement Split diff (side-by-side diff view) in Zed is currently built as a **coordination problem**. Two entirely separate editors, each with their own multibuffer, display pipeline, scroll state, and selections, are kept in sync by an elaborate synchronization layer: - **Two multibuffers**: The RHS editor has an editable `MultiBuffer` with excerpts from the working buffer. The LHS editor has a read-only `MultiBuffer` with excerpts from the base text buffer (owned by `BufferDiff`). The same `BufferDiff` entity is registered on the LHS in "inverted" mode. - **`Companion` struct** (`display_map.rs` ~L232): Maintains bidirectional hash maps mapping excerpt IDs, buffer IDs, and row conversion functions between the two sides. - **Spacer blocks** (`block_map.rs`, `spacer_blocks()` ~L1207): The block map inserts invisible spacer blocks on the shorter side of each hunk to keep rows aligned. This requires threading points through the full display pipeline (`InlayMap → FoldMap → TabMap → WrapMap`) on both sides to compute wrap counts, then computing deltas, then inserting spacers at the right positions. - **Balancing blocks**: Custom blocks (diagnostics, etc.) on one side trigger mirrored "balancing" blocks on the other side. - **`SharedScrollAnchor`** (`scroll.rs` ~L79): A shared scroll anchor entity is shared between both editors, with display-map-aware resolution. - **Cursor syncing** (`split.rs` ~L732): `sync_cursor_to_other_side` translates cursor positions between the two editors. - **Action interception**: Actions on the LHS (stage, restore, open file) are intercepted and translated to RHS coordinates before execution. - **Excerpt mirroring** (`split.rs` ~L1871): `LhsEditor::sync_path_excerpts` mirrors excerpt additions, removals, and expansions from the RHS to the LHS. - **`DiffTransform` tree** (`multi_buffer.rs` ~L638): A secondary `SumTree<DiffTransform>` in `MultiBufferSnapshot` interleaves `BufferContent` and `DeletedHunk` items to inject base text into the multibuffer's coordinate space. This tree must be walked in lockstep with the `excerpts` tree by `MultiBufferCursor`. - **O(n) coordinate translation**: `BufferDiffSnapshot::patch_for_buffer_range` and `patch_for_base_text_range` (`buffer_diff.rs` ~L412-510) build `Patch<Point>` (a flat `Vec<Edit<T>>`) for coordinate translation, requiring linear traversal. This architecture has six fundamental problems: 1. **Every new editor feature must be taught about the companion system.** Diagnostics, inlay hints, folds, breakpoints — anything that adds blocks or affects layout must consider how it interacts with the two-editor coordination. 2. **Spacer block alignment is fragile.** It reverse-engineers wrap counts through the entire display pipeline of a companion editor. Changes to any display pipeline stage can break alignment. 3. **The architecture is asymmetric.** The RHS is a real editor operating on a real buffer. The LHS is a synthetic read-only mirror that exists only to show the "other side." This asymmetry creates special cases throughout the codebase. 4. **Coordinate translation is O(n)** through `Patch<Point>`, rather than O(log n) through a SumTree. 5. **The `DiffTransform` tree and excerpt tree must be walked in lockstep**, creating complex cursor logic (`MultiBufferCursor` ~200 lines) that is a frequent source of bugs. 6. **`sync_diff_transforms`** (`multi_buffer.rs` ~L3127) is approximately 300 lines of the most complex code in the multibuffer, rebuilding the `DiffTransform` tree in response to buffer edits and diff recomputations. --- ## Overview This document describes the redesign that eliminates these problems. **The fundamental shift:** Diff awareness moves from being bolted onto the multibuffer via a separate `DiffTransform` tree and `Companion` synchronization system, to being a native property of the content — baked into the summaries that flow through the SumTree, threaded as metadata through every display pipeline layer, and rendered by a single diff-aware editor. Three architectural pillars: 1. **The MultiBuffer is composed on `Entity<BufferDiff>`.** `BufferDiff` wraps a buffer and optionally holds a diff base. A buffer with no diff is the degenerate case. Different views of the same buffer can have independent diffs. 2. **A `DiffMap` pipeline stage produces a unified diff text stream.** It sits between the MultiBuffer and InlayMap, introducing Removed text from the base and tagging every chunk with `diff_status`. Downstream layers carry this metadata through, and some make behavioral decisions based on it (editability, fold boundaries, etc.). 3. **One diff-aware editor renders everything.** Split view is a rendering mode of the `EditorElement`, not a structural concept. There are no two synchronized editors — one editor, one multibuffer, one display pipeline, one cursor. The renderer draws two panels from the same display snapshot, routing content to left/right based on `diff_status`. --- ## Architecture Decision: Separate `BufferDiff` Entity The `BufferDiff` entity is the fundamental unit that the `MultiBuffer` is composed on. It wraps an `Entity<language::Buffer>` and optionally holds a diff base (and secondary diff base for staging). ### Rationale Different views of the same live buffer legitimately need different diffs simultaneously: - The **regular editor** shows a git diff (HEAD → working copy) for gutter indicators. - The **agent diff pane** shows an action-log diff (pre-agent state → working copy) for the same buffer. - During **agent review**, the action-log diff replaces the git diff on the regular editor's multibuffer (`agent_diff.rs` ~L1489 adds the action log's diff to the regular editor's multibuffer). If diff state lived on `language::Buffer` itself, these scenarios would require mutating the buffer's diff base — clobbering the git state for every other view. The separate entity makes the diff-to-buffer association a property of the view (multibuffer), not the data (buffer). Many current `BufferDiff` constructions are artifacts of the current architecture. The git UI views (`commit_view`, `file_diff_view`, `multi_diff_view`), the edit prediction rating modal, and similar callers all create purpose-built buffers that aren't shared with the editor. For these, setting a diff base directly on the buffer would work fine. But the action log and agent diff cases are genuine multi-diff scenarios that require independent diff entities wrapping the same live buffer. ### What Stays the Same - `language::Buffer` is **unchanged**. It does not gain diff fields. - `text::Buffer` and `text::BufferSnapshot` are **unchanged**. - The rope, CRDT, fragment tree, and all text operations are unmodified. ### What Changes - `BufferDiff` is restructured: base text becomes a `Rope` (not `Entity<language::Buffer>`), and the hunk tree becomes `SumTree<DiffRegion>`. This eliminates one entity per diff compared to today. - `MultiBuffer` is composed on `Entity<BufferDiff>`. Its `buffers` map becomes `buffer_diffs`. Excerpt snapshots carry diff region information. - The `DiffTransform` tree, `Companion` system, `LhsEditor`, spacer blocks, and excerpt mirroring are all eliminated. - A new `DiffMap` display pipeline stage handles the unified diff text stream. - The editor and `EditorElement` become natively diff-aware. Split view is a rendering mode, not a structural concept. --- ## Data Model ### `DiffRegion` and the Region Tree The relationship between buffer text and base text is described by a `SumTree<DiffRegion>`. Each region is classified: - **Unchanged** — text present in both buffer and base (identical content). `buffer_len > 0`, `base_len > 0`, and they are equal. - **Added** — text present in buffer only. `buffer_len > 0`, `base_len == 0`. - **Removed** — text present in base only. `buffer_len == 0`, `base_len > 0`. The tree uses length-based boundaries, not anchors. Base text is a `Rope` with no CRDT, so anchor-based boundaries don't work for the base side. **Key invariants:** - The cumulative `buffer_len` across all regions equals the buffer's text length. - The cumulative `base_len` across all regions equals the base text length. - Adjacent regions never have the same `kind` (they would be merged). ```rust // In the `text` crate: pub enum DiffRegionKind { Unchanged, Added, Removed, } pub struct DiffRegion { pub kind: DiffRegionKind, pub buffer_len: usize, pub base_len: usize, } ``` ### Three-Dimensional Summary `DiffRegionSummary` carries three `TextSummary` dimensions plus `DiffStats`: ```rust pub struct DiffRegionSummary { /// Cumulative buffer text extent (Unchanged + Added regions). pub buffer: TextSummary, /// Cumulative base text extent (Unchanged + Removed regions). pub base: TextSummary, /// Cumulative unified diff extent (all regions — the total vertical space). pub diff: TextSummary, /// Accumulated diff statistics. pub stats: DiffStats, } pub struct DiffStats { pub lines_added: u32, pub lines_removed: u32, pub bytes_added: usize, pub bytes_removed: usize, } ``` For each region kind: | Kind | `buffer` | `base` | `diff` | `stats` | | --------- | ------------------- | ---------------------------- | ------------------- | ----------------------- | | Unchanged | buffer text summary | base text summary (= buffer) | buffer text summary | all zeros | | Added | buffer text summary | zero | buffer text summary | lines/bytes from buffer | | Removed | zero | base text summary | base text summary | lines/bytes from base | The `diff` dimension for each region is whichever side has text: for Unchanged and Added it's the buffer summary, for Removed it's the base summary. The total `diff` extent equals `buffer.lines + stats.lines_removed` (equivalently `base.lines + stats.lines_added`). This is the **unified diff height** — the total vertical space needed to display the complete diff. All three dimensions are seekable in O(log n) through the SumTree. The `diff` dimension is what the display pipeline operates in for vertical layout. ### `DiffStats` in the Summary Algebra `DiffStats` fields accumulate through SumTree addition (each field is simply added). This means: - Total diff stats for a multibuffer: `snapshot.excerpts.summary().diff_stats` — **O(1)**. - Diff stats for a range: SumTree range query — **O(log n)**. - Diff stats per buffer in a multibuffer: walk relevant excerpts — **O(log n) per excerpt**. This replaces the current O(n)-in-hunks computation used by `CommitView::calculate_changed_lines` and various diff stats displays. ### `rope::TextSummary` Is Unchanged The three-dimensional summary (`DiffRegionSummary`) lives at the buffer-diff level, not the rope level. Adding diff fields to `rope::TextSummary` would bloat every chunk summary in every rope by ~50% for a feature most ropes don't use. The rope stays lean. ### `BufferDiff` Entity Structure ```rust pub struct BufferDiff { pub buffer_id: BufferId, buffer: Entity<language::Buffer>, diff_base: Option<DiffBaseState>, secondary_diff_base: Option<DiffBaseState>, diff_version: usize, _buffer_subscription: gpui::Subscription, } struct DiffBaseState { text: Rope, regions: SumTree<DiffRegion>, word_diffs: Vec<WordDiffHunk>, } ``` Compared to today: - `SumTree<InternalDiffHunk>` is replaced by `SumTree<DiffRegion>`. - `Entity<language::Buffer>` for base text is replaced by `Rope`. This eliminates one entity per diff and removes the need for CRDT machinery on base text. - The `BufferDiff` subscribes to the buffer for edit-time region updates (via `_buffer_subscription`). - The secondary diff (for staging status) is inlined as a `DiffBaseState` rather than being a separate `Entity<BufferDiff>`. ### Snapshot Structure ```rust pub struct BufferDiffSnapshot { pub buffer: language::BufferSnapshot, diff_base: Option<DiffBaseSnapshot>, secondary_diff_base: Option<DiffBaseSnapshot>, diff_version: usize, } struct DiffBaseSnapshot { text: Rope, regions: SumTree<DiffRegion>, word_diffs: Vec<WordDiffHunk>, } ``` The snapshot carries the buffer snapshot alongside the diff information. When there's no diff, `diff_base` is `None` — the degenerate case. The `MultiBuffer` works with this snapshot type for all excerpt operations. ### Degenerate Case (No Diff) When a `BufferDiff` has no base text (`diff_base: None`): - The region tree is empty. - `diff_text_summary_for_range` returns `buffer: TextSummary, base: zero, diff: buffer, stats: zero`. - The excerpt's `diff_base_summary` is zero and `diff_stats` is zero. - The `diff` dimension equals the `text` dimension. - The `DiffMap` pipeline stage is a passthrough (zero cost). This means every buffer can flow through the same diff-aware pipeline with no overhead when there's no diff. --- ## MultiBuffer Composition on `BufferDiff` ### Structural Changes `MultiBuffer`'s fundamental unit changes from `Entity<Buffer>` to `Entity<BufferDiff>`: ```rust pub struct MultiBuffer { snapshot: RefCell<MultiBufferSnapshot>, buffer_diffs: BTreeMap<BufferId, BufferDiffState>, // ... other fields unchanged ... } struct BufferDiffState { diff: Entity<BufferDiff>, last_version: RefCell<clock::Global>, last_diff_version: Cell<usize>, last_non_text_state_update_count: Cell<usize>, excerpts: Vec<Locator>, _subscriptions: [gpui::Subscription; 2], // buffer events + diff events } ``` The `diffs: TreeMap<BufferId, DiffState>` map and the `DiffTransform` SumTree are eliminated. The `add_diff()` / `add_inverted_diff()` methods are eliminated. ### Excerpt Changes The `Excerpt` carries the `BufferDiffSnapshot` and three summary dimensions: ```rust struct Excerpt { id: ExcerptId, locator: Locator, buffer_id: BufferId, buffer_diff: BufferDiffSnapshot, range: Range<text::Anchor>, max_buffer_row: BufferRow, text_summary: TextSummary, diff_base_summary: TextSummary, diff_summary: TextSummary, // unified diff extent diff_stats: DiffStats, has_trailing_newline: bool, } ``` `ExcerptSummary` carries all four accumulable fields: ```rust pub struct ExcerptSummary { excerpt_id: ExcerptId, excerpt_locator: Option<Locator>, widest_line_number: u32, text: MBTextSummary, diff_base: MBTextSummary, diff: MBTextSummary, // unified diff extent diff_stats: DiffStats, } ``` The `diff` field serves as the primary vertical dimension for the display pipeline. In the degenerate case (no diff), `diff == text`. ### API Surface ```rust impl MultiBuffer { /// Primary constructor — takes a BufferDiff. pub fn singleton(buffer_diff: Entity<BufferDiff>, cx: &mut Context<Self>) -> Self; /// Convenience — wraps a buffer in a no-diff BufferDiff. pub fn singleton_buffer(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self; pub fn push_excerpts( &mut self, buffer_diff: Entity<BufferDiff>, ranges: impl IntoIterator<Item = ExcerptRange<Point>>, cx: &mut Context<Self>, ) -> Vec<ExcerptId>; /// Convenience — wraps buffer in a no-diff BufferDiff. pub fn push_buffer_excerpts( &mut self, buffer: Entity<Buffer>, ranges: impl IntoIterator<Item = ExcerptRange<Point>>, cx: &mut Context<Self>, ) -> Vec<ExcerptId>; } ``` Callers that work with diffs (editor, agent diff, git UI) use the primary `Entity<BufferDiff>` API. Callers that don't care about diffs (search results, scratch buffers, completion menus) use the convenience methods. ### `MultiBufferCursor` Simplification The current `MultiBufferCursor` walks two SumTrees in lockstep: the excerpt tree and the `DiffTransform` tree. This is ~200 lines of complex cursor logic. In the new model, the cursor walks a single excerpt tree. The diff information is a native property of each excerpt's summary. The `DiffTransform` cursor, lockstep logic, `DiffTransformSummary`, and all associated dimension types are eliminated. ```rust struct MultiBufferCursor<'a, MBD, BD> { excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension<MBD>>, cached_region: OnceCell<Option<MultiBufferRegion<'a, MBD, BD>>>, } ``` ### Sync Path When the buffer is edited or the diff changes, the sync path rebuilds the affected excerpt with a fresh `BufferDiffSnapshot`. The three summary dimensions (`text`, `diff_base`, `diff`) and `diff_stats` are recomputed from the snapshot's region tree. The `sync_diff_transforms` function (~300 lines) and `recompute_diff_transforms_for_edit` (~200 lines) are eliminated entirely. --- ## The Unified Diff Coordinate Space The three-dimensional summary on `DiffRegionSummary` provides a **unified diff coordinate space** that is the key to the entire design. ### Why Three Dimensions - **`buffer`** — the working copy text. Offsets, points, and anchors refer to this space. This is the coordinate space of the CRDT buffer. - **`base`** — the diff base text. Used for reading Removed text, coordinate translation, and staging operations. - **`diff`** — the unified diff extent. This is the total vertical space needed to display the complete diff. Both sides' text contributes: Unchanged lines appear once, Added lines appear once, Removed lines appear once. The total is `unchanged_lines + added_lines + removed_lines`. ### Vertical Space Accounting The `diff` dimension answers the question: "how tall is this diff?" For a concrete example: ``` Unchanged: "hello\nworld\n" → 2 buffer lines, 2 base lines, 2 diff lines Removed: "old line\n" → 0 buffer lines, 1 base line, 1 diff line Added: "new line 1\nnew 2\n" → 2 buffer lines, 0 base lines, 2 diff lines Unchanged: "goodbye\n" → 1 buffer line, 1 base line, 1 diff line ``` Total: buffer = 5 lines, base = 4 lines, diff = 6 lines. In the unified diff stream (what the display pipeline processes): - Row 0: "hello\n" — diff_status: Unchanged - Row 1: "world\n" — diff_status: Unchanged - Row 2: "old line\n" — diff_status: Removed (from base text) - Row 3: "new line 1\n" — diff_status: Added (from buffer text) - Row 4: "new 2\n" — diff_status: Added (from buffer text) - Row 5: "goodbye\n" — diff_status: Unchanged In split view rendering, each panel sees 6 rows: - Left panel: rows 0-1 text, row 2 text (Removed, red bg), rows 3-4 gap (Added), row 5 text. - Right panel: rows 0-1 text, row 2 gap (Removed), rows 3-4 text (Added, green bg), row 5 text. The alignment is free — both panels have 6 rows because both are rendering from the same unified diff stream. Scroll position is one value. No synchronization logic. ### Coordinate Translation All coordinate translations are O(log n) via the region tree: - `to_diff_base_offset(buffer_offset) -> base_offset` — seek by buffer dimension, read base dimension. - `from_diff_base_offset(base_offset) -> buffer_offset` — seek by base dimension, read buffer dimension. - `to_diff_offset(buffer_offset) -> diff_offset` — seek by buffer dimension, read diff dimension. - `from_diff_offset(diff_offset) -> buffer_offset` — seek by diff dimension, read buffer dimension. - Analogous `Point` variants for all of the above. ### How the Dimensions Flow Through the Architecture ``` DiffRegion SumTree └── DiffRegionSummary { buffer, base, diff, stats } BufferDiffSnapshot └── Contains the region tree, provides O(log n) queries in all three dimensions Excerpt └── text_summary (buffer), diff_base_summary (base), diff_summary (diff), diff_stats ExcerptSummary └── text (buffer), diff_base (base), diff (diff), diff_stats MultiBufferSnapshot └── excerpts: SumTree<Excerpt> — seekable by all four accumulated dimensions DiffMap (new pipeline stage) └── Operates in the `diff` dimension └── Produces unified text stream with diff_status on every chunk InlayMap → FoldMap → TabMap → WrapMap → BlockMap └── All carry diff_status through their Chunk types └── Some make behavioral decisions (editability, fold boundaries) EditorElement └── In split mode: draws two panels from the same display snapshot └── Routes content to left/right based on diff_status ``` --- ## Display Pipeline: The `DiffMap` ### Position in the Pipeline ``` MultiBuffer(BufferDiff) → DiffMap ← NEW: produces unified diff text stream → InlayMap → FoldMap → TabMap → WrapMap → BlockMap → display rows ``` ### What the DiffMap Does The DiffMap takes the `MultiBufferSnapshot` (which carries diff region information in its excerpts) and produces a unified text stream in the `diff` coordinate space. Each chunk in this stream carries a `diff_status` annotation. For a buffer with no diff (degenerate case), the DiffMap is a zero-cost passthrough: diff coordinates equal buffer coordinates, and all chunks have `diff_status: None`. For a buffer with a diff, the DiffMap: 1. **Iterates through excerpt diff regions** in order. 2. For **Unchanged** regions: yields the buffer text with `diff_status: Unchanged`. 3. For **Added** regions: yields the buffer text with `diff_status: Added`. 4. For **Removed** regions (when expanded): yields the base text (read from the diff base `Rope`) with `diff_status: Removed`. 5. For **Removed** regions (when collapsed in inline mode): yields nothing (the region is elided from the stream). A gutter indicator marks the hunk boundary. ### DiffMap Transforms Like other pipeline stages, the DiffMap maintains a `SumTree<DiffMapTransform>` with input/output summary pairs: ```rust enum DiffMapTransform { /// Pass-through: input excerpt text maps directly to output. /// Covers Unchanged and Added regions (their text is in the buffer). Isomorphic(TextSummary), /// Inserted content from the diff base that doesn't exist in the /// MultiBuffer's buffer coordinate space. DiffBaseInsertion { output: TextSummary, buffer_id: BufferId, base_byte_range: Range<usize>, }, /// Elided content: a collapsed Removed region that produces no output. /// In split view, this variant is never used (all hunks are expanded). Elided, } ``` ### Expand/Collapse State The DiffMap holds the expand/collapse state for inline diff hunks: - **Inline view, collapsed hunk (default):** The Removed region produces an `Elided` transform. No base text appears in the stream. The gutter shows a diff indicator. - **Inline view, expanded hunk:** The Removed region produces a `DiffBaseInsertion` transform. The base text appears in the stream with `diff_status: Removed`. - **Split view:** All hunks are always expanded. Every Removed region produces a `DiffBaseInsertion`. The editor tracks which hunks are expanded via a set of hunk identifiers (buffer anchors that survive edits). The DiffMap consults this set when building its transforms. ### The `sync` Method The DiffMap's `sync` follows the same pattern as `InlayMap::sync` (see `inlay_map.rs` ~L555): 1. Takes a new `MultiBufferSnapshot` and a set of `Edit<MultiBufferOffset>` edits. 2. Rebuilds the affected portion of its transform tree. 3. Produces transformed `Edit<DiffOffset>` edits for the layer above (InlayMap). When the diff base changes (new diff computation completes), the DiffMap receives this as an excerpt rebuild from the MultiBuffer. The affected regions get new transforms. In the worst case (full diff recomputation changes many regions), the DiffMap rebuild touches all transforms for that excerpt — but this is bounded by the number of diff regions, not the text size. --- ## Diff Awareness Through the Pipeline