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 language::Capability::ReadWrite;
97 use multi_buffer::{ExcerptRange, MultiBuffer};
98 use project::{FakeFs, Project};
99 use unindent::Unindent;
100 #[gpui::test]
101 async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
102 use git::diff::DiffHunkStatus;
103 init_test(cx, |_| {});
104
105 let fs = FakeFs::new(cx.background_executor.clone());
106 let project = Project::test(fs, [], cx).await;
107
108 // buffer has two modified hunks with two rows each
109 let buffer_1 = project
110 .update(cx, |project, cx| {
111 project.create_buffer(
112 "
113 1.zero
114 1.ONE
115 1.TWO
116 1.three
117 1.FOUR
118 1.FIVE
119 1.six
120 "
121 .unindent()
122 .as_str(),
123 None,
124 cx,
125 )
126 })
127 .unwrap();
128 buffer_1.update(cx, |buffer, cx| {
129 buffer.set_diff_base(
130 Some(
131 "
132 1.zero
133 1.one
134 1.two
135 1.three
136 1.four
137 1.five
138 1.six
139 "
140 .unindent(),
141 ),
142 cx,
143 );
144 });
145
146 // buffer has a deletion hunk and an insertion hunk
147 let buffer_2 = project
148 .update(cx, |project, cx| {
149 project.create_buffer(
150 "
151 2.zero
152 2.one
153 2.two
154 2.three
155 2.four
156 2.five
157 2.six
158 "
159 .unindent()
160 .as_str(),
161 None,
162 cx,
163 )
164 })
165 .unwrap();
166 buffer_2.update(cx, |buffer, cx| {
167 buffer.set_diff_base(
168 Some(
169 "
170 2.zero
171 2.one
172 2.one-and-a-half
173 2.two
174 2.three
175 2.four
176 2.six
177 "
178 .unindent(),
179 ),
180 cx,
181 );
182 });
183
184 cx.background_executor.run_until_parked();
185
186 let multibuffer = cx.new_model(|cx| {
187 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
188 multibuffer.push_excerpts(
189 buffer_1.clone(),
190 [
191 // excerpt ends in the middle of a modified hunk
192 ExcerptRange {
193 context: Point::new(0, 0)..Point::new(1, 5),
194 primary: Default::default(),
195 },
196 // excerpt begins in the middle of a modified hunk
197 ExcerptRange {
198 context: Point::new(5, 0)..Point::new(6, 5),
199 primary: Default::default(),
200 },
201 ],
202 cx,
203 );
204 multibuffer.push_excerpts(
205 buffer_2.clone(),
206 [
207 // excerpt ends at a deletion
208 ExcerptRange {
209 context: Point::new(0, 0)..Point::new(1, 5),
210 primary: Default::default(),
211 },
212 // excerpt starts at a deletion
213 ExcerptRange {
214 context: Point::new(2, 0)..Point::new(2, 5),
215 primary: Default::default(),
216 },
217 // excerpt fully contains a deletion hunk
218 ExcerptRange {
219 context: Point::new(1, 0)..Point::new(2, 5),
220 primary: Default::default(),
221 },
222 // excerpt fully contains an insertion hunk
223 ExcerptRange {
224 context: Point::new(4, 0)..Point::new(6, 5),
225 primary: Default::default(),
226 },
227 ],
228 cx,
229 );
230 multibuffer
231 });
232
233 let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
234
235 assert_eq!(
236 snapshot.text(),
237 "
238 1.zero
239 1.ONE
240 1.FIVE
241 1.six
242 2.zero
243 2.one
244 2.two
245 2.one
246 2.two
247 2.four
248 2.five
249 2.six"
250 .unindent()
251 );
252
253 let expected = [
254 (DiffHunkStatus::Modified, 1..2),
255 (DiffHunkStatus::Modified, 2..3),
256 //TODO: Define better when and where removed hunks show up at range extremities
257 (DiffHunkStatus::Removed, 6..6),
258 (DiffHunkStatus::Removed, 8..8),
259 (DiffHunkStatus::Added, 10..11),
260 ];
261
262 assert_eq!(
263 snapshot
264 .git_diff_hunks_in_range(0..12)
265 .map(|hunk| (hunk.status(), hunk.buffer_range))
266 .collect::<Vec<_>>(),
267 &expected,
268 );
269
270 assert_eq!(
271 snapshot
272 .git_diff_hunks_in_range_rev(0..12)
273 .map(|hunk| (hunk.status(), hunk.buffer_range))
274 .collect::<Vec<_>>(),
275 expected
276 .iter()
277 .rev()
278 .cloned()
279 .collect::<Vec<_>>()
280 .as_slice(),
281 );
282 }
283}