git: Fix panic when committing from side-by-side view (#47425)

Cole Miller created

When committing, it was possible for the left-hand side multibuffer to
get the updated base text (via the `buffer_changed_since_sync`
mechanism) without updating its diff snapshot for that buffer (via the
diff subscription), causing a mismatch between that multibuffer's diff
state and its buffer state. The fix is to ensure for inverted diffs that
we always update pull an updated diff snapshot as part of
`sync_from_buffer_changes`.

This also removes some code that we added in #44838 to sync the
left-hand side multibuffer when edits on the right-hand side invalided
diff hunks. Instead, the left-hand side will just sync the next time the
diff recalculates when this happens, and will always consider hunks from
the last diff calculation as valid--so there will be a short window
where the diff transforms and `diff_hunks_in_range` don't match between
the two sides. That's okay because we don't rely on this in the display
map--the code that translates positions between the two sides accesses
the diff's `InternalDiffHunk`s directly rather than going through a
multibuffer API.

Release Notes:

- N/A

Change summary

crates/editor/src/display_map/block_map.rs    |   2 
crates/editor/src/split.rs                    |   6 
crates/multi_buffer/src/multi_buffer.rs       | 232 ++------------------
crates/multi_buffer/src/multi_buffer_tests.rs | 153 ++++++-------
4 files changed, 104 insertions(+), 289 deletions(-)

Detailed changes

crates/editor/src/display_map/block_map.rs 🔗

@@ -4222,7 +4222,7 @@ mod tests {
                 [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
                 cx,
             );
-            mb.add_inverted_diff(diff.clone(), rhs_buffer.clone(), cx);
+            mb.add_inverted_diff(diff.clone(), cx);
             mb
         });
         let rhs_multibuffer = cx.new(|cx| {

crates/editor/src/split.rs 🔗

@@ -1577,10 +1577,6 @@ impl SecondaryEditor {
             })
             .collect();
 
-        let main_buffer = primary_multibuffer_ref
-            .buffer(main_buffer.remote_id())
-            .unwrap();
-
         self.editor.update(cx, |editor, cx| {
             editor.buffer().update(cx, |buffer, cx| {
                 let (ids, _) = buffer.update_path_excerpts(
@@ -1595,7 +1591,7 @@ impl SecondaryEditor {
                         .diff_for(base_text_buffer.read(cx).remote_id())
                         .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
                 {
-                    buffer.add_inverted_diff(diff, main_buffer, cx);
+                    buffer.add_inverted_diff(diff, cx);
                 }
             })
         });

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -15,7 +15,7 @@ use buffer_diff::{
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet};
-use gpui::{App, Context, Entity, EntityId, EventEmitter, WeakEntity};
+use gpui::{App, Context, Entity, EntityId, EventEmitter};
 use itertools::Itertools;
 use language::{
     AutoindentMode, BracketMatch, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability,
@@ -499,10 +499,7 @@ struct BufferState {
 
 struct DiffState {
     diff: Entity<BufferDiff>,
-    /// If set, this diff is "inverted" (i.e. showing additions as deletions).
-    /// This is used in the side-by-side diff view. The main_buffer is the
-    /// editable buffer, while the excerpt shows the base text.
-    main_buffer: Option<WeakEntity<Buffer>>,
+    is_inverted: bool,
     _subscription: gpui::Subscription,
 }
 
@@ -510,11 +507,7 @@ impl DiffState {
     fn snapshot(&self, cx: &App) -> DiffStateSnapshot {
         DiffStateSnapshot {
             diff: self.diff.read(cx).snapshot(cx),
-            main_buffer: self.main_buffer.as_ref().and_then(|main_buffer| {
-                main_buffer
-                    .read_with(cx, |main_buffer, _| main_buffer.text_snapshot())
-                    .ok()
-            }),
+            is_inverted: self.is_inverted,
         }
     }
 }
@@ -522,7 +515,7 @@ impl DiffState {
 #[derive(Clone)]
 struct DiffStateSnapshot {
     diff: BufferDiffSnapshot,
-    main_buffer: Option<text::BufferSnapshot>,
+    is_inverted: bool,
 }
 
 impl std::ops::Deref for DiffStateSnapshot {
@@ -557,19 +550,13 @@ impl DiffState {
                 _ => {}
             }),
             diff,
-            main_buffer: None,
+            is_inverted: false,
         }
     }
 
-    fn new_inverted(
-        diff: Entity<BufferDiff>,
-        main_buffer: Entity<Buffer>,
-        cx: &mut Context<MultiBuffer>,
-    ) -> Self {
-        let main_buffer = main_buffer.downgrade();
+    fn new_inverted(diff: Entity<BufferDiff>, cx: &mut Context<MultiBuffer>) -> Self {
         DiffState {
             _subscription: cx.subscribe(&diff, {
-                let main_buffer = main_buffer.clone();
                 move |this, diff, event, cx| match event {
                     BufferDiffEvent::DiffChanged(DiffChanged {
                         changed_range: _,
@@ -577,23 +564,18 @@ impl DiffState {
                         extended_range: _,
                     }) => {
                         if let Some(base_text_changed_range) = base_text_changed_range.clone() {
-                            this.inverted_buffer_diff_changed(
-                                diff,
-                                base_text_changed_range,
-                                main_buffer.clone(),
-                                cx,
-                            )
+                            this.inverted_buffer_diff_changed(diff, base_text_changed_range, cx)
                         }
                         cx.emit(Event::BufferDiffChanged);
                     }
                     BufferDiffEvent::LanguageChanged => {
-                        this.inverted_buffer_diff_language_changed(diff, main_buffer.clone(), cx)
+                        this.inverted_buffer_diff_language_changed(diff, cx)
                     }
                     _ => {}
                 }
             }),
             diff,
-            main_buffer: Some(main_buffer),
+            is_inverted: true,
         }
     }
 }
@@ -2223,10 +2205,7 @@ impl MultiBuffer {
 
         // Recalculate has_inverted_diff after removing diffs
         if !removed_buffer_ids.is_empty() {
-            snapshot.has_inverted_diff = snapshot
-                .diffs
-                .iter()
-                .any(|(_, diff)| diff.main_buffer.is_some());
+            snapshot.has_inverted_diff = snapshot.diffs.iter().any(|(_, diff)| diff.is_inverted);
         }
 
         if changed_trailing_excerpt {
@@ -2328,7 +2307,7 @@ impl MultiBuffer {
         let buffer_id = diff.buffer_id;
         let diff = DiffStateSnapshot {
             diff: diff.snapshot(cx),
-            main_buffer: None,
+            is_inverted: false,
         };
         self.snapshot.get_mut().diffs.insert(buffer_id, diff);
     }
@@ -2336,16 +2315,13 @@ impl MultiBuffer {
     fn inverted_buffer_diff_language_changed(
         &mut self,
         diff: Entity<BufferDiff>,
-        main_buffer: WeakEntity<Buffer>,
         cx: &mut Context<Self>,
     ) {
         let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id();
         let diff = diff.read(cx);
         let diff = DiffStateSnapshot {
             diff: diff.snapshot(cx),
-            main_buffer: main_buffer
-                .update(cx, |main_buffer, _| main_buffer.text_snapshot())
-                .ok(),
+            is_inverted: true,
         };
         self.snapshot
             .get_mut()
@@ -2369,7 +2345,7 @@ impl MultiBuffer {
         };
         let new_diff = DiffStateSnapshot {
             diff: diff.snapshot(cx),
-            main_buffer: None,
+            is_inverted: false,
         };
         let mut snapshot = self.snapshot.get_mut();
         let base_text_changed = snapshot
@@ -2401,7 +2377,6 @@ impl MultiBuffer {
         &mut self,
         diff: Entity<BufferDiff>,
         diff_change_range: Range<usize>,
-        main_buffer: WeakEntity<Buffer>,
         cx: &mut Context<Self>,
     ) {
         self.sync_mut(cx);
@@ -2411,13 +2386,10 @@ impl MultiBuffer {
         let Some(buffer_state) = self.buffers.get(&base_text_buffer_id) else {
             return;
         };
-        self.buffer_changed_since_sync.replace(true);
 
         let new_diff = DiffStateSnapshot {
             diff: diff.snapshot(cx),
-            main_buffer: main_buffer
-                .update(cx, |main_buffer, _| main_buffer.text_snapshot())
-                .ok(),
+            is_inverted: true,
         };
         let mut snapshot = self.snapshot.get_mut();
         snapshot
@@ -2429,7 +2401,7 @@ impl MultiBuffer {
             &mut snapshot,
             excerpt_edits,
             DiffChangeKind::DiffUpdated {
-                // We don't use read this field for inverted diffs.
+                // We don't read this field for inverted diffs.
                 base_changed: false,
             },
         );
@@ -2602,28 +2574,13 @@ impl MultiBuffer {
         self.diffs.insert(buffer_id, DiffState::new(diff, cx));
     }
 
-    pub fn add_inverted_diff(
-        &mut self,
-        diff: Entity<BufferDiff>,
-        main_buffer: Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) {
+    pub fn add_inverted_diff(&mut self, diff: Entity<BufferDiff>, cx: &mut Context<Self>) {
         let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id();
         let diff_change_range = 0..diff.read(cx).base_text(cx).len();
         self.snapshot.get_mut().has_inverted_diff = true;
-        main_buffer.update(cx, |buffer, _| {
-            buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync));
-        });
-        self.inverted_buffer_diff_changed(
-            diff.clone(),
-            diff_change_range,
-            main_buffer.downgrade(),
-            cx,
-        );
-        self.diffs.insert(
-            base_text_buffer_id,
-            DiffState::new_inverted(diff, main_buffer, cx),
-        );
+        self.inverted_buffer_diff_changed(diff.clone(), diff_change_range, cx);
+        self.diffs
+            .insert(base_text_buffer_id, DiffState::new_inverted(diff, cx));
     }
 
     pub fn diff_for(&self, buffer_id: BufferId) -> Option<Entity<BufferDiff>> {
@@ -3048,71 +3005,12 @@ impl MultiBuffer {
         }
 
         for (id, diff) in diffs.iter() {
-            if buffer_diff.get(id).is_none() {
+            if buffer_diff.get(id).is_none() || diff.is_inverted {
                 buffer_diff.insert(*id, diff.snapshot(cx));
             }
         }
 
-        // Check for main buffer changes in inverted diffs
-        let mut main_buffer_changed_diffs = Vec::new();
-        for (id, diff_state) in diffs.iter() {
-            if let Some(main_buffer) = &diff_state.main_buffer {
-                if let Ok(current_main_buffer) =
-                    main_buffer.read_with(cx, |buffer, _| buffer.text_snapshot())
-                {
-                    if let Some(stored_diff) = buffer_diff.get(id) {
-                        if let Some(stored_main_buffer) = &stored_diff.main_buffer {
-                            if current_main_buffer
-                                .version()
-                                .changed_since(stored_main_buffer.version())
-                            {
-                                main_buffer_changed_diffs.push((
-                                    *id,
-                                    stored_diff.clone(),
-                                    current_main_buffer,
-                                ));
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        let mut inverted_diff_touch_info: HashMap<
-            Locator,
-            (
-                BufferDiffSnapshot,
-                text::BufferSnapshot,
-                text::BufferSnapshot,
-            ),
-        > = HashMap::default();
-        for (buffer_id, old_diff_snapshot, new_main_buffer) in &main_buffer_changed_diffs {
-            if let Some(old_main_buffer) = &old_diff_snapshot.main_buffer {
-                if let Some(buffer_state) = buffers.get(buffer_id) {
-                    for locator in &buffer_state.excerpts {
-                        inverted_diff_touch_info.insert(
-                            locator.clone(),
-                            (
-                                old_diff_snapshot.diff.clone(),
-                                old_main_buffer.clone(),
-                                new_main_buffer.clone(),
-                            ),
-                        );
-                        excerpts_to_edit.push((locator, buffer_state.buffer.clone(), false));
-                    }
-                }
-            }
-        }
-
         excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
-        excerpts_to_edit.dedup_by(|a, b| {
-            if a.0 == b.0 {
-                b.2 |= a.2;
-                true
-            } else {
-                false
-            }
-        });
 
         let mut edits = Vec::new();
         let mut new_excerpts = SumTree::default();
@@ -3124,59 +3022,6 @@ impl MultiBuffer {
             let buffer = buffer.read(cx);
             let buffer_id = buffer.remote_id();
 
-            let excerpt_old_start = cursor.start().1;
-            let excerpt_new_start = ExcerptDimension(new_excerpts.summary().text.len);
-
-            if !buffer_edited
-                && let Some((old_diff, old_main_buffer, new_main_buffer)) =
-                    inverted_diff_touch_info.get(locator)
-            {
-                // TODO(split-diff) this iterates over all excerpts for all edited buffers;
-                // it would be nice to be able to skip excerpts that weren't edited using
-                // new_main_buffer.has_edits_since_in_range.
-                let excerpt_buffer_start = old_excerpt
-                    .range
-                    .context
-                    .start
-                    .to_offset(&old_excerpt.buffer);
-                let excerpt_buffer_end = excerpt_buffer_start + old_excerpt.text_summary.len;
-
-                let mut hunks = old_diff
-                    .hunks_intersecting_base_text_range(
-                        excerpt_buffer_start..excerpt_buffer_end,
-                        old_main_buffer,
-                    )
-                    .chain(old_diff.hunks_intersecting_base_text_range(
-                        excerpt_buffer_start..excerpt_buffer_end,
-                        new_main_buffer,
-                    ))
-                    .filter(|hunk| {
-                        hunk.buffer_range.start.is_valid(&old_main_buffer)
-                            != hunk.buffer_range.start.is_valid(&new_main_buffer)
-                    })
-                    .collect::<Vec<_>>();
-                hunks.sort_by(|l, r| {
-                    l.diff_base_byte_range
-                        .start
-                        .cmp(&r.diff_base_byte_range.start)
-                });
-
-                for hunk in hunks {
-                    let hunk_buffer_start = hunk.diff_base_byte_range.start;
-                    if hunk_buffer_start >= excerpt_buffer_start
-                        && hunk_buffer_start <= excerpt_buffer_end
-                    {
-                        let hunk_offset = hunk_buffer_start - excerpt_buffer_start;
-                        let old_hunk_pos = excerpt_old_start + hunk_offset;
-                        let new_hunk_pos = excerpt_new_start + hunk_offset;
-                        edits.push(Edit {
-                            old: old_hunk_pos..old_hunk_pos,
-                            new: new_hunk_pos..new_hunk_pos,
-                        });
-                    }
-                }
-            }
-
             let mut new_excerpt;
             if buffer_edited {
                 edits.extend(
@@ -3221,14 +3066,6 @@ impl MultiBuffer {
         drop(cursor);
         *excerpts = new_excerpts;
 
-        for (buffer_id, _, new_main_buffer) in main_buffer_changed_diffs {
-            if let Some(stored) = buffer_diff.get(&buffer_id) {
-                let mut updated = stored.clone();
-                updated.main_buffer = Some(new_main_buffer);
-                buffer_diff.insert(buffer_id, updated);
-            }
-        }
-
         Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited)
     }
 
@@ -3320,9 +3157,9 @@ impl MultiBuffer {
                             ..
                         }) => excerpts.item().is_some_and(|excerpt| {
                             if let Some(diff) = snapshot.diffs.get(&excerpt.buffer_id)
-                                && let Some(main_buffer) = &diff.main_buffer
+                                && diff.is_inverted
                             {
-                                return hunk.hunk_start_anchor.is_valid(main_buffer);
+                                return true;
                             }
                             hunk.hunk_start_anchor.is_valid(&excerpt.buffer)
                         }),
@@ -3432,10 +3269,10 @@ impl MultiBuffer {
                     excerpt_buffer_start + edit.new.end.saturating_sub(excerpt_start);
                 let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end);
 
-                if let Some(main_buffer) = &diff.main_buffer {
+                if diff.is_inverted {
                     for hunk in diff.hunks_intersecting_base_text_range(
                         edit_buffer_start..edit_buffer_end,
-                        main_buffer,
+                        diff.original_buffer_snapshot(),
                     ) {
                         did_expand_hunks = true;
                         let hunk_buffer_range = hunk.diff_base_byte_range.clone();
@@ -3974,13 +3811,13 @@ impl MultiBufferSnapshot {
         self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| {
             let diff = self.diffs.get(&buffer.remote_id())?;
             let iter: Box<dyn Iterator<Item = (DiffHunk, &BufferSnapshot, bool)>> =
-                if let Some(main_buffer) = &diff.main_buffer {
+                if diff.is_inverted {
                     let buffer_start = buffer.point_to_offset(buffer_range.start);
                     let buffer_end = buffer.point_to_offset(buffer_range.end);
                     Box::new(
                         diff.hunks_intersecting_base_text_range(
                             buffer_start..buffer_end,
-                            main_buffer,
+                            diff.original_buffer_snapshot(),
                         )
                         .map(move |hunk| (hunk, buffer, true)),
                     )
@@ -4433,10 +4270,10 @@ impl MultiBufferSnapshot {
             .to_offset(&excerpt.buffer);
 
         if let Some(diff) = self.diffs.get(&excerpt.buffer_id) {
-            if let Some(main_buffer) = diff.main_buffer.as_ref() {
+            if diff.is_inverted {
                 for hunk in diff.hunks_intersecting_base_text_range_rev(
                     excerpt_start..excerpt_end,
-                    &main_buffer,
+                    diff.original_buffer_snapshot(),
                 ) {
                     if hunk.diff_base_byte_range.end >= current_position {
                         continue;
@@ -4471,11 +4308,11 @@ impl MultiBufferSnapshot {
             let Some(diff) = self.diffs.get(&excerpt.buffer_id) else {
                 continue;
             };
-            if let Some(main_buffer) = diff.main_buffer.as_ref() {
+            if diff.is_inverted {
                 let Some(hunk) = diff
                     .hunks_intersecting_base_text_range_rev(
                         excerpt.range.context.to_offset(&excerpt.buffer),
-                        main_buffer,
+                        diff.original_buffer_snapshot(),
                     )
                     .next()
                 else {
@@ -6811,15 +6648,6 @@ impl MultiBufferSnapshot {
         self.diffs.get(&buffer_id).map(|diff| &diff.diff)
     }
 
-    /// For inverted diffs (used in side-by-side diff view), returns the main buffer
-    /// snapshot that the diff's anchors refer to. Returns `None` if the diff is not
-    /// inverted or if there's no diff for the given buffer ID.
-    pub fn inverted_diff_main_buffer(&self, buffer_id: BufferId) -> Option<&text::BufferSnapshot> {
-        self.diffs
-            .get(&buffer_id)
-            .and_then(|diff| diff.main_buffer.as_ref())
-    }
-
     /// Visually annotates a position or range with the `Debug` representation of a value. The
     /// callsite of this function is used as a key - previous annotations will be removed.
     #[cfg(debug_assertions)]

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -487,7 +487,7 @@ async fn test_inverted_diff_hunks_in_range(cx: &mut TestAppContext) {
     });
 
     multibuffer.update(cx, |multibuffer, cx| {
-        multibuffer.add_inverted_diff(diff, buffer.clone(), cx);
+        multibuffer.add_inverted_diff(diff, cx);
     });
 
     assert_new_snapshot(
@@ -2295,7 +2295,7 @@ async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
 struct ReferenceMultibuffer {
     excerpts: Vec<ReferenceExcerpt>,
     diffs: HashMap<BufferId, Entity<BufferDiff>>,
-    inverted_diffs: HashMap<BufferId, (Entity<BufferDiff>, WeakEntity<Buffer>)>,
+    inverted_diffs: HashMap<BufferId, Entity<BufferDiff>>,
 }
 
 #[derive(Debug)]
@@ -2442,16 +2442,14 @@ impl ReferenceMultibuffer {
             let buffer_id = buffer.remote_id();
             let buffer_range = excerpt.range.to_offset(buffer);
 
-            if let Some((diff, main_buffer)) = self.inverted_diffs.get(&buffer_id) {
+            if let Some(diff) = self.inverted_diffs.get(&buffer_id) {
                 let diff_snapshot = diff.read(cx).snapshot(cx);
-                let main_buffer_snapshot = main_buffer
-                    .read_with(cx, |main_buffer, _| main_buffer.snapshot())
-                    .unwrap();
 
                 let mut offset = buffer_range.start;
-                for hunk in diff_snapshot
-                    .hunks_intersecting_base_text_range(buffer_range.clone(), &main_buffer_snapshot)
-                {
+                for hunk in diff_snapshot.hunks_intersecting_base_text_range(
+                    buffer_range.clone(),
+                    diff_snapshot.original_buffer_snapshot(),
+                ) {
                     let mut hunk_base_range = hunk.diff_base_byte_range.clone();
 
                     hunk_base_range.end = hunk_base_range.end.min(buffer_range.end);
@@ -2461,10 +2459,6 @@ impl ReferenceMultibuffer {
                         continue;
                     }
 
-                    if !hunk.buffer_range.start.is_valid(&main_buffer_snapshot) {
-                        continue;
-                    }
-
                     // Add the text before the hunk
                     if hunk_base_range.start >= offset {
                         let len = text.len();
@@ -2753,15 +2747,9 @@ impl ReferenceMultibuffer {
         self.diffs.insert(buffer_id, diff);
     }
 
-    fn add_inverted_diff(
-        &mut self,
-        diff: Entity<BufferDiff>,
-        main_buffer: Entity<Buffer>,
-        cx: &App,
-    ) {
+    fn add_inverted_diff(&mut self, diff: Entity<BufferDiff>, cx: &App) {
         let base_text_buffer_id = diff.read(cx).base_text(cx).remote_id();
-        self.inverted_diffs
-            .insert(base_text_buffer_id, (diff, main_buffer.downgrade()));
+        self.inverted_diffs.insert(base_text_buffer_id, diff);
     }
 }
 
@@ -3133,9 +3121,9 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
                     excerpt_buffer.read_with(cx, |buffer, _| buffer.remote_id());
                 multibuffer.update(cx, |multibuffer, cx| {
                     if multibuffer.diff_for(excerpt_buffer_id).is_none() {
-                        if let Some(main_buffer) = inverted_main_buffer {
-                            reference.add_inverted_diff(diff.clone(), main_buffer.clone(), cx);
-                            multibuffer.add_inverted_diff(diff, main_buffer, cx);
+                        if inverted_main_buffer.is_some() {
+                            reference.add_inverted_diff(diff.clone(), cx);
+                            multibuffer.add_inverted_diff(diff, cx);
                         } else {
                             reference.add_diff(diff.clone(), cx);
                             multibuffer.add_diff(diff, cx);
@@ -3877,62 +3865,6 @@ fn format_diff(
 //         .join("\n")
 // }
 
-#[gpui::test]
-async fn test_inverted_diff_hunk_invalidation_on_main_buffer_edit(cx: &mut TestAppContext) {
-    let text = "one\ntwo\nthree\n";
-    let base_text = "one\nTWO\nthree\n";
-
-    let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let diff = cx
-        .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
-    cx.run_until_parked();
-
-    let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer());
-
-    let multibuffer = cx.new(|cx| {
-        let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
-        multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx);
-        multibuffer
-    });
-
-    let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
-        (multibuffer.snapshot(cx), multibuffer.subscribe())
-    });
-
-    assert_new_snapshot(
-        &multibuffer,
-        &mut snapshot,
-        &mut subscription,
-        cx,
-        indoc!(
-            "
-              one
-            - TWO
-              three
-            "
-        ),
-    );
-
-    buffer.update(cx, |buffer, cx| {
-        buffer.edit([(3..5, "")], None, cx);
-    });
-    cx.run_until_parked();
-
-    assert_new_snapshot(
-        &multibuffer,
-        &mut snapshot,
-        &mut subscription,
-        cx,
-        indoc!(
-            "
-            one
-            TWO
-            three
-            "
-        ),
-    );
-}
-
 #[gpui::test]
 async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
     let text = indoc!(
@@ -3965,7 +3897,7 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
     let multibuffer = cx.new(|cx| {
         let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
         multibuffer.set_all_diff_hunks_expanded(cx);
-        multibuffer.add_inverted_diff(diff.clone(), buffer.clone(), cx);
+        multibuffer.add_inverted_diff(diff.clone(), cx);
         multibuffer
     });
 
@@ -4102,6 +4034,65 @@ async fn test_singleton_with_inverted_diff(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_inverted_diff_base_text_change(cx: &mut TestAppContext) {
+    let base_text = "aaa\nbbb\nccc\n";
+    let text = "ddd\n";
+    let buffer = cx.new(|cx| Buffer::local(text, cx));
+    let diff = cx
+        .new(|cx| BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx));
+    cx.run_until_parked();
+
+    let base_text_buffer = diff.read_with(cx, |diff, _| diff.base_text_buffer());
+
+    let multibuffer = cx.new(|cx| {
+        let mut multibuffer = MultiBuffer::singleton(base_text_buffer.clone(), cx);
+        multibuffer.set_all_diff_hunks_expanded(cx);
+        multibuffer.add_inverted_diff(diff.clone(), cx);
+        multibuffer
+    });
+
+    let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
+        (multibuffer.snapshot(cx), multibuffer.subscribe())
+    });
+
+    assert_eq!(snapshot.text(), base_text);
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+            - aaa
+            - bbb
+            - ccc
+            "
+        ),
+    );
+
+    let update = diff
+        .update(cx, |diff, cx| {
+            diff.update_diff(
+                buffer.read(cx).text_snapshot(),
+                Some("ddd\n".into()),
+                Some(true),
+                None,
+                cx,
+            )
+        })
+        .await;
+    diff.update(cx, |diff, cx| {
+        diff.set_snapshot(update, &buffer.read(cx).text_snapshot(), cx)
+    })
+    .detach();
+
+    let _hunks: Vec<_> = multibuffer
+        .read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx))
+        .diff_hunks()
+        .collect();
+}
+
 #[track_caller]
 fn assert_excerpts_match(
     multibuffer: &Entity<MultiBuffer>,