pub mod blame; use std::ops::Range; use git::diff::{DiffHunk, DiffHunkStatus}; use language::Point; use multi_buffer::{Anchor, MultiBufferRow}; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, hunk_status, AnchorRangeExt, DisplayRow, }; #[derive(Debug, Clone, PartialEq, Eq)] pub enum DisplayDiffHunk { Folded { display_row: DisplayRow, }, Unfolded { diff_base_byte_range: Range, display_row_range: Range, multi_buffer_range: Range, status: DiffHunkStatus, }, } impl DisplayDiffHunk { pub fn start_display_row(&self) -> DisplayRow { 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: DisplayRow) -> 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.associated_range.start.0, 0); let hunk_start_point_sub = Point::new(hunk.associated_range.start.0.saturating_sub(1), 0); let hunk_end_point_sub = Point::new( hunk.associated_range .end .0 .saturating_sub(1) .max(hunk.associated_range.start.0), 0, ); let status = hunk_status(hunk); let is_removal = status == DiffHunkStatus::Removed; let folds_start = Point::new(hunk.associated_range.start.0.saturating_sub(2), 0); let folds_end = Point::new(hunk.associated_range.end.0 + 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.associated_range.end.max(hunk.associated_range.start); let hunk_end_point = Point::new(hunk_end_row.0, 0); let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point); let multi_buffer_end = snapshot.buffer_snapshot.anchor_before(hunk_end_point); let end = hunk_end_point.to_display_point(snapshot).row(); DisplayDiffHunk::Unfolded { display_row_range: start..end, multi_buffer_range: multi_buffer_start..multi_buffer_end, status, diff_base_byte_range: hunk.diff_base_byte_range.clone(), } } } #[cfg(test)] mod tests { use crate::Point; use crate::{editor_tests::init_test, hunk_status}; use gpui::{Context, TestAppContext}; use language::Capability::ReadWrite; use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow}; 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_local_buffer( " 1.zero 1.ONE 1.TWO 1.three 1.FOUR 1.FIVE 1.six " .unindent() .as_str(), None, cx, ) }); 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_local_buffer( " 2.zero 2.one 2.two 2.three 2.four 2.five 2.six " .unindent() .as_str(), None, cx, ) }); 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, MultiBufferRow(1)..MultiBufferRow(2), ), ( DiffHunkStatus::Modified, MultiBufferRow(2)..MultiBufferRow(3), ), //TODO: Define better when and where removed hunks show up at range extremities ( DiffHunkStatus::Removed, MultiBufferRow(6)..MultiBufferRow(6), ), ( DiffHunkStatus::Removed, MultiBufferRow(8)..MultiBufferRow(8), ), ( DiffHunkStatus::Added, MultiBufferRow(10)..MultiBufferRow(11), ), ]; assert_eq!( snapshot .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12)) .map(|hunk| (hunk_status(&hunk), hunk.associated_range)) .collect::>(), &expected, ); assert_eq!( snapshot .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12)) .map(|hunk| (hunk_status(&hunk), hunk.associated_range)) .collect::>(), expected .iter() .rev() .cloned() .collect::>() .as_slice(), ); } }