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