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
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}