From 8bf3b4fece0fe639be9db178b86571af3e9f68e8 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 3 Feb 2026 22:18:59 -0500 Subject: [PATCH] git: Refactor buffer_diff point translation APIs for more efficient side-by-side diff syncing (#48237) The side-by-side diff heavily relies on a primitive from `buffer_diff` that converts a point on one side of the diff to a range of points on the other side. The way this primitive is set up on main is pretty naive--every time we call `points_to_base_text_points` (or `base_text_points_to_points`), we need to iterate over all hunks in the diff. That's particularly bad for the case of constructing a new side-by-side diff starting from a multibuffer, because we call those APIs once per excerpt, and the number of excerpts is ~equal to the number of hunks. This PR changes the point translation APIs exposed by `buffer_diff` to make it easier to use them efficiently in `editor`. The new shape is a pair of functions that return a patch that can be used to translate from the main buffer to the base text or vice versa. When syncing edits through the block map that touch several excerpts for the same buffer, we can reuse this patch for excerpts after the first--so when building a new side-by-side diff, we'll iterate over each hunk just once. The shape of the new APIs also sets us up to scale down to cases like editing on the right-hand side of the diff: we can pass in a point range and give them permission to return an approximate patch that's only guaranteed to give the correct results when used with points in that range. For edits that only affect one excerpt, and given how the project diff is set up, that should allow us to skip iterating over most of the hunks in a buffer. Release Notes: - N/A --------- Co-authored-by: cameron --- crates/buffer_diff/src/buffer_diff.rs | 1446 +------------------- crates/editor/src/display_map.rs | 36 +- crates/editor/src/display_map/block_map.rs | 116 +- crates/editor/src/editor.rs | 25 +- crates/editor/src/split.rs | 308 ++--- crates/text/src/patch.rs | 40 + 6 files changed, 352 insertions(+), 1619 deletions(-) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 63df9b3cd3277b0493a07fe6c2414c5f3c777a71..5919770c61397d1b275ae3fb970887f8dee24dd0 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -6,42 +6,19 @@ use language::{ language_settings::language_settings, word_diff_ranges, }; use rope::Rope; -use std::{cmp::Ordering, future::Future, iter, ops::Range, sync::Arc}; +use std::{ + cmp::Ordering, + future::Future, + iter, + ops::{Range, RangeInclusive}, + sync::Arc, +}; use sum_tree::SumTree; use text::{ Anchor, Bias, BufferId, Edit, OffsetRangeExt, Patch, Point, ToOffset as _, ToPoint as _, }; use util::ResultExt; -fn translate_point_through_patch( - patch: &Patch, - point: Point, -) -> (Range, Range) { - let edits = patch.edits(); - - let ix = match edits.binary_search_by(|probe| probe.old.start.cmp(&point)) { - Ok(ix) => ix, - Err(ix) => { - if ix == 0 { - return (point..point, point..point); - } else { - ix - 1 - } - } - }; - - if let Some(edit) = edits.get(ix) { - if point > edit.old.end { - let translated = edit.new.end + (point - edit.old.end); - (translated..translated, point..point) - } else { - (edit.new.start..edit.new.end, edit.old.start..edit.old.end) - } - } else { - (point..point, point..point) - } -} - pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5; pub struct BufferDiff { @@ -430,15 +407,15 @@ impl BufferDiffSnapshot { result } - pub fn points_to_base_text_points<'a>( + /// Returns a patch mapping the provided main buffer snapshot to the base text of this diff. + /// + /// The returned patch is guaranteed to be accurate for all main buffer points in the provided range, + /// but not necessarily for points outside that range. + pub fn patch_for_buffer_range<'a>( &'a self, - points: impl IntoIterator + 'a, + _range: RangeInclusive, buffer: &'a text::BufferSnapshot, - ) -> ( - impl 'a + Iterator>, - Option>, - Option<(Point, Range)>, - ) { + ) -> Patch { let original_snapshot = self.original_buffer_snapshot(); let edits_since: Vec> = buffer @@ -447,7 +424,7 @@ impl BufferDiffSnapshot { let mut inverted_edits_since = Patch::new(edits_since); inverted_edits_since.invert(); - let composed = inverted_edits_since.compose( + inverted_edits_since.compose( self.inner .hunks .iter() @@ -475,42 +452,18 @@ impl BufferDiffSnapshot { None }, ), - ); - - let mut points = points.into_iter().peekable(); - - let first_group = points.peek().map(|point| { - let (_, old_range) = translate_point_through_patch(&composed, *point); - old_range - }); - - let prev_boundary = points.peek().and_then(|first_point| { - if first_point.row > 0 { - let prev_point = Point::new(first_point.row - 1, 0); - let (range, _) = translate_point_through_patch(&composed, prev_point); - Some((prev_point, range)) - } else { - None - } - }); - - let iter = points.map(move |point| { - let (range, _) = translate_point_through_patch(&composed, point); - range - }); - - (iter, first_group, prev_boundary) + ) } - pub fn base_text_points_to_points<'a>( + /// Returns a patch mapping the base text of this diff to the provided buffer snapshot. + /// + /// The returned patch is guaranteed to be accurate for all base text points in the provided range, + /// but not necessarily for points outside that range. + pub fn patch_for_base_text_range<'a>( &'a self, - points: impl IntoIterator + 'a, + _range: RangeInclusive, buffer: &'a text::BufferSnapshot, - ) -> ( - impl 'a + Iterator>, - Option>, - Option<(Point, Range)>, - ) { + ) -> Patch { let original_snapshot = self.original_buffer_snapshot(); let mut hunk_edits: Vec> = Vec::new(); @@ -536,31 +489,55 @@ impl BufferDiffSnapshot { } let hunk_patch = Patch::new(hunk_edits); - let composed = hunk_patch.compose(buffer.edits_since::(original_snapshot.version())); - - let mut points = points.into_iter().peekable(); + hunk_patch.compose(buffer.edits_since::(original_snapshot.version())) + } - let first_group = points.peek().map(|point| { - let (_, result) = translate_point_through_patch(&composed, *point); - result - }); + pub fn buffer_point_to_base_text_range( + &self, + point: Point, + buffer: &text::BufferSnapshot, + ) -> Range { + let patch = self.patch_for_buffer_range(point..=point, buffer); + let edit = patch.edit_for_old_position(point); + edit.new + } - let prev_boundary = points.peek().and_then(|first_point| { - if first_point.row > 0 { - let prev_point = Point::new(first_point.row - 1, 0); - let (range, _) = translate_point_through_patch(&composed, prev_point); - Some((prev_point, range)) - } else { - None - } - }); + pub fn base_text_point_to_buffer_range( + &self, + point: Point, + buffer: &text::BufferSnapshot, + ) -> Range { + let patch = self.patch_for_base_text_range(point..=point, buffer); + let edit = patch.edit_for_old_position(point); + edit.new + } - let iter = points.map(move |point| { - let (range, _) = translate_point_through_patch(&composed, point); - range - }); + pub fn buffer_point_to_base_text_point( + &self, + point: Point, + buffer: &text::BufferSnapshot, + ) -> Point { + let patch = self.patch_for_buffer_range(point..=point, buffer); + let edit = patch.edit_for_old_position(point); + if point == edit.old.end { + edit.new.end + } else { + edit.new.start + } + } - (iter, first_group, prev_boundary) + pub fn base_text_point_to_buffer_point( + &self, + point: Point, + buffer: &text::BufferSnapshot, + ) -> Point { + let patch = self.patch_for_base_text_range(point..=point, buffer); + let edit = patch.edit_for_old_position(point); + if point == edit.old.end { + edit.new.end + } else { + edit.new.start + } } } @@ -3190,1291 +3167,4 @@ mod tests { "extended_range should equal changed_range when edit is within the hunk" ); } - - fn assert_rows_to_base_text_rows_visual( - buffer: &Entity, - diff: &Entity, - source_text: &str, - annotated_target: &str, - cx: &mut gpui::TestAppContext, - ) { - let (target_text, expected_ranges) = parse_row_annotations(annotated_target); - - let buffer = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); - let diff = diff.update(cx, |diff, cx| diff.snapshot(cx)); - - assert_eq!( - buffer.text(), - source_text, - "buffer text does not match source text" - ); - - assert_eq!( - diff.base_text_string().unwrap_or_default(), - target_text, - "base text does not match stripped annotated target" - ); - - let num_rows = source_text.lines().count() as u32; - let max_point = buffer.max_point(); - let points = (0..=num_rows).map(move |row| { - if row == num_rows && max_point.column > 0 { - max_point - } else { - Point::new(row, 0) - } - }); - let actual_ranges: Vec<_> = diff.points_to_base_text_points(points, &buffer).0.collect(); - - assert_eq!( - actual_ranges, expected_ranges, - "\nsource (buffer):\n{}\ntarget (base):\n{}\nexpected: {:?}\nactual: {:?}", - source_text, target_text, expected_ranges, actual_ranges - ); - } - - fn assert_base_text_rows_to_rows_visual( - buffer: &Entity, - diff: &Entity, - source_text: &str, - annotated_target: &str, - cx: &mut gpui::TestAppContext, - ) { - let (target_text, expected_ranges) = parse_row_annotations(annotated_target); - - let buffer = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); - let diff = diff.update(cx, |diff, cx| diff.snapshot(cx)); - - assert_eq!( - diff.base_text_string().unwrap_or_default(), - source_text, - "base text does not match source text" - ); - - assert_eq!( - buffer.text(), - target_text, - "buffer text does not match stripped annotated target" - ); - - let num_rows = source_text.lines().count() as u32; - let base_max_point = diff.base_text().max_point(); - let points = (0..=num_rows).map(move |row| { - if row == num_rows && base_max_point.column > 0 { - base_max_point - } else { - Point::new(row, 0) - } - }); - let actual_ranges: Vec<_> = diff.base_text_points_to_points(points, &buffer).0.collect(); - - assert_eq!( - actual_ranges, expected_ranges, - "\nsource (base):\n{}\ntarget (buffer):\n{}\nexpected: {:?}\nactual: {:?}", - source_text, target_text, expected_ranges, actual_ranges - ); - } - - fn parse_row_annotations(annotated_text: &str) -> (String, Vec>) { - let mut starts: std::collections::HashMap = std::collections::HashMap::new(); - let mut ends: std::collections::HashMap = std::collections::HashMap::new(); - - let mut clean_text = String::new(); - let mut current_point = Point::new(0, 0); - let mut chars = annotated_text.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '<' { - let mut num_str = String::new(); - while let Some(&next) = chars.peek() { - if next.is_ascii_digit() { - num_str.push(chars.next().unwrap()); - } else { - break; - } - } - if !num_str.is_empty() { - let row_num: u32 = num_str.parse().unwrap(); - starts.insert(row_num, current_point); - - if chars.peek() == Some(&'>') { - chars.next(); - ends.insert(row_num, current_point); - } - } else { - clean_text.push(c); - current_point.column += 1; - } - } else if c.is_ascii_digit() { - let mut num_str = String::from(c); - while let Some(&next) = chars.peek() { - if next.is_ascii_digit() { - num_str.push(chars.next().unwrap()); - } else { - break; - } - } - if chars.peek() == Some(&'>') { - chars.next(); - let row_num: u32 = num_str.parse().unwrap(); - ends.insert(row_num, current_point); - } else { - for ch in num_str.chars() { - clean_text.push(ch); - current_point.column += 1; - } - } - } else if c == '\n' { - clean_text.push(c); - current_point.row += 1; - current_point.column = 0; - } else { - clean_text.push(c); - current_point.column += 1; - } - } - - let max_row = starts.keys().chain(ends.keys()).max().copied().unwrap_or(0); - let mut ranges: Vec> = Vec::new(); - for row in 0..=max_row { - let start = starts.get(&row).copied().unwrap_or(Point::new(0, 0)); - let end = ends.get(&row).copied().unwrap_or(start); - ranges.push(start..end); - } - - (clean_text, ranges) - } - - fn make_diff( - base_text: &str, - buffer_text: &str, - cx: &mut gpui::TestAppContext, - ) -> (Entity, Entity) { - let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx)); - let diff = cx.new(|cx| { - BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx) - }); - (buffer, diff) - } - - #[gpui::test] - async fn test_row_translation_visual(cx: &mut gpui::TestAppContext) { - use unindent::Unindent; - - { - let buffer_text = " - aaa - bbb - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1>bbb - <2>ccc - <3>" - .unindent(); - let (base_text, _) = parse_row_annotations(&annotated_base); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_rows_to_base_text_rows_visual(&buffer, &diff, &buffer_text, &annotated_base, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let annotated_buffer = " - <0>aaa - <1>bbb - <2>ccc - <3>" - .unindent(); - let (buffer_text, _) = parse_row_annotations(&annotated_buffer); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let buffer_text = " - XXX - bbb - ccc - " - .unindent(); - let annotated_base = " - <0<1aaa - 0>1>bbb - <2>ccc - <3>" - .unindent(); - let (base_text, _) = parse_row_annotations(&annotated_base); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_rows_to_base_text_rows_visual(&buffer, &diff, &buffer_text, &annotated_base, cx); - } - - { - let buffer_text = " - aaa - NEW - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1><2>ccc - <3>" - .unindent(); - let (base_text, _) = parse_row_annotations(&annotated_base); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_rows_to_base_text_rows_visual(&buffer, &diff, &buffer_text, &annotated_base, cx); - } - - { - let base_text = " - aaa - ccc - " - .unindent(); - let annotated_buffer = " - <0>aaa - <1NEW - 1>ccc - <2>" - .unindent(); - let (buffer_text, _) = parse_row_annotations(&annotated_buffer); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let buffer_text = "aaa\nbbb"; - let annotated_base = "<0>aaa\n<1>bbb<2>"; - let (base_text, _) = parse_row_annotations(annotated_base); - let (buffer, diff) = make_diff(&base_text, buffer_text, cx); - assert_rows_to_base_text_rows_visual(&buffer, &diff, buffer_text, annotated_base, cx); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, annotated_base, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let annotated_buffer = " - <0<1XXX - 0>1>bbb - <2>ccc - <3>" - .unindent(); - let (buffer_text, _) = parse_row_annotations(&annotated_buffer); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let buffer_text = " - aaa - bbb - XXX - " - .unindent(); - let annotated_base = " - <0>aaa - <1>bbb - <2<3ccc - 2>3>" - .unindent(); - let (base_text, _) = parse_row_annotations(&annotated_base); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_rows_to_base_text_rows_visual(&buffer, &diff, &buffer_text, &annotated_base, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let annotated_buffer = " - <0>aaa - <1>bbb - <2<3XXX - 2>3>" - .unindent(); - let (buffer_text, _) = parse_row_annotations(&annotated_buffer); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let buffer_text = " - aaa - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1DELETED - 1>ccc - <2>" - .unindent(); - let (base_text, _) = parse_row_annotations(&annotated_base); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_rows_to_base_text_rows_visual(&buffer, &diff, &buffer_text, &annotated_base, cx); - } - - { - let base_text = " - aaa - DELETED - ccc - " - .unindent(); - let annotated_buffer = " - <0>aaa - <1><2>ccc - <3>" - .unindent(); - let (buffer_text, _) = parse_row_annotations(&annotated_buffer); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - } - - #[gpui::test] - async fn test_row_translation_with_edits_since_diff(cx: &mut gpui::TestAppContext) { - use unindent::Unindent; - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..7, "XXX")], None, cx); - }); - - let new_buffer_text = " - aaa - XXX - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1bbb1> - <2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..7, "XXX")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1XXX1> - <2>ccc - <3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "NEW\n")], None, cx); - }); - - let new_buffer_text = " - aaa - NEW - bbb - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1><2>bbb - <3>ccc - <4>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "NEW\n")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1NEW - 1>bbb - <2>ccc - <3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..8, "")], None, cx); - }); - - let new_buffer_text = " - aaa - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1bbb - 1>ccc - <2>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..8, "")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1><2>ccc - <3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - ddd - eee - " - .unindent(); - let buffer_text = " - aaa - XXX - ccc - ddd - eee - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(12..15, "YYY")], None, cx); - }); - - let new_buffer_text = " - aaa - XXX - ccc - YYY - eee - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3ddd3> - <4>eee - <5>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - ddd - eee - " - .unindent(); - let buffer_text = " - aaa - XXX - ccc - ddd - eee - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(12..15, "YYY")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1<2XXX - 1>2>ccc - <3YYY3> - <4>eee - <5>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "NEW\n")], None, cx); - }); - - let new_buffer_text = " - NEW - aaa - bbb - ccc - " - .unindent(); - let annotated_base = " - <0><1>aaa - <2>bbb - <3>ccc - <4>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "NEW\n")], None, cx); - }); - - let annotated_buffer = " - <0NEW - 0>aaa - <1>bbb - <2>ccc - <3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(12..12, "NEW\n")], None, cx); - }); - - let new_buffer_text = " - aaa - bbb - ccc - NEW - " - .unindent(); - let annotated_base = " - <0>aaa - <1>bbb - <2>ccc - <3><4>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = base_text.clone(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(12..12, "NEW\n")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1>bbb - <2>ccc - <3NEW - 3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = ""; - let buffer_text = "aaa\n"; - let (buffer, diff) = make_diff(base_text, buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "bbb\n")], None, cx); - }); - - let new_buffer_text = " - aaa - bbb - " - .unindent(); - let annotated_base = "<0><1><2>"; - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = "aaa\n"; - let buffer_text = ""; - let (buffer, diff) = make_diff(base_text, buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "bbb\n")], None, cx); - }); - - let new_buffer_text = "bbb\n"; - let annotated_base = " - <0<1aaa - 0>1>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = ""; - let buffer_text = ""; - let (buffer, diff) = make_diff(base_text, buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "aaa\n")], None, cx); - }); - - let new_buffer_text = "aaa\n"; - let annotated_base = "<0><1>"; - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..7, "YYY")], None, cx); - }); - - let new_buffer_text = " - aaa - YYY - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..7, "YYY")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1<2YYY - 1>2>ccc - <3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..6, "YY")], None, cx); - }); - - let new_buffer_text = " - aaa - YYXX - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(6..8, "YY")], None, cx); - }); - - let new_buffer_text = " - aaa - XXYY - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "NEW")], None, cx); - }); - - let new_buffer_text = " - aaa - NEWXXX - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(7..7, "NEW")], None, cx); - }); - - let new_buffer_text = " - aaa - XXXNEW - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "NEW\n")], None, cx); - }); - - let new_buffer_text = " - aaa - NEW - ccc - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2bbb - 1>2>ccc - <3>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "NEW\n")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1<2NEW - 1>2>ccc - <3>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - ddd - " - .unindent(); - let buffer_text = " - aaa - ddd - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "XXX\nYYY\n")], None, cx); - }); - - let new_buffer_text = " - aaa - XXX - YYY - ddd - " - .unindent(); - let annotated_base = " - <0>aaa - <1<2<3bbb - ccc - 1>2>3>ddd - <4>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - ddd - " - .unindent(); - let buffer_text = " - aaa - ddd - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "XXX\nYYY\n")], None, cx); - }); - - let annotated_buffer = " - <0>aaa - <1<2<3XXX - YYY - 1>2>3>ddd - <4>" - .unindent(); - assert_base_text_rows_to_rows_visual(&buffer, &diff, &base_text, &annotated_buffer, cx); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(2..10, "YY\nZZ")], None, cx); - }); - - let new_buffer_text = " - aaYY - ZZcc - " - .unindent(); - let annotated_base = " - <0>aa<1a - bbb - c1>cc - <2>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - - { - let base_text = " - aaa - bbb - ccc - " - .unindent(); - let buffer_text = " - aaa - XXXX - ccc - " - .unindent(); - let (buffer, diff) = make_diff(&base_text, &buffer_text, cx); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..9, "ZZ\n")], None, cx); - }); - - let new_buffer_text = " - ZZ - ccc - " - .unindent(); - let annotated_base = " - <0<1aaa - bbb - 0>1>ccc - <2>" - .unindent(); - assert_rows_to_base_text_rows_visual( - &buffer, - &diff, - &new_buffer_text, - &annotated_base, - cx, - ); - } - } - - #[gpui::test] - async fn test_row_translation_no_base_text(cx: &mut gpui::TestAppContext) { - let buffer_text = "aaa\nbbb\nccc\n"; - let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx)); - let diff = cx.new(|cx| BufferDiff::new(&buffer.read(cx).text_snapshot(), cx)); - - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot()); - let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx)); - - let points = vec![ - Point::new(0, 0), - Point::new(1, 0), - Point::new(2, 0), - Point::new(3, 0), - ]; - let base_rows: Vec<_> = diff_snapshot - .points_to_base_text_points(points, &buffer_snapshot) - .0 - .collect(); - - let zero = Point::new(0, 0); - assert_eq!( - base_rows, - vec![zero..zero, zero..zero, zero..zero, zero..zero], - "all buffer rows should map to point 0,0 in empty base text" - ); - - let base_points = vec![Point::new(0, 0)]; - let (rows_iter, _, _) = - diff_snapshot.base_text_points_to_points(base_points, &buffer_snapshot); - let buffer_rows: Vec<_> = rows_iter.collect(); - - let max_point = buffer_snapshot.max_point(); - assert_eq!( - buffer_rows, - vec![zero..max_point], - "base text row 0 should map to entire buffer range" - ); - } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 696c1aed868047a110ec290332e298e9c801db45..0f83c0c518de998158b618b046f8ef49e84677cc 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -150,12 +150,11 @@ type TextHighlights = TreeMap>; #[derive(Debug)] -pub struct MultiBufferRowMapping { - pub first_group: Option>, - pub boundaries: Vec<(MultiBufferPoint, Range)>, - pub prev_boundary: Option<(MultiBufferPoint, Range)>, - pub source_excerpt_end: MultiBufferPoint, - pub target_excerpt_end: MultiBufferPoint, +pub struct CompanionExcerptPatch { + pub patch: Patch, + pub edited_range: Range, + pub source_excerpt_range: Range, + pub target_excerpt_range: Range, } pub type ConvertMultiBufferRows = fn( @@ -163,7 +162,7 @@ pub type ConvertMultiBufferRows = fn( &MultiBufferSnapshot, &MultiBufferSnapshot, (Bound, Bound), -) -> Vec; +) -> Vec; /// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints, /// folding, hard tabs, soft wrapping, custom blocks (like diagnostics), and highlighting. @@ -233,7 +232,7 @@ impl Companion { companion_snapshot: &MultiBufferSnapshot, our_snapshot: &MultiBufferSnapshot, bounds: (Bound, Bound), - ) -> Vec { + ) -> Vec { let (excerpt_map, convert_fn) = if display_map_id == self.rhs_display_map_id { (&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows) } else { @@ -242,19 +241,32 @@ impl Companion { convert_fn(excerpt_map, companion_snapshot, our_snapshot, bounds) } - pub(crate) fn convert_rows_from_companion( + pub(crate) fn convert_point_from_companion( &self, display_map_id: EntityId, our_snapshot: &MultiBufferSnapshot, companion_snapshot: &MultiBufferSnapshot, - bounds: (Bound, Bound), - ) -> Vec { + point: MultiBufferPoint, + ) -> Range { let (excerpt_map, convert_fn) = if display_map_id == self.rhs_display_map_id { (&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows) } else { (&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows) }; - convert_fn(excerpt_map, our_snapshot, companion_snapshot, bounds) + + let excerpt = convert_fn( + excerpt_map, + our_snapshot, + companion_snapshot, + (Bound::Included(point), Bound::Included(point)), + ) + .into_iter() + .next(); + + let Some(excerpt) = excerpt else { + return Point::zero()..our_snapshot.max_point(); + }; + excerpt.patch.edit_for_old_position(point).new } pub(crate) fn companion_excerpt_to_excerpt( diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4a4d6881e538a650d98cbc2b25385640b1617935..7c926cbfbb46845833688cbb854eaa1af468bb1d 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -656,36 +656,25 @@ impl BlockMap { .to_point(WrapPoint::new(edit.new.end, 0), Bias::Left); let my_start = companion - .convert_rows_from_companion( + .convert_point_from_companion( display_map_id, wrap_snapshot.buffer_snapshot(), companion_new_snapshot.buffer_snapshot(), - ( - Bound::Included(companion_start), - Bound::Included(companion_start), - ), + companion_start, ) - .first() - .and_then(|t| t.boundaries.first()) - .map(|(_, range)| range.start) - .unwrap_or(wrap_snapshot.buffer_snapshot().max_point()); + .start; let my_end = companion - .convert_rows_from_companion( + .convert_point_from_companion( display_map_id, wrap_snapshot.buffer_snapshot(), companion_new_snapshot.buffer_snapshot(), - ( - Bound::Included(companion_end), - Bound::Included(companion_end), - ), + companion_end, ) - .first() - .and_then(|t| t.boundaries.last()) - .map(|(_, range)| range.end) - .unwrap_or(wrap_snapshot.buffer_snapshot().max_point()); + .end; let mut my_start = wrap_snapshot.make_wrap_point(my_start, Bias::Left); let mut my_end = wrap_snapshot.make_wrap_point(my_end, Bias::Left); + // TODO(split-diff) should use trailing_excerpt_update_count for the second case if my_end.column() > 0 || my_end == wrap_snapshot.max_point() { *my_end.row_mut() += 1; *my_end.column_mut() = 0; @@ -1104,7 +1093,7 @@ impl BlockMap { let our_buffer = wrap_snapshot.buffer_snapshot(); let companion_buffer = companion_snapshot.buffer_snapshot(); - let row_mappings = companion.convert_rows_to_companion( + let patches = companion.convert_rows_to_companion( display_map_id, companion_buffer, our_buffer, @@ -1129,14 +1118,27 @@ impl BlockMap { let mut result = Vec::new(); - for row_mapping in row_mappings { - let mut iter = row_mapping.boundaries.iter().cloned().peekable(); + // old approach: buffer_diff computed the patch, and then passed a chosen sequence of points through it, returning the results + // new approach: buffer_diff gives us the patch directly, and then we pass through the points we are interested in + for excerpt in patches { + let mut source_points = (excerpt.edited_range.start.row..=excerpt.edited_range.end.row) + .map(|row| MultiBufferPoint::new(row, 0)) + .chain(if excerpt.edited_range.end.column > 0 { + Some(excerpt.edited_range.end) + } else { + None + }) + .peekable(); + let last_source_point = if excerpt.edited_range.end.column > 0 { + excerpt.edited_range.end + } else { + MultiBufferPoint::new(excerpt.edited_range.end.row, 0) + }; - let Some(((first_boundary, first_range), first_group)) = - iter.peek().cloned().zip(row_mapping.first_group.clone()) - else { + let Some(first_point) = source_points.peek().copied() else { continue; }; + let edit_for_first_point = excerpt.patch.edit_for_old_position(first_point); // Because we calculate spacers based on differences in wrap row // counts between the RHS and LHS for corresponding buffer points, @@ -1145,12 +1147,20 @@ impl BlockMap { // counts should have been balanced already by spacers above this // edit, so we only need to insert spacers for when the difference // in counts diverges from that baseline value. - let (our_baseline, their_baseline) = if first_group.start < first_boundary { - (first_group.start, first_range.start) - } else if let Some((prev_boundary, prev_range)) = row_mapping.prev_boundary { - (prev_boundary, prev_range.end) + let (our_baseline, their_baseline) = if edit_for_first_point.old.start < first_point { + // Case 1: We are inside a hunk/group--take the start of the hunk/group on both sides as the baseline. + ( + edit_for_first_point.old.start, + edit_for_first_point.new.start, + ) + } else if first_point.row > excerpt.source_excerpt_range.start.row { + // Case 2: We are not inside a hunk/group--go back by one row to find the baseline. + let prev_point = Point::new(first_point.row - 1, 0); + let edit_for_prev_point = excerpt.patch.edit_for_old_position(prev_point); + (prev_point, edit_for_prev_point.new.end) } else { - (first_boundary, first_range.start) + // Case 3: We are at the start of the excerpt--no previous row to use as the baseline. + (first_point, edit_for_first_point.new.start) }; let our_baseline = wrap_snapshot .make_wrap_point(our_baseline, Bias::Left) @@ -1161,14 +1171,17 @@ impl BlockMap { let mut delta = their_baseline.0 as i32 - our_baseline.0 as i32; - if first_group.start < first_boundary { - let mut current_boundary = first_boundary; - let current_range = first_range; - while let Some((next_boundary, next_range)) = iter.peek().cloned() - && next_range.end <= current_range.end - { - iter.next(); - current_boundary = next_boundary; + // If we started out in the middle of a hunk/group, work up to the end of that group to set up the main loop below. + if edit_for_first_point.old.start < first_point { + let mut current_boundary = first_point; + let current_range = edit_for_first_point.new; + while let Some(next_point) = source_points.peek().cloned() { + let edit_for_next_point = excerpt.patch.edit_for_old_position(next_point); + if edit_for_next_point.new.end > current_range.end { + break; + } + source_points.next(); + current_boundary = next_point; } let (new_delta, spacer) = @@ -1187,13 +1200,14 @@ impl BlockMap { } } - while let Some((boundary, range)) = iter.next() { - let mut current_boundary = boundary; - let current_range = range; + // Main loop: process one hunk/group at a time, possibly inserting spacers before and after. + while let Some(source_point) = source_points.next() { + let mut current_boundary = source_point; + let current_range = excerpt.patch.edit_for_old_position(current_boundary).new; // This can only occur at the end of an excerpt. if current_boundary.column > 0 { - debug_assert_eq!(current_boundary, row_mapping.source_excerpt_end); + debug_assert_eq!(current_boundary, excerpt.source_excerpt_range.end); break; } @@ -1202,9 +1216,12 @@ impl BlockMap { determine_spacer(current_boundary, current_range.start, delta); delta = delta_at_start; - while let Some((next_boundary, next_range)) = iter.peek() - && next_range.end <= current_range.end - { + while let Some(next_point) = source_points.peek().copied() { + let edit_for_next_point = excerpt.patch.edit_for_old_position(next_point); + if edit_for_next_point.new.end > current_range.end { + break; + } + if let Some((wrap_row, height)) = spacer_at_start.take() { result.push(( BlockPlacement::Above(wrap_row), @@ -1216,13 +1233,13 @@ impl BlockMap { )); } - current_boundary = *next_boundary; - iter.next(); + current_boundary = next_point; + source_points.next(); } // This can only occur at the end of an excerpt. if current_boundary.column > 0 { - debug_assert_eq!(current_boundary, row_mapping.source_excerpt_end); + debug_assert_eq!(current_boundary, excerpt.source_excerpt_range.end); break; } @@ -1254,10 +1271,9 @@ impl BlockMap { } } - let (last_boundary, _last_range) = row_mapping.boundaries.last().cloned().unwrap(); - if last_boundary == row_mapping.source_excerpt_end { + if last_source_point == excerpt.source_excerpt_range.end { let (_new_delta, spacer) = - determine_spacer(last_boundary, row_mapping.target_excerpt_end, delta); + determine_spacer(last_source_point, excerpt.target_excerpt_range.end, delta); if let Some((wrap_row, height)) = spacer { result.push(( BlockPlacement::Below(wrap_row), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7d937866b89b4b3ed3b615ae661f566777b146a5..b0dae69e82bdfae79d58817e165aad80002044b0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22742,29 +22742,24 @@ impl Editor { buffer_ranges.last() }?; - let start_row_in_buffer = text::ToPoint::to_point(&range.start, buffer).row; - let end_row_in_buffer = text::ToPoint::to_point(&range.end, buffer).row; + let buffer_range = range.to_point(buffer); let Some(buffer_diff) = multi_buffer.diff_for(buffer.remote_id()) else { - let selection = start_row_in_buffer..end_row_in_buffer; - - return Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection)); + return Some(( + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer_range.start.row..buffer_range.end.row, + )); }; let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx); - let (mut translated, _, _) = buffer_diff_snapshot.points_to_base_text_points( - [ - Point::new(start_row_in_buffer, 0), - Point::new(end_row_in_buffer, 0), - ], - buffer, - ); - let start_row = translated.next().unwrap().start.row; - let end_row = translated.next().unwrap().end.row; + let start = + buffer_diff_snapshot.buffer_point_to_base_text_point(buffer_range.start, buffer); + let end = + buffer_diff_snapshot.buffer_point_to_base_text_point(buffer_range.end, buffer); Some(( multi_buffer.buffer(buffer.remote_id()).unwrap(), - start_row..end_row, + start.row..end.row, )) }); diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 2268bc5a8062f5f9ad15f92c9f0a3f7bdea32056..f6f720f348f60bcc7da43a2fd33defe63acbaf41 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -1,4 +1,4 @@ -use std::ops::{Bound, Range}; +use std::ops::{Bound, Range, RangeInclusive}; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use collections::HashMap; @@ -6,19 +6,19 @@ use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscription, WeakEntity}; use language::{Buffer, Capability}; use multi_buffer::{ - Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, - MultiBufferPoint, MultiBufferSnapshot, PathKey, + Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferPoint, + MultiBufferSnapshot, PathKey, }; use project::Project; use rope::Point; -use text::{OffsetRangeExt as _, ToPoint as _}; +use text::{OffsetRangeExt as _, Patch, ToPoint as _}; use ui::{ App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render, Styled as _, Window, div, }; use crate::{ - display_map::MultiBufferRowMapping, + display_map::CompanionExcerptPatch, split_editor_view::{SplitEditorState, SplitEditorView}, }; use workspace::{ActivatePaneLeft, ActivatePaneRight, Item, Workspace}; @@ -35,17 +35,13 @@ pub(crate) fn convert_lhs_rows_to_rhs( rhs_snapshot: &MultiBufferSnapshot, lhs_snapshot: &MultiBufferSnapshot, lhs_bounds: (Bound, Bound), -) -> Vec { - convert_rows( +) -> Vec { + patches_for_range( lhs_excerpt_to_rhs_excerpt, lhs_snapshot, rhs_snapshot, lhs_bounds, - |diff, points, buffer| { - let (points, first_group, prev_boundary) = - diff.base_text_points_to_points(points, buffer); - (points.collect(), first_group, prev_boundary) - }, + |diff, range, buffer| diff.patch_for_base_text_range(range, buffer), ) } @@ -54,182 +50,171 @@ pub(crate) fn convert_rhs_rows_to_lhs( lhs_snapshot: &MultiBufferSnapshot, rhs_snapshot: &MultiBufferSnapshot, rhs_bounds: (Bound, Bound), -) -> Vec { - convert_rows( +) -> Vec { + patches_for_range( rhs_excerpt_to_lhs_excerpt, rhs_snapshot, lhs_snapshot, rhs_bounds, - |diff, points, buffer| { - let (points, first_group, prev_boundary) = - diff.points_to_base_text_points(points, buffer); - (points.collect(), first_group, prev_boundary) - }, + |diff, range, buffer| diff.patch_for_buffer_range(range, buffer), ) } -fn convert_rows( +fn patches_for_range( excerpt_map: &HashMap, source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_bounds: (Bound, Bound), translate_fn: F, -) -> Vec +) -> Vec where - F: Fn( - &BufferDiffSnapshot, - Vec, - &text::BufferSnapshot, - ) -> ( - Vec>, - Option>, - Option<(Point, Range)>, - ), + F: Fn(&BufferDiffSnapshot, RangeInclusive, &text::BufferSnapshot) -> Patch, { let mut result = Vec::new(); + let mut patches = HashMap::default(); - for (buffer, buffer_offset_range, source_excerpt_id) in + for (source_buffer, buffer_offset_range, source_excerpt_id) in source_snapshot.range_to_buffer_ranges(source_bounds) { - if let Some(translation) = convert_excerpt_rows( - excerpt_map, + let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap(); + let target_buffer = target_snapshot + .buffer_for_excerpt(target_excerpt_id) + .unwrap(); + let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| { + let diff = source_snapshot + .diff_for_buffer_id(source_buffer.remote_id()) + .unwrap(); + let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() { + &target_buffer + } else { + source_buffer + }; + // TODO(split-diff) pass only the union of the ranges for the affected excerpts + translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer) + }); + let buffer_point_range = buffer_offset_range.to_point(source_buffer); + + // TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt + // (less useful for project diff, but important if we want to do singleton side-by-side diff) + result.push(patch_for_excerpt( source_snapshot, target_snapshot, source_excerpt_id, - buffer, - buffer_offset_range, - &translate_fn, - ) { - result.push(translation); - } + target_excerpt_id, + source_buffer, + target_buffer, + patch, + buffer_point_range, + )); } result } -fn convert_excerpt_rows( - excerpt_map: &HashMap, +fn patch_for_excerpt( source_snapshot: &MultiBufferSnapshot, target_snapshot: &MultiBufferSnapshot, source_excerpt_id: ExcerptId, + target_excerpt_id: ExcerptId, source_buffer: &text::BufferSnapshot, - source_buffer_range: Range, - translate_fn: F, -) -> Option -where - F: Fn( - &BufferDiffSnapshot, - Vec, - &text::BufferSnapshot, - ) -> ( - Vec>, - Option>, - Option<(Point, Range)>, - ), -{ - let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied()?; - let target_buffer = target_snapshot.buffer_for_excerpt(target_excerpt_id)?; - - let diff = source_snapshot.diff_for_buffer_id(source_buffer.remote_id())?; - let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() { - &target_buffer - } else { - source_buffer - }; - - let local_start = source_buffer.offset_to_point(source_buffer_range.start.0); - let local_end = source_buffer.offset_to_point(source_buffer_range.end.0); - - let mut input_points: Vec = (local_start.row..=local_end.row) - .map(|row| Point::new(row, 0)) - .collect(); - if local_end.column > 0 { - input_points.push(local_end); - } - - let (translated_ranges, first_group, prev_boundary) = - translate_fn(&diff, input_points.clone(), rhs_buffer); - - let source_multibuffer_range = source_snapshot.range_for_excerpt(source_excerpt_id)?; + target_buffer: &text::BufferSnapshot, + patch: &Patch, + source_edited_range: Range, +) -> CompanionExcerptPatch { + let source_multibuffer_range = source_snapshot + .range_for_excerpt(source_excerpt_id) + .unwrap(); let source_excerpt_start_in_multibuffer = source_multibuffer_range.start; - let source_context_range = source_snapshot.context_range_for_excerpt(source_excerpt_id)?; + let source_context_range = source_snapshot + .context_range_for_excerpt(source_excerpt_id) + .unwrap(); let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer); let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer); - let target_multibuffer_range = target_snapshot.range_for_excerpt(target_excerpt_id)?; + let target_multibuffer_range = target_snapshot + .range_for_excerpt(target_excerpt_id) + .unwrap(); let target_excerpt_start_in_multibuffer = target_multibuffer_range.start; - let target_context_range = target_snapshot.context_range_for_excerpt(target_excerpt_id)?; + let target_context_range = target_snapshot + .context_range_for_excerpt(target_excerpt_id) + .unwrap(); let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer); let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer); - let boundaries: Vec<_> = input_points - .into_iter() - .zip(translated_ranges) - .map(|(source_buffer_point, target_range)| { - let source_multibuffer_point = source_excerpt_start_in_multibuffer - + (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point)); - - let clamped_target_start = target_range + let edits = patch + .edits() + .iter() + .skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer) + .take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer) + .map(|edit| { + let clamped_source_start = edit + .old + .start + .max(source_excerpt_start_in_buffer) + .min(source_excerpt_end_in_buffer); + let clamped_source_end = edit + .old + .end + .max(source_excerpt_start_in_buffer) + .min(source_excerpt_end_in_buffer); + let source_multibuffer_start = source_excerpt_start_in_multibuffer + + (clamped_source_start - source_excerpt_start_in_buffer); + let source_multibuffer_end = source_excerpt_start_in_multibuffer + + (clamped_source_end - source_excerpt_start_in_buffer); + let clamped_target_start = edit + .new .start .max(target_excerpt_start_in_buffer) .min(target_excerpt_end_in_buffer); - let clamped_target_end = target_range + let clamped_target_end = edit + .new .end .max(target_excerpt_start_in_buffer) .min(target_excerpt_end_in_buffer); - let target_multibuffer_start = target_excerpt_start_in_multibuffer + (clamped_target_start - target_excerpt_start_in_buffer); - let target_multibuffer_end = target_excerpt_start_in_multibuffer + (clamped_target_end - target_excerpt_start_in_buffer); + text::Edit { + old: source_multibuffer_start..source_multibuffer_end, + new: target_multibuffer_start..target_multibuffer_end, + } + }); + + let edits = [text::Edit { + old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer, + new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer, + }] + .into_iter() + .chain(edits); - ( - source_multibuffer_point, - target_multibuffer_start..target_multibuffer_end, - ) - }) - .collect(); - let first_group = first_group.map(|first_group| { - let start = source_excerpt_start_in_multibuffer - + (first_group.start - source_excerpt_start_in_buffer.min(first_group.start)); - let end = source_excerpt_start_in_multibuffer - + (first_group.end - source_excerpt_start_in_buffer.min(first_group.end)); - start..end - }); - - let prev_boundary = prev_boundary.map(|(source_buffer_point, target_range)| { - let source_multibuffer_point = source_excerpt_start_in_multibuffer - + (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point)); - - let clamped_target_start = target_range - .start - .max(target_excerpt_start_in_buffer) - .min(target_excerpt_end_in_buffer); - let clamped_target_end = target_range - .end - .max(target_excerpt_start_in_buffer) - .min(target_excerpt_end_in_buffer); - - let target_multibuffer_start = target_excerpt_start_in_multibuffer - + (clamped_target_start - target_excerpt_start_in_buffer); - let target_multibuffer_end = target_excerpt_start_in_multibuffer - + (clamped_target_end - target_excerpt_start_in_buffer); - - ( - source_multibuffer_point, - target_multibuffer_start..target_multibuffer_end, - ) - }); - - Some(MultiBufferRowMapping { - boundaries, - first_group, - prev_boundary, - source_excerpt_end: source_excerpt_start_in_multibuffer - + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer), - target_excerpt_end: target_excerpt_start_in_multibuffer - + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer), - }) + let mut merged_edits: Vec> = Vec::new(); + for edit in edits { + if let Some(last) = merged_edits.last_mut() { + if edit.new.start <= last.new.end { + last.old.end = last.old.end.max(edit.old.end); + last.new.end = last.new.end.max(edit.new.end); + continue; + } + } + merged_edits.push(edit); + } + + let edited_range = source_excerpt_start_in_multibuffer + + (source_edited_range.start - source_excerpt_start_in_buffer) + ..source_excerpt_start_in_multibuffer + + (source_edited_range.end - source_excerpt_start_in_buffer); + + let source_excerpt_end = source_excerpt_start_in_multibuffer + + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer); + let target_excerpt_end = target_excerpt_start_in_multibuffer + + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer); + + CompanionExcerptPatch { + patch: Patch::new(merged_edits), + edited_range, + source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end, + target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end, + } } pub struct SplitDiffFeatureFlag; @@ -623,24 +608,16 @@ impl SplittableEditor { let source_snapshot = source_multibuffer.read(cx).snapshot(cx); let target_snapshot = target_multibuffer.read(cx).snapshot(cx); - let target_point = target_editor.update(cx, |target_editor, cx| { + let target_range = target_editor.update(cx, |target_editor, cx| { target_editor.display_map.update(cx, |display_map, cx| { let display_map_id = cx.entity_id(); display_map.companion().unwrap().update(cx, |companion, _| { - companion - .convert_rows_from_companion( - display_map_id, - &target_snapshot, - &source_snapshot, - (Bound::Included(source_point), Bound::Included(source_point)), - ) - .first() - .unwrap() - .boundaries - .first() - .unwrap() - .1 - .start + companion.convert_point_from_companion( + display_map_id, + &target_snapshot, + &source_snapshot, + source_point, + ) }) }) }); @@ -648,7 +625,7 @@ impl SplittableEditor { target_editor.update(cx, |editor, cx| { editor.set_suppress_selection_callback(true); editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([target_point..target_point]); + s.select_ranges([target_range]); }); editor.set_suppress_selection_callback(false); }); @@ -1501,14 +1478,17 @@ impl LhsEditor { .into_iter() .map(|(_, excerpt_range)| { let point_range_to_base_text_point_range = |range: Range| { - let (mut translated, _, _) = diff_snapshot.points_to_base_text_points( - [Point::new(range.start.row, 0), Point::new(range.end.row, 0)], - main_buffer, - ); - let start_row = translated.next().unwrap().start.row; - let end_row = translated.next().unwrap().end.row; - let end_column = diff_snapshot.base_text().line_len(end_row); - Point::new(start_row, 0)..Point::new(end_row, end_column) + let start = diff_snapshot + .buffer_point_to_base_text_range( + Point::new(range.start.row, 0), + main_buffer, + ) + .start; + let end = diff_snapshot + .buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer) + .end; + let end_column = diff_snapshot.base_text().line_len(end.row); + Point::new(start.row, 0)..Point::new(end.row, end_column) }; let rhs = excerpt_range.primary.to_point(main_buffer); let context = excerpt_range.context.to_point(main_buffer); diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index ec495f60fd78bdd3618a8b44ffb59e833439f629..c9f353a9cce22b54ecf7e2fa872035cd486438e6 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -225,6 +225,46 @@ where old } } + + /// Returns the edit that touches the given old position. + /// + /// An edit is considered to touch the given old position if edit.old.start <= old <= edit.old.end (note, inclusive on the right). + /// + /// If there are no edits touching the given old position, an empty edit with appropriate (empty) old and new ranges is returned. + pub fn edit_for_old_position(&self, old: T) -> Edit { + let edits = self.edits(); + + let ix = match edits.binary_search_by(|probe| probe.old.start.cmp(&old)) { + Ok(ix) => ix, + Err(ix) => { + if ix == 0 { + return Edit { + old: old..old, + new: old..old, + }; + } else { + ix - 1 + } + } + }; + + if let Some(edit) = edits.get(ix) { + if old > edit.old.end { + let translated = edit.new.end + (old - edit.old.end); + Edit { + new: translated..translated, + old: old..old, + } + } else { + edit.clone() + } + } else { + Edit { + old: old..old, + new: old..old, + } + } + } } impl Patch {