Merge pull request #1983 from zed-industries/multi-buffer-git-gutter

Julia created

Multi buffer git gutter

Change summary

crates/collab/src/integration_tests.rs        |  18 
crates/diagnostics/src/diagnostics.rs         |   9 
crates/editor/src/multi_buffer.rs             | 247 ++++++++++++++++++++
crates/editor/src/test/editor_test_context.rs |   2 
crates/git/src/diff.rs                        |  26 +
crates/language/src/buffer.rs                 |  14 
crates/search/src/project_search.rs           |   9 
crates/sum_tree/src/cursor.rs                 |   4 
8 files changed, 302 insertions(+), 27 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -1817,7 +1817,7 @@ async fn test_git_diff_base_change(
     buffer_local_a.read_with(cx_a, |buffer, _| {
         assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(1..2, "", "two\n")],
@@ -1837,7 +1837,7 @@ async fn test_git_diff_base_change(
     buffer_remote_a.read_with(cx_b, |buffer, _| {
         assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(1..2, "", "two\n")],
@@ -1861,7 +1861,7 @@ async fn test_git_diff_base_change(
         assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
 
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(2..3, "", "three\n")],
@@ -1872,7 +1872,7 @@ async fn test_git_diff_base_change(
     buffer_remote_a.read_with(cx_b, |buffer, _| {
         assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(2..3, "", "three\n")],
@@ -1915,7 +1915,7 @@ async fn test_git_diff_base_change(
     buffer_local_b.read_with(cx_a, |buffer, _| {
         assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(1..2, "", "two\n")],
@@ -1935,7 +1935,7 @@ async fn test_git_diff_base_change(
     buffer_remote_b.read_with(cx_b, |buffer, _| {
         assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(1..2, "", "two\n")],
@@ -1963,12 +1963,12 @@ async fn test_git_diff_base_change(
             "{:?}",
             buffer
                 .snapshot()
-                .git_diff_hunks_in_range(0..4, false)
+                .git_diff_hunks_in_row_range(0..4, false)
                 .collect::<Vec<_>>()
         );
 
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(2..3, "", "three\n")],
@@ -1979,7 +1979,7 @@ async fn test_git_diff_base_change(
     buffer_remote_b.read_with(cx_b, |buffer, _| {
         assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
         git::diff::assert_hunks(
-            buffer.snapshot().git_diff_hunks_in_range(0..4, false),
+            buffer.snapshot().git_diff_hunks_in_row_range(0..4, false),
             &buffer,
             &diff_base,
             &[(2..3, "", "three\n")],

crates/diagnostics/src/diagnostics.rs 🔗

@@ -575,6 +575,15 @@ impl Item for ProjectDiagnosticsEditor {
         unreachable!()
     }
 
+    fn git_diff_recalc(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        self.editor
+            .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
+    }
+
     fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
         Editor::to_item_events(event)
     }

crates/editor/src/multi_buffer.rs 🔗

@@ -2710,11 +2710,73 @@ impl MultiBufferSnapshot {
         row_range: Range<u32>,
         reversed: bool,
     ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
-        self.as_singleton()
-            .into_iter()
-            .flat_map(move |(_, _, buffer)| {
-                buffer.git_diff_hunks_in_range(row_range.clone(), reversed)
-            })
+        let mut cursor = self.excerpts.cursor::<Point>();
+
+        if reversed {
+            cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
+            if cursor.item().is_none() {
+                cursor.prev(&());
+            }
+        } else {
+            cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
+        }
+
+        std::iter::from_fn(move || {
+            let excerpt = cursor.item()?;
+            let multibuffer_start = *cursor.start();
+            let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
+            if multibuffer_start.row >= row_range.end {
+                return None;
+            }
+
+            let mut buffer_start = excerpt.range.context.start;
+            let mut buffer_end = excerpt.range.context.end;
+            let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
+            let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
+
+            if row_range.start > multibuffer_start.row {
+                let buffer_start_point =
+                    excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
+                buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
+            }
+
+            if row_range.end < multibuffer_end.row {
+                let buffer_end_point =
+                    excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
+                buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
+            }
+
+            let buffer_hunks = excerpt
+                .buffer
+                .git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
+                .filter_map(move |hunk| {
+                    let start = multibuffer_start.row
+                        + hunk
+                            .buffer_range
+                            .start
+                            .saturating_sub(excerpt_start_point.row);
+                    let end = multibuffer_start.row
+                        + hunk
+                            .buffer_range
+                            .end
+                            .min(excerpt_end_point.row + 1)
+                            .saturating_sub(excerpt_start_point.row);
+
+                    Some(DiffHunk {
+                        buffer_range: start..end,
+                        diff_base_byte_range: hunk.diff_base_byte_range.clone(),
+                    })
+                });
+
+            if reversed {
+                cursor.prev(&());
+            } else {
+                cursor.next(&());
+            }
+
+            Some(buffer_hunks)
+        })
+        .flatten()
     }
 
     pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
@@ -3546,11 +3608,12 @@ impl ToPointUtf16 for PointUtf16 {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use gpui::MutableAppContext;
+    use gpui::{MutableAppContext, TestAppContext};
     use language::{Buffer, Rope};
     use rand::prelude::*;
     use settings::Settings;
     use std::{env, rc::Rc};
+    use unindent::Unindent;
 
     use util::test::sample_text;
 
@@ -4168,6 +4231,178 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
+        use git::diff::DiffHunkStatus;
+
+        // buffer has two modified hunks with two rows each
+        let buffer_1 = cx.add_model(|cx| {
+            let mut buffer = Buffer::new(
+                0,
+                "
+                1.zero
+                1.ONE
+                1.TWO
+                1.three
+                1.FOUR
+                1.FIVE
+                1.six
+            "
+                .unindent(),
+                cx,
+            );
+            buffer.set_diff_base(
+                Some(
+                    "
+                1.zero
+                1.one
+                1.two
+                1.three
+                1.four
+                1.five
+                1.six
+            "
+                    .unindent(),
+                ),
+                cx,
+            );
+            buffer
+        });
+
+        // buffer has a deletion hunk and an insertion hunk
+        let buffer_2 = cx.add_model(|cx| {
+            let mut buffer = Buffer::new(
+                0,
+                "
+                2.zero
+                2.one
+                2.two
+                2.three
+                2.four
+                2.five
+                2.six
+            "
+                .unindent(),
+                cx,
+            );
+            buffer.set_diff_base(
+                Some(
+                    "
+                2.zero
+                2.one
+                2.one-and-a-half
+                2.two
+                2.three
+                2.four
+                2.six
+            "
+                    .unindent(),
+                ),
+                cx,
+            );
+            buffer
+        });
+
+        cx.foreground().run_until_parked();
+
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    // excerpt ends in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt begins in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(5, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    // excerpt ends at a deletion
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt starts at a deletion
+                    ExcerptRange {
+                        context: Point::new(2, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains a deletion hunk
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains an insertion hunk
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
+
+        assert_eq!(
+            snapshot.text(),
+            "
+                1.zero
+                1.ONE
+                1.FIVE
+                1.six
+                2.zero
+                2.one
+                2.two
+                2.one
+                2.two
+                2.four
+                2.five
+                2.six"
+                .unindent()
+        );
+
+        let expected = [
+            (DiffHunkStatus::Modified, 1..2),
+            (DiffHunkStatus::Modified, 2..3),
+            //TODO: Define better when and where removed hunks show up at range extremities
+            (DiffHunkStatus::Removed, 6..6),
+            (DiffHunkStatus::Removed, 8..8),
+            (DiffHunkStatus::Added, 10..11),
+        ];
+
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range(0..12, false)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            &expected,
+        );
+
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range(0..12, true)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            expected
+                .iter()
+                .rev()
+                .cloned()
+                .collect::<Vec<_>>()
+                .as_slice(),
+        );
+    }
+
     #[gpui::test(iterations = 100)]
     fn test_random_multibuffer(cx: &mut MutableAppContext, mut rng: StdRng) {
         let operations = env::var("OPERATIONS")

crates/git/src/diff.rs 🔗

@@ -71,18 +71,26 @@ impl BufferDiff {
         }
     }
 
-    pub fn hunks_in_range<'a>(
+    pub fn hunks_in_row_range<'a>(
         &'a self,
-        query_row_range: Range<u32>,
+        range: Range<u32>,
         buffer: &'a BufferSnapshot,
         reversed: bool,
     ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
-        let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
-        let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
+        let start = buffer.anchor_before(Point::new(range.start, 0));
+        let end = buffer.anchor_after(Point::new(range.end, 0));
+        self.hunks_intersecting_range(start..end, buffer, reversed)
+    }
 
+    pub fn hunks_intersecting_range<'a>(
+        &'a self,
+        range: Range<Anchor>,
+        buffer: &'a BufferSnapshot,
+        reversed: bool,
+    ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
         let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
-            let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
-            let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
+            let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
+            let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
             !before_start && !after_end
         });
 
@@ -141,7 +149,9 @@ impl BufferDiff {
 
     #[cfg(test)]
     fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
-        self.hunks_in_range(0..u32::MAX, text, false)
+        let start = text.anchor_before(Point::new(0, 0));
+        let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
+        self.hunks_intersecting_range(start..end, text, false)
     }
 
     fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@@ -355,7 +365,7 @@ mod tests {
         assert_eq!(diff.hunks(&buffer).count(), 8);
 
         assert_hunks(
-            diff.hunks_in_range(7..12, &buffer, false),
+            diff.hunks_in_row_range(7..12, &buffer, false),
             &buffer,
             &diff_base,
             &[

crates/language/src/buffer.rs 🔗

@@ -2310,13 +2310,21 @@ impl BufferSnapshot {
             })
     }
 
-    pub fn git_diff_hunks_in_range<'a>(
+    pub fn git_diff_hunks_in_row_range<'a>(
         &'a self,
-        query_row_range: Range<u32>,
+        range: Range<u32>,
+        reversed: bool,
+    ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
+        self.git_diff.hunks_in_row_range(range, self, reversed)
+    }
+
+    pub fn git_diff_hunks_intersecting_range<'a>(
+        &'a self,
+        range: Range<Anchor>,
         reversed: bool,
     ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
         self.git_diff
-            .hunks_in_range(query_row_range, self, reversed)
+            .hunks_intersecting_range(range, self, reversed)
     }
 
     pub fn diagnostics_in_range<'a, T, O>(

crates/search/src/project_search.rs 🔗

@@ -334,6 +334,15 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.navigate(data, cx))
     }
 
+    fn git_diff_recalc(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        self.results_editor
+            .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
+    }
+
     fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
         match event {
             ViewEvent::UpdateTab => vec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab],

crates/sum_tree/src/cursor.rs 🔗

@@ -597,6 +597,10 @@ where
         self.cursor.item()
     }
 
+    pub fn item_summary(&self) -> Option<&'a T::Summary> {
+        self.cursor.item_summary()
+    }
+
     pub fn next(&mut self, cx: &<T::Summary as Summary>::Context) {
         self.cursor.next_internal(&mut self.filter_node, cx);
     }