diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 0c26486667..7dc2b3af18 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -1817,7 +1817,7 @@ async fn test_git_diff_base_change( buffer_local_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -1837,7 +1837,7 @@ async fn test_git_diff_base_change( buffer_remote_a.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -1861,7 +1861,7 @@ async fn test_git_diff_base_change( assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -1872,7 +1872,7 @@ async fn test_git_diff_base_change( buffer_remote_a.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -1915,7 +1915,7 @@ async fn test_git_diff_base_change( buffer_local_b.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -1935,7 +1935,7 @@ async fn test_git_diff_base_change( buffer_remote_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -1963,12 +1963,12 @@ async fn test_git_diff_base_change( "{:?}", buffer .snapshot() - .git_diff_hunks_in_range(0..4, false) + .git_diff_hunks_in_row_range(0..4, false) .collect::>() ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -1979,7 +1979,7 @@ async fn test_git_diff_base_change( buffer_remote_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), &buffer, &diff_base, &[(2..3, "", "three\n")], diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 89f5bb54a9..e98aa645d5 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -575,6 +575,15 @@ impl Item for ProjectDiagnosticsEditor { unreachable!() } + fn git_diff_recalc( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.editor + .update(cx, |editor, cx| editor.git_diff_recalc(project, cx)) + } + fn to_item_events(event: &Self::Event) -> Vec { Editor::to_item_events(event) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index d0dd34a931..0f6e357ddd 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2710,11 +2710,73 @@ impl MultiBufferSnapshot { row_range: Range, reversed: bool, ) -> impl 'a + Iterator> { - self.as_singleton() - .into_iter() - .flat_map(move |(_, _, buffer)| { - buffer.git_diff_hunks_in_range(row_range.clone(), reversed) - }) + let mut cursor = self.excerpts.cursor::(); + + if reversed { + cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + } else { + cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &()); + } + + std::iter::from_fn(move || { + let excerpt = cursor.item()?; + let multibuffer_start = *cursor.start(); + let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; + if multibuffer_start.row >= row_range.end { + return None; + } + + let mut buffer_start = excerpt.range.context.start; + let mut buffer_end = excerpt.range.context.end; + let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); + let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; + + if row_range.start > multibuffer_start.row { + let buffer_start_point = + excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0); + buffer_start = excerpt.buffer.anchor_before(buffer_start_point); + } + + if row_range.end < multibuffer_end.row { + let buffer_end_point = + excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0); + buffer_end = excerpt.buffer.anchor_before(buffer_end_point); + } + + let buffer_hunks = excerpt + .buffer + .git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed) + .filter_map(move |hunk| { + let start = multibuffer_start.row + + hunk + .buffer_range + .start + .saturating_sub(excerpt_start_point.row); + let end = multibuffer_start.row + + hunk + .buffer_range + .end + .min(excerpt_end_point.row + 1) + .saturating_sub(excerpt_start_point.row); + + Some(DiffHunk { + buffer_range: start..end, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }); + + if reversed { + cursor.prev(&()); + } else { + cursor.next(&()); + } + + Some(buffer_hunks) + }) + .flatten() } pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { @@ -3546,11 +3608,12 @@ impl ToPointUtf16 for PointUtf16 { #[cfg(test)] mod tests { use super::*; - use gpui::MutableAppContext; + use gpui::{MutableAppContext, TestAppContext}; use language::{Buffer, Rope}; use rand::prelude::*; use settings::Settings; use std::{env, rc::Rc}; + use unindent::Unindent; use util::test::sample_text; @@ -4168,6 +4231,178 @@ mod tests { ); } + #[gpui::test] + async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { + use git::diff::DiffHunkStatus; + + // buffer has two modified hunks with two rows each + let buffer_1 = cx.add_model(|cx| { + let mut buffer = Buffer::new( + 0, + " + 1.zero + 1.ONE + 1.TWO + 1.three + 1.FOUR + 1.FIVE + 1.six + " + .unindent(), + cx, + ); + buffer.set_diff_base( + Some( + " + 1.zero + 1.one + 1.two + 1.three + 1.four + 1.five + 1.six + " + .unindent(), + ), + cx, + ); + buffer + }); + + // buffer has a deletion hunk and an insertion hunk + let buffer_2 = cx.add_model(|cx| { + let mut buffer = Buffer::new( + 0, + " + 2.zero + 2.one + 2.two + 2.three + 2.four + 2.five + 2.six + " + .unindent(), + cx, + ); + buffer.set_diff_base( + Some( + " + 2.zero + 2.one + 2.one-and-a-half + 2.two + 2.three + 2.four + 2.six + " + .unindent(), + ), + cx, + ); + buffer + }); + + cx.foreground().run_until_parked(); + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // excerpt ends in the middle of a modified hunk + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt begins in the middle of a modified hunk + ExcerptRange { + context: Point::new(5, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + // excerpt ends at a deletion + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt starts at a deletion + ExcerptRange { + context: Point::new(2, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains a deletion hunk + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains an insertion hunk + ExcerptRange { + context: Point::new(4, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer + }); + + let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); + + assert_eq!( + snapshot.text(), + " + 1.zero + 1.ONE + 1.FIVE + 1.six + 2.zero + 2.one + 2.two + 2.one + 2.two + 2.four + 2.five + 2.six" + .unindent() + ); + + let expected = [ + (DiffHunkStatus::Modified, 1..2), + (DiffHunkStatus::Modified, 2..3), + //TODO: Define better when and where removed hunks show up at range extremities + (DiffHunkStatus::Removed, 6..6), + (DiffHunkStatus::Removed, 8..8), + (DiffHunkStatus::Added, 10..11), + ]; + + assert_eq!( + snapshot + .git_diff_hunks_in_range(0..12, false) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + &expected, + ); + + assert_eq!( + snapshot + .git_diff_hunks_in_range(0..12, true) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + expected + .iter() + .rev() + .cloned() + .collect::>() + .as_slice(), + ); + } + #[gpui::test(iterations = 100)] fn test_random_multibuffer(cx: &mut MutableAppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 74b6bdd416..568f29d3e1 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -254,7 +254,7 @@ impl<'a> EditorTestContext<'a> { Actual selections: {} - "}, + "}, self.assertion_context(), expected_marked_text, actual_marked_text, diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index e808eee24f..b28af26f16 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -71,18 +71,26 @@ impl BufferDiff { } } - pub fn hunks_in_range<'a>( + pub fn hunks_in_row_range<'a>( &'a self, - query_row_range: Range, + range: Range, buffer: &'a BufferSnapshot, reversed: bool, ) -> impl 'a + Iterator> { - let start = buffer.anchor_before(Point::new(query_row_range.start, 0)); - let end = buffer.anchor_after(Point::new(query_row_range.end, 0)); + let start = buffer.anchor_before(Point::new(range.start, 0)); + let end = buffer.anchor_after(Point::new(range.end, 0)); + self.hunks_intersecting_range(start..end, buffer, reversed) + } + pub fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + reversed: bool, + ) -> impl 'a + Iterator> { let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { - let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt(); - let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt(); + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); !before_start && !after_end }); @@ -141,7 +149,9 @@ impl BufferDiff { #[cfg(test)] fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { - self.hunks_in_range(0..u32::MAX, text, false) + let start = text.anchor_before(Point::new(0, 0)); + let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); + self.hunks_intersecting_range(start..end, text, false) } fn diff<'a>(head: &'a str, current: &'a str) -> Option> { @@ -355,7 +365,7 @@ mod tests { assert_eq!(diff.hunks(&buffer).count(), 8); assert_hunks( - diff.hunks_in_range(7..12, &buffer, false), + diff.hunks_in_row_range(7..12, &buffer, false), &buffer, &diff_base, &[ diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e8bc2bf314..a78bb4af79 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2310,13 +2310,21 @@ impl BufferSnapshot { }) } - pub fn git_diff_hunks_in_range<'a>( + pub fn git_diff_hunks_in_row_range<'a>( &'a self, - query_row_range: Range, + range: Range, + reversed: bool, + ) -> impl 'a + Iterator> { + self.git_diff.hunks_in_row_range(range, self, reversed) + } + + pub fn git_diff_hunks_intersecting_range<'a>( + &'a self, + range: Range, reversed: bool, ) -> impl 'a + Iterator> { self.git_diff - .hunks_in_range(query_row_range, self, reversed) + .hunks_intersecting_range(range, self, reversed) } pub fn diagnostics_in_range<'a, T, O>( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1659ddd451..9d2f31e269 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -334,6 +334,15 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.navigate(data, cx)) } + fn git_diff_recalc( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.git_diff_recalc(project, cx)) + } + fn to_item_events(event: &Self::Event) -> Vec { match event { ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab], diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 52200d64cf..88412f6059 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -597,6 +597,10 @@ where self.cursor.item() } + pub fn item_summary(&self) -> Option<&'a T::Summary> { + self.cursor.item_summary() + } + pub fn next(&mut self, cx: &::Context) { self.cursor.next_internal(&mut self.filter_node, cx); }