1use std::ops::Range;
2
3use git::diff::{DiffHunk, DiffHunkStatus};
4use language::Point;
5
6use crate::{
7 display_map::{DisplaySnapshot, ToDisplayPoint},
8 AnchorRangeExt,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum DisplayDiffHunk {
13 Folded {
14 display_row: u32,
15 },
16
17 Unfolded {
18 display_row_range: Range<u32>,
19 status: DiffHunkStatus,
20 },
21}
22
23impl DisplayDiffHunk {
24 pub fn start_display_row(&self) -> u32 {
25 match self {
26 &DisplayDiffHunk::Folded { display_row } => display_row,
27 DisplayDiffHunk::Unfolded {
28 display_row_range, ..
29 } => display_row_range.start,
30 }
31 }
32
33 pub fn contains_display_row(&self, display_row: u32) -> bool {
34 let range = match self {
35 &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
36
37 DisplayDiffHunk::Unfolded {
38 display_row_range, ..
39 } => display_row_range.start..=display_row_range.end,
40 };
41
42 range.contains(&display_row)
43 }
44}
45
46pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
47 let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
48 let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
49 let hunk_end_point_sub = Point::new(
50 hunk.buffer_range
51 .end
52 .saturating_sub(1)
53 .max(hunk.buffer_range.start),
54 0,
55 );
56
57 let is_removal = hunk.status() == DiffHunkStatus::Removed;
58
59 let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
60 let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
61 let folds_range = folds_start..folds_end;
62
63 let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
64 let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
65 let fold_point_range = fold_point_range.start..=fold_point_range.end;
66
67 let folded_start = fold_point_range.contains(&hunk_start_point);
68 let folded_end = fold_point_range.contains(&hunk_end_point_sub);
69 let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
70
71 (folded_start && folded_end) || (is_removal && folded_start_sub)
72 });
73
74 if let Some(fold) = containing_fold {
75 let row = fold.range.start.to_display_point(snapshot).row();
76 DisplayDiffHunk::Folded { display_row: row }
77 } else {
78 let start = hunk_start_point.to_display_point(snapshot).row();
79
80 let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
81 let hunk_end_point = Point::new(hunk_end_row, 0);
82 let end = hunk_end_point.to_display_point(snapshot).row();
83
84 DisplayDiffHunk::Unfolded {
85 display_row_range: start..end,
86 status: hunk.status(),
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use crate::editor_tests::init_test;
94 use crate::Point;
95 use gpui::{Context, TestAppContext};
96 use multi_buffer::{ExcerptRange, MultiBuffer};
97 use project::{FakeFs, Project};
98 use unindent::Unindent;
99 #[gpui::test]
100 async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
101 use git::diff::DiffHunkStatus;
102 init_test(cx, |_| {});
103
104 let fs = FakeFs::new(cx.background_executor.clone());
105 let project = Project::test(fs, [], cx).await;
106
107 // buffer has two modified hunks with two rows each
108 let buffer_1 = project
109 .update(cx, |project, cx| {
110 project.create_buffer(
111 "
112 1.zero
113 1.ONE
114 1.TWO
115 1.three
116 1.FOUR
117 1.FIVE
118 1.six
119 "
120 .unindent()
121 .as_str(),
122 None,
123 cx,
124 )
125 })
126 .unwrap();
127 buffer_1.update(cx, |buffer, cx| {
128 buffer.set_diff_base(
129 Some(
130 "
131 1.zero
132 1.one
133 1.two
134 1.three
135 1.four
136 1.five
137 1.six
138 "
139 .unindent(),
140 ),
141 cx,
142 );
143 });
144
145 // buffer has a deletion hunk and an insertion hunk
146 let buffer_2 = project
147 .update(cx, |project, cx| {
148 project.create_buffer(
149 "
150 2.zero
151 2.one
152 2.two
153 2.three
154 2.four
155 2.five
156 2.six
157 "
158 .unindent()
159 .as_str(),
160 None,
161 cx,
162 )
163 })
164 .unwrap();
165 buffer_2.update(cx, |buffer, cx| {
166 buffer.set_diff_base(
167 Some(
168 "
169 2.zero
170 2.one
171 2.one-and-a-half
172 2.two
173 2.three
174 2.four
175 2.six
176 "
177 .unindent(),
178 ),
179 cx,
180 );
181 });
182
183 cx.background_executor.run_until_parked();
184
185 let multibuffer = cx.build_model(|cx| {
186 let mut multibuffer = MultiBuffer::new(0);
187 multibuffer.push_excerpts(
188 buffer_1.clone(),
189 [
190 // excerpt ends in the middle of a modified hunk
191 ExcerptRange {
192 context: Point::new(0, 0)..Point::new(1, 5),
193 primary: Default::default(),
194 },
195 // excerpt begins in the middle of a modified hunk
196 ExcerptRange {
197 context: Point::new(5, 0)..Point::new(6, 5),
198 primary: Default::default(),
199 },
200 ],
201 cx,
202 );
203 multibuffer.push_excerpts(
204 buffer_2.clone(),
205 [
206 // excerpt ends at a deletion
207 ExcerptRange {
208 context: Point::new(0, 0)..Point::new(1, 5),
209 primary: Default::default(),
210 },
211 // excerpt starts at a deletion
212 ExcerptRange {
213 context: Point::new(2, 0)..Point::new(2, 5),
214 primary: Default::default(),
215 },
216 // excerpt fully contains a deletion hunk
217 ExcerptRange {
218 context: Point::new(1, 0)..Point::new(2, 5),
219 primary: Default::default(),
220 },
221 // excerpt fully contains an insertion hunk
222 ExcerptRange {
223 context: Point::new(4, 0)..Point::new(6, 5),
224 primary: Default::default(),
225 },
226 ],
227 cx,
228 );
229 multibuffer
230 });
231
232 let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
233
234 assert_eq!(
235 snapshot.text(),
236 "
237 1.zero
238 1.ONE
239 1.FIVE
240 1.six
241 2.zero
242 2.one
243 2.two
244 2.one
245 2.two
246 2.four
247 2.five
248 2.six"
249 .unindent()
250 );
251
252 let expected = [
253 (DiffHunkStatus::Modified, 1..2),
254 (DiffHunkStatus::Modified, 2..3),
255 //TODO: Define better when and where removed hunks show up at range extremities
256 (DiffHunkStatus::Removed, 6..6),
257 (DiffHunkStatus::Removed, 8..8),
258 (DiffHunkStatus::Added, 10..11),
259 ];
260
261 assert_eq!(
262 snapshot
263 .git_diff_hunks_in_range(0..12)
264 .map(|hunk| (hunk.status(), hunk.buffer_range))
265 .collect::<Vec<_>>(),
266 &expected,
267 );
268
269 assert_eq!(
270 snapshot
271 .git_diff_hunks_in_range_rev(0..12)
272 .map(|hunk| (hunk.status(), hunk.buffer_range))
273 .collect::<Vec<_>>(),
274 expected
275 .iter()
276 .rev()
277 .cloned()
278 .collect::<Vec<_>>()
279 .as_slice(),
280 );
281 }
282}