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