diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 94881c10de..66d19bd921 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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! { " diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 875d8aa19f..77b44ef230 100644 --- a/crates/git/Cargo.toml +++ b/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 = [] diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index d0fc05d82d..8dfafdbada 100644 --- a/crates/git/src/diff.rs +++ b/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, diff_base_byte_range: Range, @@ -187,6 +187,69 @@ impl BufferDiff { }) } + pub fn compare(&self, old: &Self, new_snapshot: &BufferSnapshot) -> Option> { + 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)); + } } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 995f3a65e6..a70ab85728 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/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, cx: &mut Context) { + fn buffer_diff_changed( + &mut self, + change_set: Entity, + range: Range, + cx: &mut Context, + ) { 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, cx: &mut Context) { 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>, - excerpts: &Cursor>, - old_diff_transforms: &mut Cursor, usize)>, - new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option<(TypedOffset, 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( diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 68ff9e9149..76cca4a830 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/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::>(); 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::(); 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(); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index e10bfa33ce..17098bed20 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -69,6 +69,10 @@ pub struct BufferChangeSet { pub language_registry: Option>, } +pub enum BufferChangeSetEvent { + DiffChanged { changed_range: Range }, +} + enum BufferStoreState { Local(LocalBufferStore), Remote(RemoteBufferStore), @@ -2201,6 +2205,8 @@ impl BufferStore { } } +impl EventEmitter for BufferChangeSet {} + impl BufferChangeSet { pub fn new(buffer: &Entity, cx: &mut Context) -> 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, - ) { - 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 { diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 71882bd40f..f8fc1a1654 100644 --- a/crates/text/Cargo.toml +++ b/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