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