git.rs

  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}