git.rs

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