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