pub mod permalink; 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<u32>, 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<u32>, 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::<Vec<_>>(), &expected, ); assert_eq!( snapshot .git_diff_hunks_in_range_rev(0..12) .map(|hunk| (hunk.status(), hunk.buffer_range)) .collect::<Vec<_>>(), expected .iter() .rev() .cloned() .collect::<Vec<_>>() .as_slice(), ); }}
pub mod permalink;
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<u32>,
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<u32>, 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::<Vec<_>>(),
&expected,
);
assert_eq!(
snapshot
.git_diff_hunks_in_range_rev(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
.as_slice(),
);
}
}