sketch in "inverted" diff state

Cole Miller created

Change summary

crates/buffer_diff/src/buffer_diff.rs   |  12 +
crates/multi_buffer/src/multi_buffer.rs | 181 ++++++++++++++++++++++++++
2 files changed, 190 insertions(+), 3 deletions(-)

Detailed changes

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -336,6 +336,14 @@ impl BufferDiffSnapshot {
         self.inner.hunks_intersecting_range_rev(range, buffer)
     }
 
+    pub fn hunks_intersecting_base_text_range<'a>(
+        &'a self,
+        range: Range<usize>,
+    ) -> impl 'a + Iterator<Item = DiffHunk> {
+        // FIXME
+        std::iter::empty()
+    }
+
     pub fn base_text(&self) -> &language::BufferSnapshot {
         &self.inner.base_text
     }
@@ -1060,6 +1068,7 @@ impl std::fmt::Debug for BufferDiff {
 pub enum BufferDiffEvent {
     DiffChanged {
         changed_range: Option<Range<text::Anchor>>,
+        base_text_changed_range: Option<Range<usize>>,
     },
     LanguageChanged,
     HunksStagedOrUnstaged(Option<Rope>),
@@ -1127,6 +1136,7 @@ impl BufferDiff {
             });
             cx.emit(BufferDiffEvent::DiffChanged {
                 changed_range: Some(Anchor::min_max_range_for_buffer(self.buffer_id)),
+                base_text_changed_range: todo!(),
             });
         }
     }
@@ -1154,6 +1164,7 @@ impl BufferDiff {
             let changed_range = first.buffer_range.start..last.buffer_range.end;
             cx.emit(BufferDiffEvent::DiffChanged {
                 changed_range: Some(changed_range),
+                base_text_changed_range: todo!(),
             });
         }
         new_index_text
@@ -1284,6 +1295,7 @@ impl BufferDiff {
 
         cx.emit(BufferDiffEvent::DiffChanged {
             changed_range: changed_range.clone(),
+            base_text_changed_range: todo!(),
         });
         changed_range
     }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -506,14 +506,39 @@ struct BufferState {
 
 struct DiffState {
     diff: Entity<BufferDiff>,
+    base_text_buffer_id: Option<BufferId>,
     _subscription: gpui::Subscription,
 }
 
+#[derive(Clone)]
+struct DiffStateSnapshot {
+    diff: BufferDiffSnapshot,
+    base_text_buffer_id: Option<BufferId>,
+}
+
+impl DiffStateSnapshot {
+    fn is_inverted(&self) -> bool {
+        self.base_text_buffer_id.is_some()
+    }
+}
+
+// FIXME
+impl std::ops::Deref for DiffStateSnapshot {
+    type Target = BufferDiffSnapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.diff
+    }
+}
+
 impl DiffState {
     fn new(diff: Entity<BufferDiff>, cx: &mut Context<MultiBuffer>) -> Self {
         DiffState {
             _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event {
-                BufferDiffEvent::DiffChanged { changed_range } => {
+                BufferDiffEvent::DiffChanged {
+                    changed_range,
+                    base_text_changed_range: _,
+                } => {
                     if let Some(changed_range) = changed_range.clone() {
                         this.buffer_diff_changed(diff, changed_range, cx)
                     }
@@ -523,15 +548,50 @@ impl DiffState {
                 _ => {}
             }),
             diff,
+            base_text_buffer_id: None,
+        }
+    }
+
+    fn new_inverted(
+        diff: Entity<BufferDiff>,
+        base_text_buffer_id: BufferId,
+        cx: &mut Context<MultiBuffer>,
+    ) -> Self {
+        DiffState {
+            _subscription: cx.subscribe(&diff, move |this, diff, event, cx| match event {
+                BufferDiffEvent::DiffChanged {
+                    changed_range: _,
+                    base_text_changed_range,
+                } => {
+                    if let Some(base_text_changed_range) = base_text_changed_range.clone() {
+                        this.inverted_buffer_diff_changed(
+                            diff,
+                            base_text_changed_range,
+                            base_text_buffer_id,
+                            cx,
+                        )
+                    }
+                    cx.emit(Event::BufferDiffChanged);
+                }
+                // FIXME
+                BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
+                _ => {}
+            }),
+            diff,
+            base_text_buffer_id: Some(base_text_buffer_id),
         }
     }
+
+    fn snapshot(&self, cx: &App) -> DiffStateSnapshot {
+        todo!()
+    }
 }
 
 /// The contents of a [`MultiBuffer`] at a single point in time.
 #[derive(Clone, Default)]
 pub struct MultiBufferSnapshot {
     excerpts: SumTree<Excerpt>,
-    diffs: TreeMap<BufferId, BufferDiffSnapshot>,
+    diffs: TreeMap<BufferId, DiffStateSnapshot>,
     diff_transforms: SumTree<DiffTransform>,
     non_text_state_update_count: usize,
     edit_count: usize,
@@ -2262,6 +2322,87 @@ impl MultiBuffer {
         });
     }
 
+    fn inverted_buffer_diff_changed(
+        &mut self,
+        diff: Entity<BufferDiff>,
+        diff_change_range: Range<usize>,
+        base_text_buffer_id: BufferId,
+        cx: &mut Context<Self>,
+    ) {
+        let diff = diff.read(cx);
+        let new_diff = diff.snapshot(cx);
+        let snapshot = self.snapshot.get_mut();
+        let base_text_changed = snapshot
+            .diffs
+            .get(&base_text_buffer_id)
+            .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff));
+        if base_text_changed {
+            let Some(base_text_buffer) = self.buffers.get(&base_text_buffer_id) else {
+                return;
+            };
+            base_text_buffer.buffer.update(cx, |buffer, cx| {
+                // FIXME take the rope directly
+                buffer.set_text(new_diff.base_text().text(), cx);
+            });
+        }
+        self.sync_mut(cx);
+
+        let Some(buffer_state) = self.buffers.get(&base_text_buffer_id) else {
+            return;
+        };
+        self.buffer_changed_since_sync.replace(true);
+        let mut snapshot = self.snapshot.get_mut();
+        snapshot
+            .diffs
+            .insert_or_replace(base_text_buffer_id, new_diff);
+
+        let mut excerpt_edits = Vec::new();
+        for locator in &buffer_state.excerpts {
+            let mut cursor = snapshot
+                .excerpts
+                .cursor::<Dimensions<Option<&Locator>, ExcerptOffset>>(());
+            cursor.seek_forward(&Some(locator), Bias::Left);
+            if let Some(excerpt) = cursor.item()
+                && excerpt.locator == *locator
+            {
+                let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer);
+                if diff_change_range.end < excerpt_buffer_range.start
+                    || diff_change_range.start > excerpt_buffer_range.end
+                {
+                    continue;
+                }
+                let excerpt_start = cursor.start().1;
+                let excerpt_len = excerpt.text_summary.len;
+                let diff_change_start_in_excerpt = diff_change_range
+                    .start
+                    .saturating_sub(excerpt_buffer_range.start);
+                let diff_change_end_in_excerpt = diff_change_range
+                    .end
+                    .saturating_sub(excerpt_buffer_range.start);
+                let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len);
+                let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len);
+                excerpt_edits.push(Edit {
+                    old: edit_start..edit_end,
+                    new: edit_start..edit_end,
+                });
+            }
+        }
+
+        let edits = Self::sync_diff_transforms(
+            &mut snapshot,
+            excerpt_edits,
+            DiffChangeKind::DiffUpdated {
+                base_changed: base_text_changed,
+            },
+        );
+        if !edits.is_empty() {
+            self.subscriptions.publish(edits);
+        }
+        cx.emit(Event::Edited {
+            edited_buffer: None,
+        });
+    }
+
     pub fn all_buffers(&self) -> HashSet<Entity<Buffer>> {
         self.buffers
             .values()
@@ -2418,6 +2559,8 @@ impl MultiBuffer {
         self.diffs.insert(buffer_id, DiffState::new(diff, cx));
     }
 
+    // FIXME add_inverted_diff
+
     pub fn diff_for(&self, buffer_id: BufferId) -> Option<Entity<BufferDiff>> {
         self.diffs.get(&buffer_id).map(|state| state.diff.clone())
     }
@@ -3082,9 +3225,41 @@ impl MultiBuffer {
                 let edit_buffer_end =
                     excerpt_buffer_start + edit.new.end.saturating_sub(excerpt_start);
                 let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end);
+
+                if diff.is_inverted() {
+                    for hunk in
+                        diff.hunks_intersecting_base_text_range(edit_buffer_start..edit_buffer_end)
+                    {
+                        let hunk_buffer_range = hunk.diff_base_byte_range.clone();
+                        if hunk_buffer_range.start < excerpt_buffer_start {
+                            log::trace!("skipping hunk that starts before excerpt");
+                            continue;
+                        }
+                        let hunk_excerpt_start = excerpt_start
+                            + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start);
+                        let hunk_excerpt_end = excerpt_end
+                            .min(excerpt_start + (hunk_buffer_range.end - excerpt_buffer_start));
+                        Self::push_buffer_content_transform(
+                            snapshot,
+                            new_diff_transforms,
+                            hunk_excerpt_start,
+                            *end_of_current_insert,
+                        );
+                        if !hunk_buffer_range.is_empty() {
+                            let hunk_info = DiffTransformHunkInfo {
+                                excerpt_id: excerpt.id,
+                                hunk_start_anchor: hunk.buffer_range.start,
+                                hunk_secondary_status: hunk.secondary_status,
+                            };
+                            *end_of_current_insert =
+                                Some((hunk_excerpt_end.min(excerpt_end), hunk_info));
+                        }
+                    }
+                    continue;
+                }
+
                 let edit_anchor_range =
                     buffer.anchor_before(edit_buffer_start)..buffer.anchor_after(edit_buffer_end);
-
                 for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) {
                     if hunk.is_created_file() && !all_diff_hunks_expanded {
                         continue;