From ed3e18de6f024b0351994fedc7b281511f11b585 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Feb 2026 16:36:23 -0700 Subject: [PATCH] Reduce rewrapping when agent diffs are present (#48423) Before this change any agent diff caused us to recompute the entire buffer's wraps, because we didn't handle the base text of diffs changing. Now, thanks to anchors, we can do that. Release Notes: - Fixed the editor rewrapping like crazy when agent edits were present --- crates/buffer_diff/src/buffer_diff.rs | 519 ++++++++++++++++++++++---- 1 file changed, 448 insertions(+), 71 deletions(-) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 5919770c61397d1b275ae3fb970887f8dee24dd0..51e0a5be572e0ddc0e769dacde9cbb46a719b2a9 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1019,6 +1019,8 @@ fn compare_hunks( old_hunks: &SumTree, old_snapshot: &text::BufferSnapshot, new_snapshot: &text::BufferSnapshot, + old_base_text: &text::BufferSnapshot, + new_base_text: &text::BufferSnapshot, ) -> DiffChanged { let mut new_cursor = new_hunks.cursor::<()>(new_snapshot); let mut old_cursor = old_hunks.cursor::<()>(new_snapshot); @@ -1026,8 +1028,8 @@ fn compare_hunks( new_cursor.next(); let mut start = None; let mut end = None; - let mut base_text_start = None; - let mut base_text_end = None; + let mut base_text_start: Option = None; + let mut base_text_end: Option = None; let mut last_unchanged_new_hunk_end: Option = None; let mut has_changes = false; @@ -1045,9 +1047,19 @@ fn compare_hunks( has_changes = true; extended_end_candidate = None; start.get_or_insert(new_hunk.buffer_range.start); - base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start); + base_text_start.get_or_insert( + new_base_text.anchor_before(new_hunk.diff_base_byte_range.start), + ); end.replace(new_hunk.buffer_range.end); - base_text_end = base_text_end.max(Some(new_hunk.diff_base_byte_range.end)); + let new_diff_range_end = + new_base_text.anchor_after(new_hunk.diff_base_byte_range.end); + if base_text_end.is_none_or(|base_text_end| { + new_diff_range_end + .cmp(&base_text_end, &new_base_text) + .is_gt() + }) { + base_text_end = Some(new_diff_range_end) + } new_cursor.next(); } Ordering::Equal => { @@ -1055,7 +1067,9 @@ fn compare_hunks( has_changes = true; extended_end_candidate = None; start.get_or_insert(new_hunk.buffer_range.start); - base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start); + base_text_start.get_or_insert( + new_base_text.anchor_before(new_hunk.diff_base_byte_range.start), + ); if old_hunk .buffer_range .end @@ -1067,11 +1081,14 @@ fn compare_hunks( end.replace(new_hunk.buffer_range.end); } + let old_hunk_diff_base_range_end = + old_base_text.anchor_after(old_hunk.diff_base_byte_range.end); + let new_hunk_diff_base_range_end = + new_base_text.anchor_after(new_hunk.diff_base_byte_range.end); + base_text_end.replace( - old_hunk - .diff_base_byte_range - .end - .max(new_hunk.diff_base_byte_range.end), + *old_hunk_diff_base_range_end + .max(&new_hunk_diff_base_range_end, new_base_text), ); } else { if !has_changes { @@ -1088,9 +1105,19 @@ fn compare_hunks( has_changes = true; extended_end_candidate = None; start.get_or_insert(old_hunk.buffer_range.start); - base_text_start.get_or_insert(old_hunk.diff_base_byte_range.start); + base_text_start.get_or_insert( + old_base_text.anchor_after(old_hunk.diff_base_byte_range.start), + ); end.replace(old_hunk.buffer_range.end); - base_text_end = base_text_end.max(Some(old_hunk.diff_base_byte_range.end)); + let old_diff_range_end = + old_base_text.anchor_after(old_hunk.diff_base_byte_range.end); + if base_text_end.is_none_or(|base_text_end| { + old_diff_range_end + .cmp(&base_text_end, new_base_text) + .is_gt() + }) { + base_text_end = Some(old_diff_range_end) + } old_cursor.next(); } } @@ -1099,24 +1126,38 @@ fn compare_hunks( has_changes = true; extended_end_candidate = None; start.get_or_insert(new_hunk.buffer_range.start); - base_text_start.get_or_insert(new_hunk.diff_base_byte_range.start); + base_text_start + .get_or_insert(new_base_text.anchor_after(new_hunk.diff_base_byte_range.start)); if end.is_none_or(|end| end.cmp(&new_hunk.buffer_range.end, &new_snapshot).is_le()) { end.replace(new_hunk.buffer_range.end); } - base_text_end = base_text_end.max(Some(new_hunk.diff_base_byte_range.end)); + let new_base_text_end = + new_base_text.anchor_after(new_hunk.diff_base_byte_range.end); + if base_text_end.is_none_or(|base_text_end| { + new_base_text_end.cmp(&base_text_end, new_base_text).is_gt() + }) { + base_text_end = Some(new_base_text_end) + } new_cursor.next(); } (None, Some(old_hunk)) => { has_changes = true; extended_end_candidate = None; start.get_or_insert(old_hunk.buffer_range.start); - base_text_start.get_or_insert(old_hunk.diff_base_byte_range.start); + base_text_start + .get_or_insert(old_base_text.anchor_after(old_hunk.diff_base_byte_range.start)); if end.is_none_or(|end| end.cmp(&old_hunk.buffer_range.end, &new_snapshot).is_le()) { end.replace(old_hunk.buffer_range.end); } - base_text_end = base_text_end.max(Some(old_hunk.diff_base_byte_range.end)); + let old_base_text_end = + old_base_text.anchor_after(old_hunk.diff_base_byte_range.end); + if base_text_end.is_none_or(|base_text_end| { + old_base_text_end.cmp(&base_text_end, new_base_text).is_gt() + }) { + base_text_end = Some(old_base_text_end); + } old_cursor.next(); } (None, None) => break, @@ -1126,7 +1167,7 @@ fn compare_hunks( let changed_range = start.zip(end).map(|(start, end)| start..end); let base_text_changed_range = base_text_start .zip(base_text_end) - .map(|(start, end)| start..end); + .map(|(start, end)| (start..end).to_offset(new_base_text)); let extended_range = if has_changes && let Some(changed_range) = changed_range.clone() { let extended_start = *last_unchanged_new_hunk_end @@ -1570,22 +1611,46 @@ impl BufferDiff { log::debug!("set snapshot with secondary {secondary_diff_change:?}"); let old_snapshot = self.snapshot(cx); - let state = &mut self.inner; let new_state = update.inner; let base_text_changed = update.base_text_changed; + let state = &mut self.inner; + state.base_text_exists = new_state.base_text_exists; + let should_compare_hunks = update.base_text_edits.is_some() || !base_text_changed; + let parsing_idle = if let Some(diff) = update.base_text_edits { + state.base_text.update(cx, |base_text, cx| { + base_text.set_capability(Capability::ReadWrite, cx); + base_text.apply_diff(diff, cx); + base_text.set_capability(Capability::ReadOnly, cx); + Some(base_text.parsing_idle()) + }) + } else if update.base_text_changed { + state.base_text.update(cx, |base_text, cx| { + base_text.set_capability(Capability::ReadWrite, cx); + base_text.set_text(new_state.base_text.clone(), cx); + base_text.set_capability(Capability::ReadOnly, cx); + Some(base_text.parsing_idle()) + }) + } else { + None + }; + let old_buffer_snapshot = &old_snapshot.inner.buffer_snapshot; + let old_base_snapshot = &old_snapshot.inner.base_text; + let new_base_snapshot = state.base_text.read(cx).snapshot(); let DiffChanged { mut changed_range, mut base_text_changed_range, mut extended_range, } = match (state.base_text_exists, new_state.base_text_exists) { (false, false) => DiffChanged::default(), - (true, true) if !base_text_changed => compare_hunks( + (true, true) if should_compare_hunks => compare_hunks( &new_state.hunks, &old_snapshot.inner.hunks, old_buffer_snapshot, buffer, + old_base_snapshot, + &new_base_snapshot, ), _ => { let full_range = text::Anchor::min_max_range_for_buffer(self.buffer_id); @@ -1597,54 +1662,9 @@ impl BufferDiff { } } }; - - if let Some(secondary_changed_range) = secondary_diff_change - && let (Some(secondary_hunk_range), Some(secondary_base_range)) = - old_snapshot.range_to_hunk_range(secondary_changed_range, buffer) - { - if let Some(range) = &mut changed_range { - range.start = *secondary_hunk_range.start.min(&range.start, buffer); - range.end = *secondary_hunk_range.end.max(&range.end, buffer); - } else { - changed_range = Some(secondary_hunk_range.clone()); - } - - if let Some(base_text_range) = base_text_changed_range.as_mut() { - base_text_range.start = secondary_base_range.start.min(base_text_range.start); - base_text_range.end = secondary_base_range.end.max(base_text_range.end); - } else { - base_text_changed_range = Some(secondary_base_range); - } - - if let Some(ext) = &mut extended_range { - ext.start = *ext.start.min(&secondary_hunk_range.start, buffer); - ext.end = *ext.end.max(&secondary_hunk_range.end, buffer); - } else { - extended_range = Some(secondary_hunk_range); - } - } - - let state = &mut self.inner; - state.base_text_exists = new_state.base_text_exists; - let parsing_idle = if let Some(diff) = update.base_text_edits { - state.base_text.update(cx, |base_text, cx| { - base_text.set_capability(Capability::ReadWrite, cx); - base_text.apply_diff(diff, cx); - base_text.set_capability(Capability::ReadOnly, cx); - Some(base_text.parsing_idle()) - }) - } else if update.base_text_changed { - state.base_text.update(cx, |base_text, cx| { - base_text.set_capability(Capability::ReadWrite, cx); - base_text.set_text(new_state.base_text.clone(), cx); - base_text.set_capability(Capability::ReadOnly, cx); - Some(base_text.parsing_idle()) - }) - } else { - None - }; state.hunks = new_state.hunks; state.buffer_snapshot = update.buffer_snapshot; + if base_text_changed || clear_pending_hunks { if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last()) { @@ -1675,6 +1695,32 @@ impl BufferDiff { state.pending_hunks = SumTree::new(buffer); } + if let Some(secondary_changed_range) = secondary_diff_change + && let (Some(secondary_hunk_range), Some(secondary_base_range)) = + old_snapshot.range_to_hunk_range(secondary_changed_range, buffer) + { + if let Some(range) = &mut changed_range { + range.start = *secondary_hunk_range.start.min(&range.start, buffer); + range.end = *secondary_hunk_range.end.max(&range.end, buffer); + } else { + changed_range = Some(secondary_hunk_range.clone()); + } + + if let Some(base_text_range) = base_text_changed_range.as_mut() { + base_text_range.start = secondary_base_range.start.min(base_text_range.start); + base_text_range.end = secondary_base_range.end.max(base_text_range.end); + } else { + base_text_changed_range = Some(secondary_base_range); + } + + if let Some(ext) = &mut extended_range { + ext.start = *ext.start.min(&secondary_hunk_range.start, buffer); + ext.end = *ext.end.max(&secondary_hunk_range.end, buffer); + } else { + extended_range = Some(secondary_hunk_range); + } + } + async move { if let Some(parsing_idle) = parsing_idle { parsing_idle.await; @@ -2595,6 +2641,8 @@ mod tests { &empty_diff.inner.hunks, &buffer, &buffer, + &diff_1.base_text(), + &diff_1.base_text(), ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0)); @@ -2623,7 +2671,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_2.inner.hunks, &diff_1.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_2.inner.hunks, + &diff_1.inner.hunks, + &buffer, + &buffer, + diff_2.base_text(), + diff_2.base_text(), + ); assert_eq!( changed_range.unwrap().to_point(&buffer), Point::new(4, 0)..Point::new(5, 0), @@ -2654,7 +2709,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_3.inner.hunks, &diff_2.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_3.inner.hunks, + &diff_2.inner.hunks, + &buffer, + &buffer, + diff_3.base_text(), + diff_3.base_text(), + ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0)); let base_text_range = base_text_changed_range.unwrap(); @@ -2681,7 +2743,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_4.inner.hunks, &diff_3.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_4.inner.hunks, + &diff_3.inner.hunks, + &buffer, + &buffer, + diff_4.base_text(), + diff_4.base_text(), + ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0)); let base_text_range = base_text_changed_range.unwrap(); @@ -2709,7 +2778,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_5.inner.hunks, &diff_4.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_5.inner.hunks, + &diff_4.inner.hunks, + &buffer, + &buffer, + diff_5.base_text(), + diff_5.base_text(), + ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0)); let base_text_range = base_text_changed_range.unwrap(); @@ -2737,7 +2813,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_6.inner.hunks, &diff_5.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_6.inner.hunks, + &diff_5.inner.hunks, + &buffer, + &buffer, + diff_6.base_text(), + diff_6.base_text(), + ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0)); let base_text_range = base_text_changed_range.unwrap(); @@ -2764,7 +2847,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_7.inner.hunks, &diff_6.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_7.inner.hunks, + &diff_6.inner.hunks, + &buffer, + &buffer, + diff_7.base_text(), + diff_7.base_text(), + ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(2, 4)..Point::new(7, 0)); let base_text_range = base_text_changed_range.unwrap(); @@ -2790,7 +2880,14 @@ mod tests { changed_range, base_text_changed_range, extended_range: _, - } = compare_hunks(&diff_8.inner.hunks, &diff_7.inner.hunks, &buffer, &buffer); + } = compare_hunks( + &diff_8.inner.hunks, + &diff_7.inner.hunks, + &buffer, + &buffer, + diff_8.base_text(), + diff_8.base_text(), + ); let range = changed_range.unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(3, 4)); let base_text_range = base_text_changed_range.unwrap(); @@ -3090,6 +3187,8 @@ mod tests { &diff_a.inner.hunks, &old_buffer, &buffer, + &diff_a.base_text(), + &diff_a.base_text(), ); let changed_range = changed_range.unwrap(); @@ -3151,6 +3250,8 @@ mod tests { &diff_2a.inner.hunks, &old_buffer_2, &buffer_2, + &diff_2a.base_text(), + &diff_2a.base_text(), ); let changed_range = changed_range.unwrap(); @@ -3167,4 +3268,280 @@ mod tests { "extended_range should equal changed_range when edit is within the hunk" ); } + + #[gpui::test] + async fn test_buffer_diff_compare_with_base_text_change(_cx: &mut TestAppContext) { + // Use a shared base text buffer so that anchors from old and new snapshots + // share the same remote_id and resolve correctly across versions. + let initial_base = "aaa\nbbb\nccc\nddd\neee\n"; + let mut base_text_buffer = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(99).unwrap(), + initial_base.to_string(), + ); + + // --- Scenario 1: Base text gains a line, producing a new deletion hunk --- + // + // Buffer has a modification (ccc → CCC). When the base text gains + // a new line "XXX" after "aaa", the diff now also contains a + // deletion for that line, and the modification hunk shifts in the + // base text. + let buffer_text_1 = "aaa\nbbb\nCCC\nddd\neee\n"; + let buffer = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(1).unwrap(), + buffer_text_1.to_string(), + ); + + let old_base_snapshot_1 = base_text_buffer.snapshot(); + let old_hunks_1 = compute_hunks( + Some((Arc::from(initial_base), Rope::from(initial_base))), + buffer.snapshot(), + None, + ); + + // Insert "XXX\n" after "aaa\n" in the base text. + base_text_buffer.edit([(4..4, "XXX\n")]); + let new_base_str_1: Arc = Arc::from(base_text_buffer.text().as_str()); + let new_base_snapshot_1 = base_text_buffer.snapshot(); + + let new_hunks_1 = compute_hunks( + Some((new_base_str_1.clone(), Rope::from(new_base_str_1.as_ref()))), + buffer.snapshot(), + None, + ); + + let DiffChanged { + changed_range, + base_text_changed_range, + extended_range: _, + } = compare_hunks( + &new_hunks_1, + &old_hunks_1, + &buffer.snapshot(), + &buffer.snapshot(), + &old_base_snapshot_1, + &new_base_snapshot_1, + ); + + // The new deletion hunk (XXX) starts at buffer row 1 and the + // modification hunk (ccc → CCC) now has a different + // diff_base_byte_range, so the changed range spans both. + let range = changed_range.unwrap(); + assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(3, 0),); + let base_range = base_text_changed_range.unwrap(); + assert_eq!( + base_range.to_point(&new_base_snapshot_1), + Point::new(1, 0)..Point::new(4, 0), + ); + + // --- Scenario 2: Base text changes to match the buffer (hunk disappears) --- + // + // Start fresh with a simple base text. + let simple_base = "one\ntwo\nthree\n"; + let mut base_buf_2 = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(100).unwrap(), + simple_base.to_string(), + ); + + let buffer_text_2 = "one\nTWO\nthree\n"; + let buffer_2 = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(2).unwrap(), + buffer_text_2.to_string(), + ); + + let old_base_snapshot_2 = base_buf_2.snapshot(); + let old_hunks_2 = compute_hunks( + Some((Arc::from(simple_base), Rope::from(simple_base))), + buffer_2.snapshot(), + None, + ); + + // The base text is edited so "two" becomes "TWO", now matching the buffer. + base_buf_2.edit([(4..7, "TWO")]); + let new_base_str_2: Arc = Arc::from(base_buf_2.text().as_str()); + let new_base_snapshot_2 = base_buf_2.snapshot(); + + let new_hunks_2 = compute_hunks( + Some((new_base_str_2.clone(), Rope::from(new_base_str_2.as_ref()))), + buffer_2.snapshot(), + None, + ); + + let DiffChanged { + changed_range, + base_text_changed_range, + extended_range: _, + } = compare_hunks( + &new_hunks_2, + &old_hunks_2, + &buffer_2.snapshot(), + &buffer_2.snapshot(), + &old_base_snapshot_2, + &new_base_snapshot_2, + ); + + // The old modification hunk (two → TWO) is now gone because the + // base text matches the buffer. The changed range covers where the + // old hunk used to be. + let range = changed_range.unwrap(); + assert_eq!( + range.to_point(&buffer_2), + Point::new(1, 0)..Point::new(2, 0), + ); + let base_range = base_text_changed_range.unwrap(); + // The old hunk's diff_base_byte_range covered "two\n" (bytes 4..8). + // anchor_after(4) is right-biased at the start of the deleted "two", + // so after the edit replacing "two" with "TWO" it resolves past the + // insertion to Point(1, 3). + assert_eq!( + base_range.to_point(&new_base_snapshot_2), + Point::new(1, 3)..Point::new(2, 0), + ); + + // --- Scenario 3: Base text edit changes one hunk but not another --- + // + // Two modification hunks exist. Only one of them is resolved by + // the base text change; the other remains identical. + let base_3 = "aaa\nbbb\nccc\nddd\neee\n"; + let mut base_buf_3 = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(101).unwrap(), + base_3.to_string(), + ); + + let buffer_text_3 = "aaa\nBBB\nccc\nDDD\neee\n"; + let buffer_3 = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(3).unwrap(), + buffer_text_3.to_string(), + ); + + let old_base_snapshot_3 = base_buf_3.snapshot(); + let old_hunks_3 = compute_hunks( + Some((Arc::from(base_3), Rope::from(base_3))), + buffer_3.snapshot(), + None, + ); + + // Change "ddd" to "DDD" in the base text so that hunk disappears, + // but "bbb" stays, so its hunk remains. + base_buf_3.edit([(12..15, "DDD")]); + let new_base_str_3: Arc = Arc::from(base_buf_3.text().as_str()); + let new_base_snapshot_3 = base_buf_3.snapshot(); + + let new_hunks_3 = compute_hunks( + Some((new_base_str_3.clone(), Rope::from(new_base_str_3.as_ref()))), + buffer_3.snapshot(), + None, + ); + + let DiffChanged { + changed_range, + base_text_changed_range, + extended_range: _, + } = compare_hunks( + &new_hunks_3, + &old_hunks_3, + &buffer_3.snapshot(), + &buffer_3.snapshot(), + &old_base_snapshot_3, + &new_base_snapshot_3, + ); + + // Only the second hunk (ddd → DDD) disappeared; the first hunk + // (bbb → BBB) is unchanged, so the changed range covers only line 3. + let range = changed_range.unwrap(); + assert_eq!( + range.to_point(&buffer_3), + Point::new(3, 0)..Point::new(4, 0), + ); + let base_range = base_text_changed_range.unwrap(); + // anchor_after(12) is right-biased at the start of deleted "ddd", + // so after the edit replacing "ddd" with "DDD" it resolves past + // the insertion to Point(3, 3). + assert_eq!( + base_range.to_point(&new_base_snapshot_3), + Point::new(3, 3)..Point::new(4, 0), + ); + + // --- Scenario 4: Both buffer and base text change simultaneously --- + // + // The buffer gains an edit that introduces a new hunk while the + // base text also changes. + let base_4 = "alpha\nbeta\ngamma\ndelta\n"; + let mut base_buf_4 = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(102).unwrap(), + base_4.to_string(), + ); + + let buffer_text_4 = "alpha\nBETA\ngamma\ndelta\n"; + let mut buffer_4 = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(4).unwrap(), + buffer_text_4.to_string(), + ); + + let old_base_snapshot_4 = base_buf_4.snapshot(); + let old_buffer_snapshot_4 = buffer_4.snapshot(); + let old_hunks_4 = compute_hunks( + Some((Arc::from(base_4), Rope::from(base_4))), + buffer_4.snapshot(), + None, + ); + + // Edit the buffer: change "delta" to "DELTA" (new modification hunk). + buffer_4.edit_via_marked_text( + &" + alpha + BETA + gamma + «DELTA» + " + .unindent(), + ); + + // Edit the base text: change "beta" to "BETA" (resolves that hunk). + base_buf_4.edit([(6..10, "BETA")]); + let new_base_str_4: Arc = Arc::from(base_buf_4.text().as_str()); + let new_base_snapshot_4 = base_buf_4.snapshot(); + + let new_hunks_4 = compute_hunks( + Some((new_base_str_4.clone(), Rope::from(new_base_str_4.as_ref()))), + buffer_4.snapshot(), + None, + ); + + let DiffChanged { + changed_range, + base_text_changed_range, + extended_range: _, + } = compare_hunks( + &new_hunks_4, + &old_hunks_4, + &old_buffer_snapshot_4, + &buffer_4.snapshot(), + &old_base_snapshot_4, + &new_base_snapshot_4, + ); + + // The old BETA hunk (line 1) is gone and a new DELTA hunk (line 3) + // appeared, so the changed range spans from line 1 through line 4. + let range = changed_range.unwrap(); + assert_eq!( + range.to_point(&buffer_4), + Point::new(1, 0)..Point::new(4, 0), + ); + let base_range = base_text_changed_range.unwrap(); + // The old BETA hunk's base range started at byte 6 ("beta"). After + // the base text edit replacing "beta" with "BETA", anchor_after(6) + // resolves past the insertion to Point(1, 4). + assert_eq!( + base_range.to_point(&new_base_snapshot_4), + Point::new(1, 4)..Point::new(4, 0), + ); + } }