Emit events from a multibuffer when adding/removing excerpts

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs       |  18 +-
crates/editor/src/multi_buffer.rs | 188 +++++++++++++++++++++++++++++---
2 files changed, 175 insertions(+), 31 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -6368,22 +6368,22 @@ impl Editor {
     fn on_buffer_event(
         &mut self,
         _: ModelHandle<MultiBuffer>,
-        event: &language::Event,
+        event: &multi_buffer::Event,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            language::Event::Edited => {
+            multi_buffer::Event::Edited => {
                 self.refresh_active_diagnostics(cx);
                 self.refresh_code_actions(cx);
                 cx.emit(Event::BufferEdited);
             }
-            language::Event::Reparsed => cx.emit(Event::Reparsed),
-            language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
-            language::Event::Saved => cx.emit(Event::Saved),
-            language::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
-            language::Event::Reloaded => cx.emit(Event::TitleChanged),
-            language::Event::Closed => cx.emit(Event::Closed),
-            language::Event::DiagnosticsUpdated => {
+            multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
+            multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
+            multi_buffer::Event::Saved => cx.emit(Event::Saved),
+            multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
+            multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
+            multi_buffer::Event::Closed => cx.emit(Event::Closed),
+            multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }
             _ => {}

crates/editor/src/multi_buffer.rs 🔗

@@ -9,9 +9,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
 pub use language::Completion;
 use language::{
     char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
-    DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
-    OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
-    ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
+    DiagnosticEntry, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem,
+    Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _,
+    ToPointUtf16 as _, TransactionId, Unclipped,
 };
 use smallvec::SmallVec;
 use std::{
@@ -833,6 +833,30 @@ impl MultiBuffer {
     ) -> Vec<ExcerptId>
     where
         O: text::ToOffset,
+    {
+        let mut ids = Vec::new();
+        let mut next_excerpt_id = self.next_excerpt_id;
+        self.insert_excerpts_with_ids_after(
+            prev_excerpt_id,
+            buffer,
+            ranges.into_iter().map(|range| {
+                let id = ExcerptId(post_inc(&mut next_excerpt_id));
+                ids.push(id);
+                (id, range)
+            }),
+            cx,
+        );
+        ids
+    }
+
+    pub fn insert_excerpts_with_ids_after<O>(
+        &mut self,
+        prev_excerpt_id: ExcerptId,
+        buffer: ModelHandle<Buffer>,
+        ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
+        cx: &mut ModelContext<Self>,
+    ) where
+        O: text::ToOffset,
     {
         assert_eq!(self.history.transaction_depth, 0);
         let mut ranges = ranges.into_iter().peekable();
@@ -858,7 +882,7 @@ impl MultiBuffer {
                 cx.observe(&buffer, |_, _, cx| cx.notify()),
                 cx.subscribe(&buffer, Self::on_buffer_event),
             ],
-            buffer,
+            buffer: buffer.clone(),
         });
 
         let mut snapshot = self.snapshot.borrow_mut();
@@ -883,8 +907,8 @@ impl MultiBuffer {
             Locator::max()
         };
 
-        let mut ids = Vec::new();
-        while let Some(range) = ranges.next() {
+        let mut excerpts = Vec::new();
+        while let Some((id, range)) = ranges.next() {
             let locator = Locator::between(&prev_locator, &next_locator);
             if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
                 buffer_state.excerpts.insert(ix, locator.clone());
@@ -897,7 +921,10 @@ impl MultiBuffer {
                         ..buffer_snapshot.anchor_after(&primary.end)
                 }),
             };
-            let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
+            if id.0 >= self.next_excerpt_id {
+                self.next_excerpt_id = id.0 + 1;
+            }
+            excerpts.push((id, range.clone()));
             let excerpt = Excerpt::new(
                 id,
                 locator.clone(),
@@ -909,7 +936,6 @@ impl MultiBuffer {
             new_excerpts.push(excerpt, &());
             prev_locator = locator.clone();
             new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
-            ids.push(id);
         }
 
         let edit_end = new_excerpts.summary().text.len;
@@ -929,12 +955,17 @@ impl MultiBuffer {
             new: edit_start..edit_end,
         }]);
         cx.emit(Event::Edited);
+        cx.emit(Event::ExcerptsAdded {
+            buffer,
+            predecessor: prev_excerpt_id,
+            excerpts,
+        });
         cx.notify();
-        ids
     }
 
     pub fn clear(&mut self, cx: &mut ModelContext<Self>) {
         self.sync(cx);
+        let ids = self.excerpt_ids();
         self.buffers.borrow_mut().clear();
         let mut snapshot = self.snapshot.borrow_mut();
         let prev_len = snapshot.len();
@@ -948,6 +979,7 @@ impl MultiBuffer {
             new: 0..0,
         }]);
         cx.emit(Event::Edited);
+        cx.emit(Event::ExcerptsRemoved { ids });
         cx.notify();
     }
 
@@ -1071,12 +1103,14 @@ impl MultiBuffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.sync(cx);
+        let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
+
         let mut buffers = self.buffers.borrow_mut();
         let mut snapshot = self.snapshot.borrow_mut();
         let mut new_excerpts = SumTree::new();
         let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
         let mut edits = Vec::new();
-        let mut excerpt_ids = excerpt_ids.into_iter().peekable();
+        let mut excerpt_ids = ids.iter().copied().peekable();
 
         while let Some(excerpt_id) = excerpt_ids.next() {
             // Seek to the next excerpt to remove, preserving any preceding excerpts.
@@ -1143,6 +1177,7 @@ impl MultiBuffer {
 
         self.subscriptions.publish_mut(edits);
         cx.emit(Event::Edited);
+        cx.emit(Event::ExcerptsRemoved { ids });
         cx.notify();
     }
 
@@ -1165,10 +1200,22 @@ impl MultiBuffer {
     fn on_buffer_event(
         &mut self,
         _: ModelHandle<Buffer>,
-        event: &Event,
+        event: &language::Event,
         cx: &mut ModelContext<Self>,
     ) {
-        cx.emit(event.clone());
+        cx.emit(match event {
+            language::Event::Edited => Event::Edited,
+            language::Event::DirtyChanged => Event::DirtyChanged,
+            language::Event::Saved => Event::Saved,
+            language::Event::FileHandleChanged => Event::FileHandleChanged,
+            language::Event::Reloaded => Event::Reloaded,
+            language::Event::Reparsed => Event::Reparsed,
+            language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
+            language::Event::Closed => Event::Closed,
+
+            //
+            language::Event::Operation(_) => return,
+        });
     }
 
     pub fn all_buffers(&self) -> HashSet<ModelHandle<Buffer>> {
@@ -1603,8 +1650,28 @@ impl MultiBuffer {
     }
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+    ExcerptsAdded {
+        buffer: ModelHandle<Buffer>,
+        predecessor: ExcerptId,
+        excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
+    },
+    ExcerptsRemoved {
+        ids: Vec<ExcerptId>,
+    },
+    Edited,
+    Reloaded,
+    Reparsed,
+    Saved,
+    FileHandleChanged,
+    Closed,
+    DirtyChanged,
+    DiagnosticsUpdated,
+}
+
 impl Entity for MultiBuffer {
-    type Event = language::Event;
+    type Event = Event;
 }
 
 impl MultiBufferSnapshot {
@@ -3468,7 +3535,7 @@ mod tests {
     use util::test::sample_text;
 
     #[gpui::test]
-    fn test_singleton_multibuffer(cx: &mut MutableAppContext) {
+    fn test_singleton(cx: &mut MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
         let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 
@@ -3495,7 +3562,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_remote_multibuffer(cx: &mut MutableAppContext) {
+    fn test_remote(cx: &mut MutableAppContext) {
         let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
         let guest_buffer = cx.add_model(|cx| {
             let state = host_buffer.read(cx).to_proto();
@@ -3526,7 +3593,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_excerpt_buffer(cx: &mut MutableAppContext) {
+    fn test_excerpt_boundaries_and_clipping(cx: &mut MutableAppContext) {
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -3535,7 +3602,9 @@ mod tests {
         multibuffer.update(cx, |_, cx| {
             let events = events.clone();
             cx.subscribe(&multibuffer, move |_, _, event, _| {
-                events.borrow_mut().push(event.clone())
+                if let Event::Edited = event {
+                    events.borrow_mut().push(event.clone())
+                }
             })
             .detach();
         });
@@ -3748,7 +3817,84 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_excerpts_with_context_lines(cx: &mut MutableAppContext) {
+    fn test_excerpt_events(cx: &mut MutableAppContext) {
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx));
+
+        let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+
+        follower_multibuffer.update(cx, |_, cx| {
+            cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
+                match event.clone() {
+                    Event::ExcerptsAdded {
+                        buffer,
+                        predecessor,
+                        excerpts,
+                    } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
+                    Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
+                    _ => {}
+                }
+            })
+            .detach();
+        });
+
+        leader_multibuffer.update(cx, |leader, cx| {
+            leader.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: 0..8,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 12..16,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            leader.insert_excerpts_after(
+                leader.excerpt_ids()[0],
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: 0..5,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 10..15,
+                        primary: None,
+                    },
+                ],
+                cx,
+            )
+        });
+        assert_eq!(
+            leader_multibuffer.read(cx).snapshot(cx).text(),
+            follower_multibuffer.read(cx).snapshot(cx).text(),
+        );
+
+        leader_multibuffer.update(cx, |leader, cx| {
+            let excerpt_ids = leader.excerpt_ids();
+            leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx);
+        });
+        assert_eq!(
+            leader_multibuffer.read(cx).snapshot(cx).text(),
+            follower_multibuffer.read(cx).snapshot(cx).text(),
+        );
+
+        leader_multibuffer.update(cx, |leader, cx| {
+            leader.clear(cx);
+        });
+        assert_eq!(
+            leader_multibuffer.read(cx).snapshot(cx).text(),
+            follower_multibuffer.read(cx).snapshot(cx).text(),
+        );
+    }
+
+    #[gpui::test]
+    fn test_push_excerpts_with_context_lines(cx: &mut MutableAppContext) {
         let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
@@ -3784,7 +3930,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_empty_excerpt_buffer(cx: &mut MutableAppContext) {
+    fn test_empty_multibuffer(cx: &mut MutableAppContext) {
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
 
         let snapshot = multibuffer.read(cx).snapshot(cx);
@@ -3872,9 +4018,7 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts(
-        cx: &mut MutableAppContext,
-    ) {
+    fn test_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, "ABCDEFGHIJKLMNOP", cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));