git.rs

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