WIP - MultiBuffer::refresh_anchors

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs       |  13 ++
crates/editor/src/multi_buffer.rs | 173 +++++++++++++++++++++++++++-----
2 files changed, 159 insertions(+), 27 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -3258,6 +3258,19 @@ impl Editor {
         );
     }
 
+    pub fn refresh_selections(&mut self, cx: &mut ViewContext<Self>) {
+        let anchors = self.buffer.update(cx, |buffer, cx| {
+            let snapshot = buffer.read(cx);
+            snapshot.refresh_anchors(
+                self.selections
+                    .iter()
+                    .flat_map(|selection| [&selection.start, &selection.end]),
+            )
+        });
+
+        todo!();
+    }
+
     fn set_selections(&mut self, selections: Arc<[Selection<Anchor>]>, cx: &mut ViewContext<Self>) {
         self.selections = selections;
         self.buffer.update(cx, |buffer, cx| {

crates/editor/src/multi_buffer.rs 🔗

@@ -1302,6 +1302,61 @@ impl MultiBufferSnapshot {
         position
     }
 
+    pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<Anchor>
+    where
+        I: 'a + IntoIterator<Item = &'a Anchor>,
+    {
+        let mut anchors = anchors.into_iter().peekable();
+        let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
+        let mut result = Vec::new();
+        while let Some(anchor) = anchors.peek() {
+            let old_excerpt_id = &anchor.excerpt_id;
+
+            // Find the location where this anchor's excerpt should be,
+            cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &());
+            if cursor.item().is_none() {
+                cursor.next(&());
+            }
+
+            let next_excerpt = cursor.item();
+            let prev_excerpt = cursor.prev_item();
+
+            // Process all of the anchors for this excerpt.
+            while let Some(&anchor) = anchors.peek() {
+                if anchor.excerpt_id != *old_excerpt_id {
+                    break;
+                }
+                let mut anchor = anchors.next().unwrap().clone();
+
+                // If the old excerpt no longer exists at this location, then attempt to
+                // find an equivalent position for this anchor in an adjacent excerpt.
+                if next_excerpt.map_or(true, |e| e.id != *old_excerpt_id) {
+                    for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
+                        if excerpt.buffer_id == anchor.buffer_id
+                            && excerpt
+                                .range
+                                .start
+                                .cmp(&anchor.text_anchor, &excerpt.buffer)
+                                .unwrap()
+                                .is_le()
+                            && excerpt
+                                .range
+                                .end
+                                .cmp(&anchor.text_anchor, &excerpt.buffer)
+                                .unwrap()
+                                .is_ge()
+                        {
+                            anchor.excerpt_id = excerpt.id.clone();
+                        }
+                    }
+                }
+
+                result.push(anchor);
+            }
+        }
+        result
+    }
+
     pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec<D>
     where
         D: TextDimension + Ord + Sub<D, Output = D>,
@@ -2375,17 +2430,17 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_multibuffer_anchors_after_replacing_excerpts(cx: &mut MutableAppContext) {
+    fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts(
+        cx: &mut MutableAppContext,
+    ) {
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx));
-
-        // Create an insertion id in buffer 1 that doesn't exist in buffer 2
-        buffer_1.update(cx, |buffer, cx| {
-            buffer.edit([4..4], "123", cx);
-        });
-
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
-        let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
+
+        // Create an insertion id in buffer 1 that doesn't exist in buffer 2.
+        // Add an excerpt from buffer 1 that spans this new insertion.
+        buffer_1.update(cx, |buffer, cx| buffer.edit([4..4], "123", cx));
+        let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.push_excerpt(
                 ExcerptProperties {
                     buffer: &buffer_1,
@@ -2395,31 +2450,95 @@ mod tests {
             )
         });
 
-        // Create an anchor in the second insertion of buffer 1
-        let anchor = multibuffer.read(cx).read(cx).anchor_before(7);
+        let snapshot_1 = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(snapshot_1.text(), "abcd123");
+
+        // Replace the buffer 1 excerpt with new excerpts from buffer 2.
+        let (excerpt_id_2, excerpt_id_3, excerpt_id_4) =
+            multibuffer.update(cx, |multibuffer, cx| {
+                multibuffer.remove_excerpts([&excerpt_id_1], cx);
+                (
+                    multibuffer.push_excerpt(
+                        ExcerptProperties {
+                            buffer: &buffer_2,
+                            range: 0..4,
+                        },
+                        cx,
+                    ),
+                    multibuffer.push_excerpt(
+                        ExcerptProperties {
+                            buffer: &buffer_2,
+                            range: 6..10,
+                        },
+                        cx,
+                    ),
+                    multibuffer.push_excerpt(
+                        ExcerptProperties {
+                            buffer: &buffer_2,
+                            range: 12..16,
+                        },
+                        cx,
+                    ),
+                )
+            });
+        let snapshot_2 = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP");
 
-        multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.remove_excerpts([&excerpt_id], cx);
-            let new_excerpt_id = multibuffer.push_excerpt(
+        // And excerpt id has been reused.
+        assert_eq!(excerpt_id_2, excerpt_id_1);
+
+        // Resolve some anchors from the previous snapshot in the new snapshot.
+        // Although there is still an excerpt with the same id, it is for
+        // a different buffer, so we don't attempt to resolve the old text
+        // anchor in the new buffer.
+        assert_eq!(
+            snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
+            0
+        );
+        assert_eq!(
+            snapshot_2.summaries_for_anchors::<usize, _>(&[
+                snapshot_1.anchor_before(2),
+                snapshot_1.anchor_after(3)
+            ]),
+            vec![0, 0]
+        );
+
+        // Replace the middle excerpt with a smaller excerpt in buffer 2,
+        // that intersects the old excerpt.
+        let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.remove_excerpts([&excerpt_id_3], cx);
+            multibuffer.insert_excerpt_after(
+                &excerpt_id_3,
                 ExcerptProperties {
                     buffer: &buffer_2,
-                    range: 0..5,
+                    range: 5..8,
                 },
                 cx,
-            );
+            )
+        });
 
-            // The same id is reused for an excerpt in a different buffer.
-            assert_eq!(new_excerpt_id, excerpt_id);
+        let snapshot_3 = multibuffer.read(cx).snapshot(cx);
+        assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP");
+        assert_ne!(excerpt_id_5, excerpt_id_3);
+
+        // Resolve some anchors from the previous snapshot in the new snapshot.
+        // The anchor in the middle excerpt snaps to the beginning of the
+        // excerpt, since it is not
+        let anchors = [
+            snapshot_2.anchor_after(2),
+            snapshot_2.anchor_after(6),
+            snapshot_2.anchor_after(14),
+        ];
+        assert_eq!(
+            snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
+            &[2, 9, 13]
+        );
 
-            // We don't attempt to resolve the text anchor from buffer 1
-            // in buffer 2.
-            let snapshot = multibuffer.snapshot(cx);
-            assert_eq!(snapshot.summary_for_anchor::<usize>(&anchor), 0);
-            assert_eq!(
-                snapshot.summaries_for_anchors::<usize, _>(&[anchor]),
-                vec![0]
-            );
-        });
+        let new_anchors = snapshot_3.refresh_anchors(&anchors);
+        assert_eq!(
+            snapshot_3.summaries_for_anchors::<usize, _>(&new_anchors),
+            &[2, 7, 13]
+        );
     }
 
     #[gpui::test(iterations = 100)]