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