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

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>
This commit is contained in:
Max Brunsfeld 2025-01-27 18:11:15 -08:00 committed by GitHub
parent 5331418f3a
commit ee5f270f3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 402 additions and 251 deletions

View file

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

View file

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

View file

@ -1,5 +1,5 @@
use rope::Rope; use rope::Rope;
use std::{iter, ops::Range}; use std::{cmp, iter, ops::Range};
use sum_tree::SumTree; use sum_tree::SumTree;
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; 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. /// 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 { struct InternalDiffHunk {
buffer_range: Range<Anchor>, buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>, 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)] #[cfg(test)]
fn clear(&mut self, buffer: &text::BufferSnapshot) { fn clear(&mut self, buffer: &text::BufferSnapshot) {
self.tree = SumTree::new(buffer); 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));
}
} }

View file

@ -21,7 +21,7 @@ use language::{
TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions,
Unclipped, Unclipped,
}; };
use project::buffer_store::BufferChangeSet; use project::buffer_store::{BufferChangeSet, BufferChangeSetEvent};
use rope::DimensionPair; use rope::DimensionPair;
use smallvec::SmallVec; use smallvec::SmallVec;
use smol::future::yield_now; use smol::future::yield_now;
@ -434,7 +434,6 @@ struct BufferEdit {
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
enum DiffChangeKind { enum DiffChangeKind {
BufferEdited, BufferEdited,
ExcerptsChanged,
DiffUpdated { base_changed: bool }, DiffUpdated { base_changed: bool },
ExpandOrCollapseHunks { expand: bool }, ExpandOrCollapseHunks { expand: bool },
} }
@ -546,8 +545,14 @@ impl MultiBuffer {
diff_bases.insert( diff_bases.insert(
*buffer_id, *buffer_id,
ChangeSetState { ChangeSetState {
_subscription: new_cx _subscription: new_cx.subscribe(
.observe(&change_set_state.change_set, Self::buffer_diff_changed), &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(), change_set: change_set_state.change_set.clone(),
}, },
); );
@ -1603,7 +1608,7 @@ impl MultiBuffer {
old: edit_start..edit_start, old: edit_start..edit_start,
new: edit_start..edit_end, new: edit_start..edit_end,
}], }],
DiffChangeKind::ExcerptsChanged, DiffChangeKind::BufferEdited,
); );
cx.emit(Event::Edited { cx.emit(Event::Edited {
singleton_buffer_edited: false, singleton_buffer_edited: false,
@ -1636,7 +1641,7 @@ impl MultiBuffer {
old: start..prev_len, old: start..prev_len,
new: start..start, new: start..start,
}], }],
DiffChangeKind::ExcerptsChanged, DiffChangeKind::BufferEdited,
); );
cx.emit(Event::Edited { cx.emit(Event::Edited {
singleton_buffer_edited: false, singleton_buffer_edited: false,
@ -1909,7 +1914,7 @@ impl MultiBuffer {
snapshot.trailing_excerpt_update_count += 1; 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 { cx.emit(Event::Edited {
singleton_buffer_edited: false, singleton_buffer_edited: false,
edited_buffer: None, 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 change_set = change_set.read(cx);
let buffer_id = change_set.buffer_id; let buffer_id = change_set.buffer_id;
let diff = change_set.diff_to_buffer.clone(); let diff = change_set.diff_to_buffer.clone();
let base_text = change_set.base_text.clone(); let base_text = change_set.base_text.clone();
self.sync(cx); self.sync(cx);
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
let base_text_version_changed = let base_text_changed = snapshot
snapshot .diffs
.diffs .get(&buffer_id)
.get(&buffer_id) .map_or(true, |diff_snapshot| {
.map_or(true, |diff_snapshot| { change_set.base_text.as_ref().map_or(true, |base_text| {
change_set.base_text.as_ref().map_or(true, |base_text| { base_text.remote_id() != diff_snapshot.base_text.remote_id()
base_text.remote_id() != diff_snapshot.base_text.remote_id() })
}) });
});
if let Some(base_text) = base_text { if let Some(base_text) = base_text {
snapshot.diffs.insert( snapshot.diffs.insert(
@ -2026,26 +2035,44 @@ impl MultiBuffer {
} else { } else {
snapshot.diffs.remove(&buffer_id); 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(); let mut excerpt_edits = Vec::new();
for locator in self for locator in &buffer_state.excerpts {
.buffers
.borrow()
.get(&buffer_id)
.map(|state| &state.excerpts)
.into_iter()
.flatten()
{
let mut cursor = snapshot let mut cursor = snapshot
.excerpts .excerpts
.cursor::<(Option<&Locator>, ExcerptOffset)>(&()); .cursor::<(Option<&Locator>, ExcerptOffset)>(&());
cursor.seek_forward(&Some(locator), Bias::Left, &()); cursor.seek_forward(&Some(locator), Bias::Left, &());
if let Some(excerpt) = cursor.item() { if let Some(excerpt) = cursor.item() {
if excerpt.locator == *locator { 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 { excerpt_edits.push(Edit {
old: excerpt_range.clone(), old: edit_start..edit_end,
new: excerpt_range.clone(), new: edit_start..edit_end,
}); });
} }
} }
@ -2055,7 +2082,7 @@ impl MultiBuffer {
snapshot, snapshot,
excerpt_edits, excerpt_edits,
DiffChangeKind::DiffUpdated { DiffChangeKind::DiffUpdated {
base_changed: base_text_version_changed, base_changed: base_text_changed,
}, },
); );
cx.emit(Event::Edited { 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>) { pub fn add_change_set(&mut self, change_set: Entity<BufferChangeSet>, cx: &mut Context<Self>) {
let buffer_id = change_set.read(cx).buffer_id; 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( self.diff_bases.insert(
buffer_id, buffer_id,
ChangeSetState { 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, change_set,
}, },
); );
@ -2329,7 +2363,7 @@ impl MultiBuffer {
drop(cursor); drop(cursor);
snapshot.excerpts = new_excerpts; snapshot.excerpts = new_excerpts;
self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged); self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
cx.emit(Event::Edited { cx.emit(Event::Edited {
singleton_buffer_edited: false, singleton_buffer_edited: false,
edited_buffer: None, edited_buffer: None,
@ -2430,7 +2464,7 @@ impl MultiBuffer {
drop(cursor); drop(cursor);
snapshot.excerpts = new_excerpts; snapshot.excerpts = new_excerpts;
self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged); self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
cx.emit(Event::Edited { cx.emit(Event::Edited {
singleton_buffer_edited: false, singleton_buffer_edited: false,
edited_buffer: None, edited_buffer: None,
@ -2595,63 +2629,52 @@ impl MultiBuffer {
let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; 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; let edit_new_start = (edit_old_start as isize + output_delta) as usize;
if change_kind == DiffChangeKind::BufferEdited { let changed_diff_hunks = self.recompute_diff_transforms_for_edit(
self.interpolate_diff_transforms_for_edit( &edit,
&edit, &mut excerpts,
&excerpts, &mut old_diff_transforms,
&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,
&mut new_diff_transforms, &mut new_diff_transforms,
edit.new.end, &mut end_of_current_insert,
end_of_current_insert, &mut old_expanded_hunks,
&snapshot,
change_kind,
); );
// Compute the end of the edit in output coordinates. // 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_overshoot = edit.old.end - old_diff_transforms.start().0;
let edit_old_end = old_diff_transforms.start().1 + edit_end_overshoot; let edit_new_end_overshoot = edit.new.end - new_diff_transforms.summary().excerpt_len();
let edit_new_end = new_diff_transforms.summary().output.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 { let output_edit = Edit {
old: edit_old_start..edit_old_end, old: edit_old_start..edit_old_end,
new: edit_new_start..edit_new_end, new: edit_new_start..edit_new_end,
}; };
output_delta += (output_edit.new.end - output_edit.new.start) as isize output_delta += (output_edit.new.end - output_edit.new.start) as isize;
- (output_edit.old.end - output_edit.old.start) as isize; output_delta -= (output_edit.old.end - output_edit.old.start) as isize;
output_edits.push(output_edit); 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, // 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| { if excerpt_edits.peek().map_or(true, |next_edit| {
next_edit.old.start >= old_diff_transforms.end(&()).0 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 { if old_diff_transforms.start().0 < edit.old.end {
let suffix = old_diff_transforms.end(&()).0 - edit.old.end; excerpt_offset += 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,
);
old_diff_transforms.next(&()); 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; at_transform_boundary = true;
} }
} }
@ -2691,7 +2714,7 @@ impl MultiBuffer {
old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>, old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>,
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
change_kind: DiffChangeKind, change_kind: DiffChangeKind,
) { ) -> bool {
log::trace!( log::trace!(
"recomputing diff transform for edit {:?} => {:?}", "recomputing diff transform for edit {:?} => {:?}",
edit.old.start.value..edit.old.end.value, edit.old.start.value..edit.old.end.value,
@ -2699,11 +2722,7 @@ impl MultiBuffer {
); );
// Record which hunks were previously expanded. // Record which hunks were previously expanded.
old_expanded_hunks.clear();
while let Some(item) = old_diff_transforms.item() { 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() { if let Some(hunk_anchor) = item.hunk_anchor() {
log::trace!( log::trace!(
"previously expanded hunk at {}", "previously expanded hunk at {}",
@ -2711,10 +2730,22 @@ impl MultiBuffer {
); );
old_expanded_hunks.insert(hunk_anchor); old_expanded_hunks.insert(hunk_anchor);
} }
if old_diff_transforms.end(&()).0 > edit.old.end {
break;
}
old_diff_transforms.next(&()); 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. // Visit each excerpt that intersects the edit.
let mut did_expand_hunks = false;
while let Some(excerpt) = excerpts.item() { while let Some(excerpt) = excerpts.item() {
if excerpt.text_summary.len == 0 { if excerpt.text_summary.len == 0 {
if excerpts.end(&()) <= edit.new.end { if excerpts.end(&()) <= edit.new.end {
@ -2754,8 +2785,10 @@ impl MultiBuffer {
+ ExcerptOffset::new( + ExcerptOffset::new(
hunk_buffer_range.start.saturating_sub(excerpt_buffer_start), hunk_buffer_range.start.saturating_sub(excerpt_buffer_start),
); );
let hunk_excerpt_end = excerpt_start let hunk_excerpt_end = excerpt_end.min(
+ ExcerptOffset::new(hunk_buffer_range.end - excerpt_buffer_start); excerpt_start
+ ExcerptOffset::new(hunk_buffer_range.end - excerpt_buffer_start),
);
self.push_buffer_content_transform( self.push_buffer_content_transform(
snapshot, snapshot,
@ -2787,7 +2820,11 @@ impl MultiBuffer {
}; };
if should_expand_hunk { 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() if !hunk.diff_base_byte_range.is_empty()
&& hunk_buffer_range.start >= edit_buffer_start && hunk_buffer_range.start >= edit_buffer_start
@ -2833,68 +2870,8 @@ impl MultiBuffer {
break; break;
} }
} }
}
fn interpolate_diff_transforms_for_edit( did_expand_hunks || !old_expanded_hunks.is_empty()
&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, &());
} }
fn append_diff_transforms( fn append_diff_transforms(

View file

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

View file

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

View file

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