Replicate multibuffer excerpt additions and removals to followers

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs        |  13 ++
crates/editor/src/editor_tests.rs  | 189 ++++++++++++++++++++++++++++---
crates/editor/src/items.rs         |  73 +++++++++--
crates/workspace/src/workspace.rs  |  10 +
styles/src/styleTree/components.ts |  10 +
5 files changed, 254 insertions(+), 41 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -6377,6 +6377,18 @@ impl Editor {
                 self.refresh_code_actions(cx);
                 cx.emit(Event::BufferEdited);
             }
+            multi_buffer::Event::ExcerptsAdded {
+                buffer,
+                predecessor,
+                excerpts,
+            } => cx.emit(Event::ExcerptsAdded {
+                buffer: buffer.clone(),
+                predecessor: *predecessor,
+                excerpts: excerpts.clone(),
+            }),
+            multi_buffer::Event::ExcerptsRemoved { ids } => {
+                cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
+            }
             multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
             multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
             multi_buffer::Event::Saved => cx.emit(Event::Saved),
@@ -6386,7 +6398,6 @@ impl Editor {
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }
-            _ => {}
         }
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -4967,19 +4967,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
 }
 
 #[gpui::test]
-fn test_following(cx: &mut gpui::MutableAppContext) {
-    let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-
-    cx.set_global(Settings::test(cx));
+async fn test_following(cx: &mut gpui::TestAppContext) {
+    Settings::test_async(cx);
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
 
-    let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
-    let (_, follower) = cx.add_window(
-        WindowOptions {
-            bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
-            ..Default::default()
-        },
-        |cx| build_editor(buffer.clone(), cx),
-    );
+    let buffer = project.update(cx, |project, cx| {
+        let buffer = project
+            .create_buffer(&sample_text(16, 8, 'a'), None, cx)
+            .unwrap();
+        cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
+    });
+    let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+    let (_, follower) = cx.update(|cx| {
+        cx.add_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
+                ..Default::default()
+            },
+            |cx| build_editor(buffer.clone(), cx),
+        )
+    });
 
     let pending_update = Rc::new(RefCell::new(None));
     follower.update(cx, {
@@ -5000,10 +5008,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     });
     follower.update(cx, |follower, cx| {
         follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+            .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
             .unwrap();
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]);
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![1..1]);
+    });
 
     // Update the scroll position only
     leader.update(cx, |leader, cx| {
@@ -5011,7 +5021,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     });
     follower.update(cx, |follower, cx| {
         follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+            .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
             .unwrap();
     });
     assert_eq!(
@@ -5028,12 +5038,14 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     follower.update(cx, |follower, cx| {
         let initial_scroll_position = follower.scroll_position(cx);
         follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+            .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
             .unwrap();
         assert_eq!(follower.scroll_position(cx), initial_scroll_position);
         assert!(follower.autoscroll_request.is_some());
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..0]);
+    });
 
     // Creating a pending selection that precedes another selection
     leader.update(cx, |leader, cx| {
@@ -5042,10 +5054,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     });
     follower.update(cx, |follower, cx| {
         follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+            .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
             .unwrap();
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]);
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
+    });
 
     // Extend the pending selection so that it surrounds another selection
     leader.update(cx, |leader, cx| {
@@ -5053,10 +5067,143 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
     });
     follower.update(cx, |follower, cx| {
         follower
-            .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+            .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
             .unwrap();
     });
-    assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]);
+    follower.read_with(cx, |follower, cx| {
+        assert_eq!(follower.selections.ranges(cx), vec![0..2]);
+    });
+}
+
+#[gpui::test]
+async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
+    Settings::test_async(cx);
+    let fs = FakeFs::new(cx.background());
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
+
+    let leader = pane.update(cx, |_, cx| {
+        let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+        cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
+    });
+
+    // Start following the editor when it has no excerpts.
+    let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+    let follower_1 = cx
+        .update(|cx| {
+            Editor::from_state_proto(pane.clone(), project.clone(), &mut state_message, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+
+    let follower_1_update = Rc::new(RefCell::new(None));
+    follower_1.update(cx, {
+        let update = follower_1_update.clone();
+        |_, cx| {
+            cx.subscribe(&leader, move |_, leader, event, cx| {
+                leader
+                    .read(cx)
+                    .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+            })
+            .detach();
+        }
+    });
+
+    let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
+        (
+            project
+                .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
+                .unwrap(),
+            project
+                .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
+                .unwrap(),
+        )
+    });
+
+    // Insert some excerpts.
+    leader.update(cx, |leader, cx| {
+        leader.buffer.update(cx, |multibuffer, cx| {
+            let excerpt_ids = multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: 1..6,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 12..15,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 0..3,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.insert_excerpts_after(
+                excerpt_ids[0],
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: 8..12,
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: 0..6,
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+        });
+    });
+
+    // Start following separately after it already has excerpts.
+    let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+    let follower_2 = cx
+        .update(|cx| {
+            Editor::from_state_proto(pane.clone(), project.clone(), &mut state_message, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+    assert_eq!(
+        follower_2.read_with(cx, Editor::text),
+        leader.read_with(cx, Editor::text)
+    );
+
+    // Apply the update of adding the excerpts.
+    follower_1.update(cx, |follower, cx| {
+        follower
+            .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx)
+            .unwrap()
+    });
+    assert_eq!(
+        follower_1.read_with(cx, Editor::text),
+        leader.read_with(cx, Editor::text)
+    );
+
+    // Remove some excerpts.
+    leader.update(cx, |leader, cx| {
+        leader.buffer.update(cx, |multibuffer, cx| {
+            let excerpt_ids = multibuffer.excerpt_ids();
+            multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
+            multibuffer.remove_excerpts([excerpt_ids[0]], cx);
+        });
+    });
+
+    // Apply the update of removing the excerpts.
+    follower_1.update(cx, |follower, cx| {
+        follower
+            .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx)
+            .unwrap()
+    });
+    assert_eq!(
+        follower_1.read_with(cx, Editor::text),
+        leader.read_with(cx, Editor::text)
+    );
 }
 
 #[test]

crates/editor/src/items.rs 🔗

@@ -72,10 +72,6 @@ impl FollowableItem for Editor {
                     editors.find(|editor| {
                         editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0])
                     })
-                } else if let Some(title) = &state.title {
-                    editors.find(|editor| {
-                        editor.read(cx).buffer().read(cx).title(cx).as_ref() == title
-                    })
                 } else {
                     None
                 }
@@ -231,16 +227,17 @@ impl FollowableItem for Editor {
                     predecessor,
                     excerpts,
                 } => {
+                    let buffer_id = buffer.read(cx).remote_id();
                     let mut excerpts = excerpts.iter();
                     if let Some((id, range)) = excerpts.next() {
                         update.inserted_excerpts.push(proto::ExcerptInsertion {
                             previous_excerpt_id: Some(predecessor.to_proto()),
-                            excerpt: serialize_excerpt(buffer, id, range, cx),
+                            excerpt: serialize_excerpt(buffer_id, id, range),
                         });
                         update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
                             proto::ExcerptInsertion {
                                 previous_excerpt_id: None,
-                                excerpt: serialize_excerpt(buffer, id, range, cx),
+                                excerpt: serialize_excerpt(buffer_id, id, range),
                             }
                         }))
                     }
@@ -275,22 +272,69 @@ impl FollowableItem for Editor {
 
     fn apply_update_proto(
         &mut self,
+        project: &ModelHandle<Project>,
         message: update_view::Variant,
         cx: &mut ViewContext<Self>,
     ) -> Result<()> {
         match message {
             update_view::Variant::Editor(message) => {
-                let buffer = self.buffer.read(cx);
-                let buffer = buffer.read(cx);
+                let multibuffer = self.buffer.read(cx);
+                let multibuffer = multibuffer.read(cx);
+                let mut removals = message
+                    .deleted_excerpts
+                    .into_iter()
+                    .map(ExcerptId::from_proto)
+                    .collect::<Vec<_>>();
+                removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
+
                 let selections = message
                     .selections
                     .into_iter()
-                    .filter_map(|selection| deserialize_selection(&buffer, selection))
+                    .filter_map(|selection| deserialize_selection(&multibuffer, selection))
                     .collect::<Vec<_>>();
                 let scroll_top_anchor = message
                     .scroll_top_anchor
-                    .and_then(|anchor| deserialize_anchor(&buffer, anchor));
-                drop(buffer);
+                    .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
+                drop(multibuffer);
+
+                self.buffer.update(cx, |multibuffer, cx| {
+                    let mut insertions = message.inserted_excerpts.into_iter().peekable();
+                    while let Some(insertion) = insertions.next() {
+                        let Some(excerpt) = insertion.excerpt else { continue };
+                        let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
+                        let buffer_id = excerpt.buffer_id;
+                        let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
+    
+                        let adjacent_excerpts = iter::from_fn(|| {
+                            let insertion = insertions.peek()?;
+                            if insertion.previous_excerpt_id.is_none()
+                                && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+                            {
+                                insertions.next()?.excerpt
+                            } else {
+                                None
+                            }
+                        });
+    
+                        multibuffer.insert_excerpts_with_ids_after(
+                            ExcerptId::from_proto(previous_excerpt_id),
+                            buffer,
+                            [excerpt]
+                                .into_iter()
+                                .chain(adjacent_excerpts)
+                                .filter_map(|excerpt| {
+                                    Some((
+                                        ExcerptId::from_proto(excerpt.id),
+                                        deserialize_excerpt_range(excerpt)?,
+                                    ))
+                                }),
+                            cx,
+                        );
+                    }
+                    
+                    multibuffer.remove_excerpts(removals, cx);
+                });
+
 
                 if !selections.is_empty() {
                     self.set_selections_from_remote(selections, cx);
@@ -318,14 +362,13 @@ impl FollowableItem for Editor {
 }
 
 fn serialize_excerpt(
-    buffer: &ModelHandle<Buffer>,
+    buffer_id: u64,
     id: &ExcerptId,
     range: &ExcerptRange<language::Anchor>,
-    cx: &AppContext,
 ) -> Option<proto::Excerpt> {
     Some(proto::Excerpt {
         id: id.to_proto(),
-        buffer_id: buffer.read(cx).remote_id(),
+        buffer_id,
         context_start: Some(serialize_text_anchor(&range.context.start)),
         context_end: Some(serialize_text_anchor(&range.context.end)),
         primary_start: range
@@ -390,7 +433,7 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
     Some(Anchor {
         excerpt_id,
         text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
-        buffer_id: Some(buffer.buffer_id_for_excerpt(excerpt_id)?),
+        buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
     })
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -463,6 +463,7 @@ pub trait FollowableItem: Item {
     ) -> bool;
     fn apply_update_proto(
         &mut self,
+        project: &ModelHandle<Project>,
         message: proto::update_view::Variant,
         cx: &mut ViewContext<Self>,
     ) -> Result<()>;
@@ -482,6 +483,7 @@ pub trait FollowableItemHandle: ItemHandle {
     ) -> bool;
     fn apply_update_proto(
         &self,
+        project: &ModelHandle<Project>,
         message: proto::update_view::Variant,
         cx: &mut MutableAppContext,
     ) -> Result<()>;
@@ -514,10 +516,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
 
     fn apply_update_proto(
         &self,
+        project: &ModelHandle<Project>,
         message: proto::update_view::Variant,
         cx: &mut MutableAppContext,
     ) -> Result<()> {
-        self.update(cx, |this, cx| this.apply_update_proto(message, cx))
+        self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
     }
 
     fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
@@ -2477,6 +2480,7 @@ impl Workspace {
                     let variant = update_view
                         .variant
                         .ok_or_else(|| anyhow!("missing update view variant"))?;
+                    let project = this.project.clone();
                     this.update_leader_state(leader_id, cx, |state, cx| {
                         let variant = variant.clone();
                         match state
@@ -2485,7 +2489,7 @@ impl Workspace {
                             .or_insert(FollowerItem::Loading(Vec::new()))
                         {
                             FollowerItem::Loaded(item) => {
-                                item.apply_update_proto(variant, cx).log_err();
+                                item.apply_update_proto(&project, variant, cx).log_err();
                             }
                             FollowerItem::Loading(updates) => updates.push(variant),
                         }
@@ -2576,7 +2580,7 @@ impl Workspace {
                             let e = e.into_mut();
                             if let FollowerItem::Loading(updates) = e {
                                 for update in updates.drain(..) {
-                                    item.apply_update_proto(update, cx)
+                                    item.apply_update_proto(&this.project, update, cx)
                                         .context("failed to apply view update")
                                         .log_err();
                                 }

styles/src/styleTree/components.ts 🔗

@@ -12,8 +12,16 @@ function isStyleSet(key: any): key is StyleSets {
     "negative",
   ].includes(key);
 }
+
 function isStyle(key: any): key is Styles {
-  return ["default", "active", "disabled", "hovered", "pressed", "inverted"].includes(key);
+  return [
+    "default",
+    "active",
+    "disabled",
+    "hovered",
+    "pressed",
+    "inverted",
+  ].includes(key);
 }
 function getStyle(
   layer: Layer,