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 ),
150 cx,
151 );
152 });
153
154 // buffer has a deletion hunk and an insertion hunk
155 let buffer_2 = project.update(cx, |project, cx| {
156 project.create_local_buffer(
157 "
158 2.zero
159 2.one
160 2.two
161 2.three
162 2.four
163 2.five
164 2.six
165 "
166 .unindent()
167 .as_str(),
168 None,
169 cx,
170 )
171 });
172 buffer_2.update(cx, |buffer, cx| {
173 buffer.set_diff_base(
174 Some(
175 "
176 2.zero
177 2.one
178 2.one-and-a-half
179 2.two
180 2.three
181 2.four
182 2.six
183 "
184 .unindent(),
185 ),
186 cx,
187 );
188 });
189
190 cx.background_executor.run_until_parked();
191
192 let multibuffer = cx.new_model(|cx| {
193 let mut multibuffer = MultiBuffer::new(0, ReadWrite);
194 multibuffer.push_excerpts(
195 buffer_1.clone(),
196 [
197 // excerpt ends in the middle of a modified hunk
198 ExcerptRange {
199 context: Point::new(0, 0)..Point::new(1, 5),
200 primary: Default::default(),
201 },
202 // excerpt begins in the middle of a modified hunk
203 ExcerptRange {
204 context: Point::new(5, 0)..Point::new(6, 5),
205 primary: Default::default(),
206 },
207 ],
208 cx,
209 );
210 multibuffer.push_excerpts(
211 buffer_2.clone(),
212 [
213 // excerpt ends at a deletion
214 ExcerptRange {
215 context: Point::new(0, 0)..Point::new(1, 5),
216 primary: Default::default(),
217 },
218 // excerpt starts at a deletion
219 ExcerptRange {
220 context: Point::new(2, 0)..Point::new(2, 5),
221 primary: Default::default(),
222 },
223 // excerpt fully contains a deletion hunk
224 ExcerptRange {
225 context: Point::new(1, 0)..Point::new(2, 5),
226 primary: Default::default(),
227 },
228 // excerpt fully contains an insertion hunk
229 ExcerptRange {
230 context: Point::new(4, 0)..Point::new(6, 5),
231 primary: Default::default(),
232 },
233 ],
234 cx,
235 );
236 multibuffer
237 });
238
239 let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
240
241 assert_eq!(
242 snapshot.text(),
243 "
244 1.zero
245 1.ONE
246 1.FIVE
247 1.six
248 2.zero
249 2.one
250 2.two
251 2.one
252 2.two
253 2.four
254 2.five
255 2.six"
256 .unindent()
257 );
258
259 let expected = [
260 (DiffHunkStatus::Modified, 1..2),
261 (DiffHunkStatus::Modified, 2..3),
262 //TODO: Define better when and where removed hunks show up at range extremities
263 (DiffHunkStatus::Removed, 6..6),
264 (DiffHunkStatus::Removed, 8..8),
265 (DiffHunkStatus::Added, 10..11),
266 ];
267
268 assert_eq!(
269 snapshot
270 .git_diff_hunks_in_range(0..12)
271 .map(|hunk| (hunk.status(), hunk.associated_range))
272 .collect::<Vec<_>>(),
273 &expected,
274 );
275
276 assert_eq!(
277 snapshot
278 .git_diff_hunks_in_range_rev(0..12)
279 .map(|hunk| (hunk.status(), hunk.associated_range))
280 .collect::<Vec<_>>(),
281 expected
282 .iter()
283 .rev()
284 .cloned()
285 .collect::<Vec<_>>()
286 .as_slice(),
287 );
288 }
289}