use std::ops::Range; use git::diff::{DiffHunk, DiffHunkStatus}; use language::Point; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, AnchorRangeExt, }; #[derive(Debug, Clone, PartialEq, Eq)] pub enum DisplayDiffHunk { Folded { display_row: u32, }, Unfolded { display_row_range: Range, status: DiffHunkStatus, }, } impl DisplayDiffHunk { pub fn start_display_row(&self) -> u32 { match self { &DisplayDiffHunk::Folded { display_row } => display_row, DisplayDiffHunk::Unfolded { display_row_range, .. } => display_row_range.start, } } pub fn contains_display_row(&self, display_row: u32) -> bool { let range = match self { &DisplayDiffHunk::Folded { display_row } => display_row..=display_row, DisplayDiffHunk::Unfolded { display_row_range, .. } => display_row_range.start..=display_row_range.end, }; range.contains(&display_row) } } pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> DisplayDiffHunk { let hunk_start_point = Point::new(hunk.buffer_range.start, 0); let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0); let hunk_end_point_sub = Point::new( hunk.buffer_range .end .saturating_sub(1) .max(hunk.buffer_range.start), 0, ); let is_removal = hunk.status() == DiffHunkStatus::Removed; let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0); let folds_end = Point::new(hunk.buffer_range.end + 2, 0); let folds_range = folds_start..folds_end; let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot); let fold_point_range = fold_point_range.start..=fold_point_range.end; let folded_start = fold_point_range.contains(&hunk_start_point); let folded_end = fold_point_range.contains(&hunk_end_point_sub); let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub); (folded_start && folded_end) || (is_removal && folded_start_sub) }); if let Some(fold) = containing_fold { let row = fold.range.start.to_display_point(snapshot).row(); DisplayDiffHunk::Folded { display_row: row } } else { let start = hunk_start_point.to_display_point(snapshot).row(); let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start); let hunk_end_point = Point::new(hunk_end_row, 0); let end = hunk_end_point.to_display_point(snapshot).row(); DisplayDiffHunk::Unfolded { display_row_range: start..end, status: hunk.status(), } } } #[cfg(test)] mod tests { use crate::editor_tests::init_test; use crate::Point; use gpui::{Context, TestAppContext}; use language::Capability::ReadWrite; use multi_buffer::{ExcerptRange, MultiBuffer}; use project::{FakeFs, Project}; use unindent::Unindent; #[gpui::test] async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { use git::diff::DiffHunkStatus; init_test(cx, |_| {}); let fs = FakeFs::new(cx.background_executor.clone()); let project = Project::test(fs, [], cx).await; // buffer has two modified hunks with two rows each let buffer_1 = project .update(cx, |project, cx| { project.create_buffer( " 1.zero 1.ONE 1.TWO 1.three 1.FOUR 1.FIVE 1.six " .unindent() .as_str(), None, cx, ) }) .unwrap(); buffer_1.update(cx, |buffer, cx| { buffer.set_diff_base( Some( " 1.zero 1.one 1.two 1.three 1.four 1.five 1.six " .unindent(), ), cx, ); }); // buffer has a deletion hunk and an insertion hunk let buffer_2 = project .update(cx, |project, cx| { project.create_buffer( " 2.zero 2.one 2.two 2.three 2.four 2.five 2.six " .unindent() .as_str(), None, cx, ) }) .unwrap(); buffer_2.update(cx, |buffer, 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, ); }); cx.background_executor.run_until_parked(); let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0, ReadWrite); 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) .map(|hunk| (hunk.status(), hunk.buffer_range)) .collect::>(), &expected, ); assert_eq!( snapshot .git_diff_hunks_in_range_rev(0..12) .map(|hunk| (hunk.status(), hunk.buffer_range)) .collect::>(), expected .iter() .rev() .cloned() .collect::>() .as_slice(), ); } }