Fix unnecessarily large edits emitted from multi buffer on diff recalculation (#23753)

Max Brunsfeld and Conrad created

This fixes an issue introduced in #22994 where soft wrap would
recalculate for the entire buffer when editing.

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>

Change summary

crates/editor/src/editor_tests.rs             |   9 
crates/git/Cargo.toml                         |   1 
crates/git/src/diff.rs                        | 191 +++++++++++++++
crates/multi_buffer/src/multi_buffer.rs       | 261 +++++++++-----------
crates/multi_buffer/src/multi_buffer_tests.rs | 109 ++++----
crates/project/src/buffer_store.rs            |  80 ++---
crates/text/Cargo.toml                        |   2 
7 files changed, 402 insertions(+), 251 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -14397,12 +14397,8 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContex
             let buffer = multibuffer.as_singleton().unwrap();
             let change_set = cx.new(|cx| {
                 let mut change_set = BufferChangeSet::new(&buffer, cx);
-                change_set.recalculate_diff_sync(
-                    base_text.into(),
-                    buffer.read(cx).text_snapshot(),
-                    true,
-                    cx,
-                );
+                let _ =
+                    change_set.set_base_text(base_text.into(), buffer.read(cx).text_snapshot(), cx);
                 change_set
             });
 
@@ -14412,6 +14408,7 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContex
             buffer.read(cx).remote_id()
         })
     });
+    cx.run_until_parked();
 
     cx.assert_state_with_diff(
         indoc! { "

crates/git/Cargo.toml 🔗

@@ -35,6 +35,7 @@ util.workspace = true
 unindent.workspace = true
 serde_json.workspace = true
 pretty_assertions.workspace = true
+text = {workspace = true, features = ["test-support"]}
 
 [features]
 test-support = []

crates/git/src/diff.rs 🔗

@@ -1,5 +1,5 @@
 use rope::Rope;
-use std::{iter, ops::Range};
+use std::{cmp, iter, ops::Range};
 use sum_tree::SumTree;
 use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
 
@@ -25,7 +25,7 @@ pub struct DiffHunk {
 }
 
 /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 struct InternalDiffHunk {
     buffer_range: Range<Anchor>,
     diff_base_byte_range: Range<usize>,
@@ -187,6 +187,69 @@ impl BufferDiff {
         })
     }
 
+    pub fn compare(&self, old: &Self, new_snapshot: &BufferSnapshot) -> Option<Range<Anchor>> {
+        let mut new_cursor = self.tree.cursor::<()>(new_snapshot);
+        let mut old_cursor = old.tree.cursor::<()>(new_snapshot);
+        old_cursor.next(new_snapshot);
+        new_cursor.next(new_snapshot);
+        let mut start = None;
+        let mut end = None;
+
+        loop {
+            match (new_cursor.item(), old_cursor.item()) {
+                (Some(new_hunk), Some(old_hunk)) => {
+                    match new_hunk
+                        .buffer_range
+                        .start
+                        .cmp(&old_hunk.buffer_range.start, new_snapshot)
+                    {
+                        cmp::Ordering::Less => {
+                            start.get_or_insert(new_hunk.buffer_range.start);
+                            end.replace(new_hunk.buffer_range.end);
+                            new_cursor.next(new_snapshot);
+                        }
+                        cmp::Ordering::Equal => {
+                            if new_hunk != old_hunk {
+                                start.get_or_insert(new_hunk.buffer_range.start);
+                                if old_hunk
+                                    .buffer_range
+                                    .end
+                                    .cmp(&new_hunk.buffer_range.end, new_snapshot)
+                                    .is_ge()
+                                {
+                                    end.replace(old_hunk.buffer_range.end);
+                                } else {
+                                    end.replace(new_hunk.buffer_range.end);
+                                }
+                            }
+
+                            new_cursor.next(new_snapshot);
+                            old_cursor.next(new_snapshot);
+                        }
+                        cmp::Ordering::Greater => {
+                            start.get_or_insert(old_hunk.buffer_range.start);
+                            end.replace(old_hunk.buffer_range.end);
+                            old_cursor.next(new_snapshot);
+                        }
+                    }
+                }
+                (Some(new_hunk), None) => {
+                    start.get_or_insert(new_hunk.buffer_range.start);
+                    end.replace(new_hunk.buffer_range.end);
+                    new_cursor.next(new_snapshot);
+                }
+                (None, Some(old_hunk)) => {
+                    start.get_or_insert(old_hunk.buffer_range.start);
+                    end.replace(old_hunk.buffer_range.end);
+                    old_cursor.next(new_snapshot);
+                }
+                (None, None) => break,
+            }
+        }
+
+        start.zip(end).map(|(start, end)| start..end)
+    }
+
     #[cfg(test)]
     fn clear(&mut self, buffer: &text::BufferSnapshot) {
         self.tree = SumTree::new(buffer);
@@ -427,4 +490,128 @@ mod tests {
             ],
         );
     }
+
+    #[test]
+    fn test_buffer_diff_compare() {
+        let base_text = "
+            zero
+            one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+        "
+        .unindent();
+
+        let buffer_text_1 = "
+            one
+            three
+            four
+            five
+            SIX
+            seven
+            eight
+            NINE
+        "
+        .unindent();
+
+        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
+
+        let empty_diff = BufferDiff::new(&buffer);
+        let diff_1 = BufferDiff::build(&base_text, &buffer);
+        let range = diff_1.compare(&empty_diff, &buffer).unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
+
+        // Edit does not affect the diff.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                three
+                four
+                five
+                «SIX.5»
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_2 = BufferDiff::build(&base_text, &buffer);
+        assert_eq!(None, diff_2.compare(&diff_1, &buffer));
+
+        // Edit turns a deletion hunk into a modification.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                «THREE»
+                four
+                five
+                SIX.5
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_3 = BufferDiff::build(&base_text, &buffer);
+        let range = diff_3.compare(&diff_2, &buffer).unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
+
+        // Edit turns a modification hunk into a deletion.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                THREE
+                four
+                five«»
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_4 = BufferDiff::build(&base_text, &buffer);
+        let range = diff_4.compare(&diff_3, &buffer).unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
+
+        // Edit introduces a new insertion hunk.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                THREE
+                four«
+                FOUR.5
+                »five
+                seven
+                eight
+                NINE
+            "
+            .unindent(),
+        );
+        let diff_5 = BufferDiff::build(&base_text, &buffer);
+        let range = diff_5.compare(&diff_4, &buffer).unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
+
+        // Edit removes a hunk.
+        buffer.edit_via_marked_text(
+            &"
+                one
+                THREE
+                four
+                FOUR.5
+                five
+                seven
+                eight
+                «nine»
+            "
+            .unindent(),
+        );
+        let diff_6 = BufferDiff::build(&base_text, &buffer);
+        let range = diff_6.compare(&diff_5, &buffer).unwrap();
+        assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
+    }
 }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -21,7 +21,7 @@ use language::{
     TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions,
     Unclipped,
 };
-use project::buffer_store::BufferChangeSet;
+use project::buffer_store::{BufferChangeSet, BufferChangeSetEvent};
 use rope::DimensionPair;
 use smallvec::SmallVec;
 use smol::future::yield_now;
@@ -434,7 +434,6 @@ struct BufferEdit {
 #[derive(Clone, Copy, Debug, PartialEq)]
 enum DiffChangeKind {
     BufferEdited,
-    ExcerptsChanged,
     DiffUpdated { base_changed: bool },
     ExpandOrCollapseHunks { expand: bool },
 }
@@ -546,8 +545,14 @@ impl MultiBuffer {
             diff_bases.insert(
                 *buffer_id,
                 ChangeSetState {
-                    _subscription: new_cx
-                        .observe(&change_set_state.change_set, Self::buffer_diff_changed),
+                    _subscription: new_cx.subscribe(
+                        &change_set_state.change_set,
+                        |this, change_set, event, cx| match event {
+                            BufferChangeSetEvent::DiffChanged { changed_range } => {
+                                this.buffer_diff_changed(change_set, changed_range.clone(), cx)
+                            }
+                        },
+                    ),
                     change_set: change_set_state.change_set.clone(),
                 },
             );
@@ -1603,7 +1608,7 @@ impl MultiBuffer {
                 old: edit_start..edit_start,
                 new: edit_start..edit_end,
             }],
-            DiffChangeKind::ExcerptsChanged,
+            DiffChangeKind::BufferEdited,
         );
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
@@ -1636,7 +1641,7 @@ impl MultiBuffer {
                 old: start..prev_len,
                 new: start..start,
             }],
-            DiffChangeKind::ExcerptsChanged,
+            DiffChangeKind::BufferEdited,
         );
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
@@ -1909,7 +1914,7 @@ impl MultiBuffer {
             snapshot.trailing_excerpt_update_count += 1;
         }
 
-        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged);
+        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
             edited_buffer: None,
@@ -1998,22 +2003,26 @@ impl MultiBuffer {
         });
     }
 
-    fn buffer_diff_changed(&mut self, change_set: Entity<BufferChangeSet>, cx: &mut Context<Self>) {
+    fn buffer_diff_changed(
+        &mut self,
+        change_set: Entity<BufferChangeSet>,
+        range: Range<text::Anchor>,
+        cx: &mut Context<Self>,
+    ) {
         let change_set = change_set.read(cx);
         let buffer_id = change_set.buffer_id;
         let diff = change_set.diff_to_buffer.clone();
         let base_text = change_set.base_text.clone();
         self.sync(cx);
         let mut snapshot = self.snapshot.borrow_mut();
-        let base_text_version_changed =
-            snapshot
-                .diffs
-                .get(&buffer_id)
-                .map_or(true, |diff_snapshot| {
-                    change_set.base_text.as_ref().map_or(true, |base_text| {
-                        base_text.remote_id() != diff_snapshot.base_text.remote_id()
-                    })
-                });
+        let base_text_changed = snapshot
+            .diffs
+            .get(&buffer_id)
+            .map_or(true, |diff_snapshot| {
+                change_set.base_text.as_ref().map_or(true, |base_text| {
+                    base_text.remote_id() != diff_snapshot.base_text.remote_id()
+                })
+            });
 
         if let Some(base_text) = base_text {
             snapshot.diffs.insert(
@@ -2026,26 +2035,44 @@ impl MultiBuffer {
         } else {
             snapshot.diffs.remove(&buffer_id);
         }
+        let buffers = self.buffers.borrow();
+        let Some(buffer_state) = buffers.get(&buffer_id) else {
+            return;
+        };
+
+        let diff_change_range = range.to_offset(buffer_state.buffer.read(cx));
 
         let mut excerpt_edits = Vec::new();
-        for locator in self
-            .buffers
-            .borrow()
-            .get(&buffer_id)
-            .map(|state| &state.excerpts)
-            .into_iter()
-            .flatten()
-        {
+        for locator in &buffer_state.excerpts {
             let mut cursor = snapshot
                 .excerpts
                 .cursor::<(Option<&Locator>, ExcerptOffset)>(&());
             cursor.seek_forward(&Some(locator), Bias::Left, &());
             if let Some(excerpt) = cursor.item() {
                 if excerpt.locator == *locator {
-                    let excerpt_range = cursor.start().1..cursor.end(&()).1;
+                    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 = ExcerptOffset::new(excerpt.text_summary.len);
+                    let diff_change_start_in_excerpt = ExcerptOffset::new(
+                        diff_change_range
+                            .start
+                            .saturating_sub(excerpt_buffer_range.start),
+                    );
+                    let diff_change_end_in_excerpt = ExcerptOffset::new(
+                        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: excerpt_range.clone(),
-                        new: excerpt_range.clone(),
+                        old: edit_start..edit_end,
+                        new: edit_start..edit_end,
                     });
                 }
             }
@@ -2055,7 +2082,7 @@ impl MultiBuffer {
             snapshot,
             excerpt_edits,
             DiffChangeKind::DiffUpdated {
-                base_changed: base_text_version_changed,
+                base_changed: base_text_changed,
             },
         );
         cx.emit(Event::Edited {
@@ -2145,11 +2172,18 @@ impl MultiBuffer {
 
     pub fn add_change_set(&mut self, change_set: Entity<BufferChangeSet>, cx: &mut Context<Self>) {
         let buffer_id = change_set.read(cx).buffer_id;
-        self.buffer_diff_changed(change_set.clone(), cx);
+        self.buffer_diff_changed(change_set.clone(), text::Anchor::MIN..text::Anchor::MAX, cx);
         self.diff_bases.insert(
             buffer_id,
             ChangeSetState {
-                _subscription: cx.observe(&change_set, Self::buffer_diff_changed),
+                _subscription: cx.subscribe(
+                    &change_set,
+                    |this, change_set, event, cx| match event {
+                        BufferChangeSetEvent::DiffChanged { changed_range } => {
+                            this.buffer_diff_changed(change_set, changed_range.clone(), cx);
+                        }
+                    },
+                ),
                 change_set,
             },
         );
@@ -2329,7 +2363,7 @@ impl MultiBuffer {
         drop(cursor);
         snapshot.excerpts = new_excerpts;
 
-        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged);
+        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
             edited_buffer: None,
@@ -2430,7 +2464,7 @@ impl MultiBuffer {
         drop(cursor);
         snapshot.excerpts = new_excerpts;
 
-        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged);
+        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
             edited_buffer: None,
@@ -2595,63 +2629,52 @@ impl MultiBuffer {
             let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot;
             let edit_new_start = (edit_old_start as isize + output_delta) as usize;
 
-            if change_kind == DiffChangeKind::BufferEdited {
-                self.interpolate_diff_transforms_for_edit(
-                    &edit,
-                    &excerpts,
-                    &mut old_diff_transforms,
-                    &mut new_diff_transforms,
-                    &mut end_of_current_insert,
-                );
-            } else {
-                self.recompute_diff_transforms_for_edit(
-                    &edit,
-                    &mut excerpts,
-                    &mut old_diff_transforms,
-                    &mut new_diff_transforms,
-                    &mut end_of_current_insert,
-                    &mut old_expanded_hunks,
-                    &snapshot,
-                    change_kind,
-                );
-            }
-
-            self.push_buffer_content_transform(
-                &snapshot,
+            let changed_diff_hunks = self.recompute_diff_transforms_for_edit(
+                &edit,
+                &mut excerpts,
+                &mut old_diff_transforms,
                 &mut new_diff_transforms,
-                edit.new.end,
-                end_of_current_insert,
+                &mut end_of_current_insert,
+                &mut old_expanded_hunks,
+                &snapshot,
+                change_kind,
             );
 
             // Compute the end of the edit in output coordinates.
-            let edit_end_overshoot = (edit.old.end - old_diff_transforms.start().0).value;
-            let edit_old_end = old_diff_transforms.start().1 + edit_end_overshoot;
-            let edit_new_end = new_diff_transforms.summary().output.len;
+            let edit_old_end_overshoot = edit.old.end - old_diff_transforms.start().0;
+            let edit_new_end_overshoot = edit.new.end - new_diff_transforms.summary().excerpt_len();
+            let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot.value;
+            let edit_new_end =
+                new_diff_transforms.summary().output.len + edit_new_end_overshoot.value;
             let output_edit = Edit {
                 old: edit_old_start..edit_old_end,
                 new: edit_new_start..edit_new_end,
             };
 
-            output_delta += (output_edit.new.end - output_edit.new.start) as isize
-                - (output_edit.old.end - output_edit.old.start) as isize;
-            output_edits.push(output_edit);
+            output_delta += (output_edit.new.end - output_edit.new.start) as isize;
+            output_delta -= (output_edit.old.end - output_edit.old.start) as isize;
+            if changed_diff_hunks || matches!(change_kind, DiffChangeKind::BufferEdited) {
+                output_edits.push(output_edit);
+            }
 
             // If this is the last edit that intersects the current diff transform,
-            // then preserve a suffix of the this diff transform.
+            // then recreate the content up to the end of this transform, to prepare
+            // for reusing additional slices of the old transforms.
             if excerpt_edits.peek().map_or(true, |next_edit| {
                 next_edit.old.start >= old_diff_transforms.end(&()).0
             }) {
+                let mut excerpt_offset = edit.new.end;
                 if old_diff_transforms.start().0 < edit.old.end {
-                    let suffix = old_diff_transforms.end(&()).0 - edit.old.end;
-                    let transform_end = new_diff_transforms.summary().excerpt_len() + suffix;
-                    self.push_buffer_content_transform(
-                        &snapshot,
-                        &mut new_diff_transforms,
-                        transform_end,
-                        end_of_current_insert,
-                    );
+                    excerpt_offset += old_diff_transforms.end(&()).0 - edit.old.end;
                     old_diff_transforms.next(&());
                 }
+                old_expanded_hunks.clear();
+                self.push_buffer_content_transform(
+                    &snapshot,
+                    &mut new_diff_transforms,
+                    excerpt_offset,
+                    end_of_current_insert,
+                );
                 at_transform_boundary = true;
             }
         }
@@ -2691,7 +2714,7 @@ impl MultiBuffer {
         old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>,
         snapshot: &MultiBufferSnapshot,
         change_kind: DiffChangeKind,
-    ) {
+    ) -> bool {
         log::trace!(
             "recomputing diff transform for edit {:?} => {:?}",
             edit.old.start.value..edit.old.end.value,
@@ -2699,11 +2722,7 @@ impl MultiBuffer {
         );
 
         // Record which hunks were previously expanded.
-        old_expanded_hunks.clear();
         while let Some(item) = old_diff_transforms.item() {
-            if old_diff_transforms.end(&()).0 > edit.old.end {
-                break;
-            }
             if let Some(hunk_anchor) = item.hunk_anchor() {
                 log::trace!(
                     "previously expanded hunk at {}",
@@ -2711,10 +2730,22 @@ impl MultiBuffer {
                 );
                 old_expanded_hunks.insert(hunk_anchor);
             }
+            if old_diff_transforms.end(&()).0 > edit.old.end {
+                break;
+            }
             old_diff_transforms.next(&());
         }
 
+        // Avoid querying diff hunks if there's no possibility of hunks being expanded.
+        if old_expanded_hunks.is_empty()
+            && change_kind == DiffChangeKind::BufferEdited
+            && !self.all_diff_hunks_expanded
+        {
+            return false;
+        }
+
         // Visit each excerpt that intersects the edit.
+        let mut did_expand_hunks = false;
         while let Some(excerpt) = excerpts.item() {
             if excerpt.text_summary.len == 0 {
                 if excerpts.end(&()) <= edit.new.end {
@@ -2754,8 +2785,10 @@ impl MultiBuffer {
                         + ExcerptOffset::new(
                             hunk_buffer_range.start.saturating_sub(excerpt_buffer_start),
                         );
-                    let hunk_excerpt_end = excerpt_start
-                        + ExcerptOffset::new(hunk_buffer_range.end - excerpt_buffer_start);
+                    let hunk_excerpt_end = excerpt_end.min(
+                        excerpt_start
+                            + ExcerptOffset::new(hunk_buffer_range.end - excerpt_buffer_start),
+                    );
 
                     self.push_buffer_content_transform(
                         snapshot,
@@ -2787,7 +2820,11 @@ impl MultiBuffer {
                     };
 
                     if should_expand_hunk {
-                        log::trace!("expanding hunk at {}", hunk_excerpt_start.value);
+                        did_expand_hunks = true;
+                        log::trace!(
+                            "expanding hunk {:?}",
+                            hunk_excerpt_start.value..hunk_excerpt_end.value,
+                        );
 
                         if !hunk.diff_base_byte_range.is_empty()
                             && hunk_buffer_range.start >= edit_buffer_start
@@ -2833,68 +2870,8 @@ impl MultiBuffer {
                 break;
             }
         }
-    }
-
-    fn interpolate_diff_transforms_for_edit(
-        &self,
-        edit: &Edit<TypedOffset<Excerpt>>,
-        excerpts: &Cursor<Excerpt, TypedOffset<Excerpt>>,
-        old_diff_transforms: &mut Cursor<DiffTransform, (TypedOffset<Excerpt>, usize)>,
-        new_diff_transforms: &mut SumTree<DiffTransform>,
-        end_of_current_insert: &mut Option<(TypedOffset<Excerpt>, ExcerptId, text::Anchor)>,
-    ) {
-        log::trace!(
-            "interpolating diff transform for edit {:?} => {:?}",
-            edit.old.start.value..edit.old.end.value,
-            edit.new.start.value..edit.new.end.value
-        );
-
-        // Preserve deleted hunks immediately preceding edits.
-        if let Some(transform) = old_diff_transforms.item() {
-            if old_diff_transforms.start().0 == edit.old.start {
-                if let DiffTransform::DeletedHunk { hunk_anchor, .. } = transform {
-                    if excerpts
-                        .item()
-                        .map_or(false, |excerpt| hunk_anchor.1.is_valid(&excerpt.buffer))
-                    {
-                        self.push_diff_transform(new_diff_transforms, transform.clone());
-                        old_diff_transforms.next(&());
-                    }
-                }
-            }
-        }
-
-        let edit_start_transform = old_diff_transforms.item();
-
-        // When an edit starts within an inserted hunks, extend the hunk
-        // to include the lines of the edit.
-        if let Some((
-            DiffTransform::BufferContent {
-                inserted_hunk_anchor: Some(inserted_hunk_anchor),
-                ..
-            },
-            excerpt,
-        )) = edit_start_transform.zip(excerpts.item())
-        {
-            let buffer = &excerpt.buffer;
-            if inserted_hunk_anchor.1.is_valid(buffer) {
-                let excerpt_start = *excerpts.start();
-                let excerpt_end = excerpt_start + ExcerptOffset::new(excerpt.text_summary.len);
-                let excerpt_buffer_start = excerpt.range.context.start.to_offset(buffer);
-                let edit_buffer_end =
-                    excerpt_buffer_start + edit.new.end.value.saturating_sub(excerpt_start.value);
-                let edit_buffer_end_point = buffer.offset_to_point(edit_buffer_end);
-                let edited_buffer_line_end =
-                    buffer.point_to_offset(edit_buffer_end_point + Point::new(1, 0));
-                let edited_line_end = excerpt_start
-                    + ExcerptOffset::new(edited_buffer_line_end - excerpt_buffer_start);
-                let hunk_end = edited_line_end.min(excerpt_end);
-                *end_of_current_insert =
-                    Some((hunk_end, inserted_hunk_anchor.0, inserted_hunk_anchor.1));
-            }
-        }
 
-        old_diff_transforms.seek_forward(&edit.old.end, Bias::Right, &());
+        did_expand_hunks || !old_expanded_hunks.is_empty()
     }
 
     fn append_diff_transforms(

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -354,16 +354,17 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut App) {
 }
 
 #[gpui::test]
-fn test_diff_boundary_anchors(cx: &mut App) {
+fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
     let base_text = "one\ntwo\nthree\n";
     let text = "one\nthree\n";
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let snapshot = buffer.read(cx).snapshot();
+    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
     let change_set = cx.new(|cx| {
         let mut change_set = BufferChangeSet::new(&buffer, cx);
-        change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx);
+        let _ = change_set.set_base_text(base_text.into(), snapshot.text, cx);
         change_set
     });
+    cx.run_until_parked();
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
     multibuffer.update(cx, |multibuffer, cx| {
         multibuffer.add_change_set(change_set, cx)
@@ -375,9 +376,9 @@ fn test_diff_boundary_anchors(cx: &mut App) {
         multibuffer.set_all_diff_hunks_expanded(cx);
         (before, after)
     });
-    cx.background_executor().run_until_parked();
+    cx.run_until_parked();
 
-    let snapshot = multibuffer.read(cx).snapshot(cx);
+    let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
     let actual_text = snapshot.text();
     let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
     let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default());
@@ -410,9 +411,10 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
     let change_set = cx.new(|cx| {
         let mut change_set = BufferChangeSet::new(&buffer, cx);
         let snapshot = buffer.read(cx).snapshot();
-        change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx);
+        let _ = change_set.set_base_text(base_text.into(), snapshot.text, cx);
         change_set
     });
+    cx.run_until_parked();
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
     let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
         (multibuffer.snapshot(cx), multibuffer.subscribe())
@@ -507,10 +509,11 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
     let buffer = cx.new(|cx| Buffer::local(text, cx));
     let change_set = cx.new(|cx| {
         let mut change_set = BufferChangeSet::new(&buffer, cx);
-        let snapshot = buffer.read(cx).snapshot();
-        change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx);
+        let snapshot = buffer.read(cx).text_snapshot();
+        let _ = change_set.set_base_text(base_text.into(), snapshot, cx);
         change_set
     });
+    cx.run_until_parked();
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 
     let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
@@ -586,14 +589,6 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
     );
 
     multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx));
-    change_set.update(cx, |change_set, cx| {
-        change_set.recalculate_diff_sync(
-            base_text.into(),
-            buffer.read(cx).text_snapshot(),
-            true,
-            cx,
-        );
-    });
     assert_new_snapshot(
         &multibuffer,
         &mut snapshot,
@@ -1861,7 +1856,7 @@ impl ReferenceMultibuffer {
                 .buffer_range
                 .end
                 .cmp(&excerpt.range.start, &buffer)
-                .is_le();
+                .is_lt();
             let hunk_follows_excerpt = hunk
                 .buffer_range
                 .start
@@ -2064,7 +2059,7 @@ impl ReferenceMultibuffer {
 }
 
 #[gpui::test(iterations = 100)]
-fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
+async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
     let operations = env::var("OPERATIONS")
         .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
         .unwrap_or(10);
@@ -2085,7 +2080,7 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                     buf.randomly_edit(&mut rng, edit_count, cx);
                     needs_diff_calculation = true;
                 });
-                reference.diffs_updated(cx);
+                cx.update(|cx| reference.diffs_updated(cx));
             }
             15..=19 if !reference.excerpts.is_empty() => {
                 multibuffer.update(cx, |multibuffer, cx| {
@@ -2119,10 +2114,11 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                         break;
                     };
                     let id = excerpt.id;
-                    reference.remove_excerpt(id, cx);
+                    cx.update(|cx| reference.remove_excerpt(id, cx));
                     ids_to_remove.push(id);
                 }
-                let snapshot = multibuffer.read(cx).read(cx);
+                let snapshot =
+                    multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
                 ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
                 drop(snapshot);
                 multibuffer.update(cx, |multibuffer, cx| {
@@ -2130,7 +2126,8 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                 });
             }
             30..=39 if !reference.excerpts.is_empty() => {
-                let multibuffer = multibuffer.read(cx).read(cx);
+                let multibuffer =
+                    multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
                 let offset =
                     multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left);
                 let bias = if rng.gen() { Bias::Left } else { Bias::Right };
@@ -2139,7 +2136,8 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                 anchors.sort_by(|a, b| a.cmp(b, &multibuffer));
             }
             40..=44 if !anchors.is_empty() => {
-                let multibuffer = multibuffer.read(cx).read(cx);
+                let multibuffer =
+                    multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
                 let prev_len = anchors.len();
                 anchors = multibuffer
                     .refresh_anchors(&anchors)
@@ -2189,12 +2187,7 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                                     "recalculating diff for buffer {:?}",
                                     snapshot.remote_id(),
                                 );
-                                change_set.recalculate_diff_sync(
-                                    change_set.base_text.clone().unwrap().text(),
-                                    snapshot.text,
-                                    false,
-                                    cx,
-                                )
+                                change_set.recalculate_diff(snapshot.text, cx)
                             });
                     }
                     reference.diffs_updated(cx);
@@ -2208,15 +2201,17 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                         .collect::<String>();
 
                     let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
-                    let snapshot = buffer.read(cx).snapshot();
-                    let change_set = cx.new(|cx| {
-                        let mut change_set = BufferChangeSet::new(&buffer, cx);
-                        change_set.recalculate_diff_sync(base_text, snapshot.text, true, cx);
-                        change_set
-                    });
+                    let change_set = cx.new(|cx| BufferChangeSet::new(&buffer, cx));
+                    change_set
+                        .update(cx, |change_set, cx| {
+                            let snapshot = buffer.read(cx).snapshot();
+                            change_set.set_base_text(base_text, snapshot.text, cx)
+                        })
+                        .await
+                        .unwrap();
 
-                    reference.add_change_set(change_set.clone(), cx);
                     multibuffer.update(cx, |multibuffer, cx| {
+                        reference.add_change_set(change_set.clone(), cx);
                         multibuffer.add_change_set(change_set, cx)
                     });
                     buffers.push(buffer);
@@ -2225,12 +2220,6 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                     buffers.choose(&mut rng).unwrap()
                 };
 
-                let buffer = buffer_handle.read(cx);
-                let end_row = rng.gen_range(0..=buffer.max_point().row);
-                let start_row = rng.gen_range(0..=end_row);
-                let end_ix = buffer.point_to_offset(Point::new(end_row, 0));
-                let start_ix = buffer.point_to_offset(Point::new(start_row, 0));
-                let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
                 let prev_excerpt_ix = rng.gen_range(0..=reference.excerpts.len());
                 let prev_excerpt_id = reference
                     .excerpts
@@ -2238,15 +2227,25 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                     .map_or(ExcerptId::max(), |e| e.id);
                 let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len());
 
-                log::info!(
-                    "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
-                    excerpt_ix,
-                    reference.excerpts.len(),
-                    buffer_handle.read(cx).remote_id(),
-                    buffer.text(),
-                    start_ix..end_ix,
-                    &buffer.text()[start_ix..end_ix]
-                );
+                let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| {
+                    let end_row = rng.gen_range(0..=buffer.max_point().row);
+                    let start_row = rng.gen_range(0..=end_row);
+                    let end_ix = buffer.point_to_offset(Point::new(end_row, 0));
+                    let start_ix = buffer.point_to_offset(Point::new(start_row, 0));
+                    let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
+
+                    log::info!(
+                        "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
+                        excerpt_ix,
+                        reference.excerpts.len(),
+                        buffer.remote_id(),
+                        buffer.text(),
+                        start_ix..end_ix,
+                        &buffer.text()[start_ix..end_ix]
+                    );
+
+                    (start_ix..end_ix, anchor_range)
+                });
 
                 let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
                     multibuffer
@@ -2254,7 +2253,7 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
                             prev_excerpt_id,
                             buffer_handle.clone(),
                             [ExcerptRange {
-                                context: start_ix..end_ix,
+                                context: range,
                                 primary: None,
                             }],
                             cx,
@@ -2277,7 +2276,7 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
             })
         }
 
-        let snapshot = multibuffer.read(cx).snapshot(cx);
+        let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
         let actual_text = snapshot.text();
         let actual_boundary_rows = snapshot
             .excerpt_boundaries_in_range(0..)
@@ -2287,7 +2286,7 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
         let actual_diff = format_diff(&actual_text, &actual_row_infos, &actual_boundary_rows);
 
         let (expected_text, expected_row_infos, expected_boundary_rows) =
-            reference.expected_content(cx);
+            cx.update(|cx| reference.expected_content(cx));
         let expected_diff =
             format_diff(&expected_text, &expected_row_infos, &expected_boundary_rows);
 
@@ -2404,7 +2403,7 @@ fn test_random_multibuffer(cx: &mut App, mut rng: StdRng) {
         }
     }
 
-    let snapshot = multibuffer.read(cx).snapshot(cx);
+    let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
     for (old_snapshot, subscription) in old_versions {
         let edits = subscription.consume().into_inner();
 

crates/project/src/buffer_store.rs 🔗

@@ -69,6 +69,10 @@ pub struct BufferChangeSet {
     pub language_registry: Option<Arc<LanguageRegistry>>,
 }
 
+pub enum BufferChangeSetEvent {
+    DiffChanged { changed_range: Range<text::Anchor> },
+}
+
 enum BufferStoreState {
     Local(LocalBufferStore),
     Remote(RemoteBufferStore),
@@ -2201,6 +2205,8 @@ impl BufferStore {
     }
 }
 
+impl EventEmitter<BufferChangeSetEvent> for BufferChangeSet {}
+
 impl BufferChangeSet {
     pub fn new(buffer: &Entity<Buffer>, cx: &mut Context<Self>) -> Self {
         cx.subscribe(buffer, |this, buffer, event, cx| match event {
@@ -2318,69 +2324,53 @@ impl BufferChangeSet {
         let (tx, rx) = oneshot::channel();
         self.diff_updated_futures.push(tx);
         self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
-            let new_base_text = if base_text_changed {
-                let base_text_rope: Rope = base_text.as_str().into();
-                let snapshot = this.update(&mut cx, |this, cx| {
-                    language::Buffer::build_snapshot(
+            let (old_diff, new_base_text) = this.update(&mut cx, |this, cx| {
+                let new_base_text = if base_text_changed {
+                    let base_text_rope: Rope = base_text.as_str().into();
+                    let snapshot = language::Buffer::build_snapshot(
                         base_text_rope,
                         this.language.clone(),
                         this.language_registry.clone(),
                         cx,
-                    )
-                })?;
-                Some(cx.background_executor().spawn(snapshot).await)
-            } else {
-                None
-            };
-            let diff = cx
-                .background_executor()
-                .spawn({
-                    let buffer_snapshot = buffer_snapshot.clone();
-                    async move { BufferDiff::build(&base_text, &buffer_snapshot) }
-                })
-                .await;
+                    );
+                    cx.background_executor()
+                        .spawn(async move { Some(snapshot.await) })
+                } else {
+                    Task::ready(None)
+                };
+                (this.diff_to_buffer.clone(), new_base_text)
+            })?;
+
+            let diff = cx.background_executor().spawn(async move {
+                let new_diff = BufferDiff::build(&base_text, &buffer_snapshot);
+                let changed_range = if base_text_changed {
+                    Some(text::Anchor::MIN..text::Anchor::MAX)
+                } else {
+                    new_diff.compare(&old_diff, &buffer_snapshot)
+                };
+                (new_diff, changed_range)
+            });
+
+            let (new_base_text, (diff, changed_range)) = futures::join!(new_base_text, diff);
+
             this.update(&mut cx, |this, cx| {
                 if let Some(new_base_text) = new_base_text {
                     this.base_text = Some(new_base_text)
                 }
                 this.diff_to_buffer = diff;
+
                 this.recalculate_diff_task.take();
                 for tx in this.diff_updated_futures.drain(..) {
                     tx.send(()).ok();
                 }
-                cx.notify();
+                if let Some(changed_range) = changed_range {
+                    cx.emit(BufferChangeSetEvent::DiffChanged { changed_range });
+                }
             })?;
             Ok(())
         }));
         rx
     }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn recalculate_diff_sync(
-        &mut self,
-        mut base_text: String,
-        buffer_snapshot: text::BufferSnapshot,
-        base_text_changed: bool,
-        cx: &mut Context<Self>,
-    ) {
-        LineEnding::normalize(&mut base_text);
-        let diff = BufferDiff::build(&base_text, &buffer_snapshot);
-        if base_text_changed {
-            self.base_text = Some(
-                cx.background_executor()
-                    .clone()
-                    .block(Buffer::build_snapshot(
-                        base_text.into(),
-                        self.language.clone(),
-                        self.language_registry.clone(),
-                        cx,
-                    )),
-            );
-        }
-        self.diff_to_buffer = diff;
-        self.recalculate_diff_task.take();
-        cx.notify();
-    }
 }
 
 impl OpenBuffer {

crates/text/Cargo.toml 🔗

@@ -13,7 +13,7 @@ path = "src/text.rs"
 doctest = false
 
 [features]
-test-support = ["rand"]
+test-support = ["rand", "util/test-support"]
 
 [dependencies]
 anyhow.workspace = true