Start work on following in multi-buffers

Max Brunsfeld created

Change summary

crates/diagnostics/src/diagnostics.rs |   2 
crates/editor/src/editor.rs           |  18 ++
crates/editor/src/editor_tests.rs     |   4 
crates/editor/src/items.rs            | 181 +++++++++++++++++++++++++---
crates/editor/src/multi_buffer.rs     |  48 ++++---
crates/rpc/proto/zed.proto            |  36 ++++-
crates/search/src/project_search.rs   |   4 
crates/text/src/text.rs               |   4 
8 files changed, 241 insertions(+), 56 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -160,7 +160,7 @@ impl ProjectDiagnosticsEditor {
             editor.set_vertical_scroll_margin(5, cx);
             editor
         });
-        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
+        cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
             .detach();
 
         let project = project_handle.read(cx);

crates/editor/src/editor.rs 🔗

@@ -6587,8 +6587,16 @@ fn compute_scroll_position(
     scroll_position
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
+    ExcerptsAdded {
+        buffer: ModelHandle<Buffer>,
+        predecessor: ExcerptId,
+        excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
+    },
+    ExcerptsRemoved {
+        ids: Vec<ExcerptId>,
+    },
     BufferEdited,
     Edited,
     Reparsed,
@@ -6596,8 +6604,12 @@ pub enum Event {
     DirtyChanged,
     Saved,
     TitleChanged,
-    SelectionsChanged { local: bool },
-    ScrollPositionChanged { local: bool },
+    SelectionsChanged {
+        local: bool,
+    },
+    ScrollPositionChanged {
+        local: bool,
+    },
     Closed,
     IgnoredInput,
 }

crates/editor/src/editor_tests.rs 🔗

@@ -38,7 +38,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
                     event,
                     Event::Edited | Event::BufferEdited | Event::DirtyChanged
                 ) {
-                    events.borrow_mut().push(("editor1", *event));
+                    events.borrow_mut().push(("editor1", event.clone()));
                 }
             })
             .detach();
@@ -53,7 +53,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
                     event,
                     Event::Edited | Event::BufferEdited | Event::DirtyChanged
                 ) {
-                    events.borrow_mut().push(("editor2", *event));
+                    events.borrow_mut().push(("editor2", event.clone()));
                 }
             })
             .detach();

crates/editor/src/items.rs 🔗

@@ -1,14 +1,16 @@
 use crate::{
     display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
-    movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
-    MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
+    movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange,
+    MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
 };
 use anyhow::{anyhow, Result};
+use collections::HashSet;
 use futures::FutureExt;
 use gpui::{
     elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
     RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
 };
+use language::proto::serialize_anchor as serialize_text_anchor;
 use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
 use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
@@ -18,6 +20,7 @@ use std::{
     borrow::Cow,
     cmp::{self, Ordering},
     fmt::Write,
+    iter,
     ops::Range,
     path::{Path, PathBuf},
 };
@@ -48,22 +51,75 @@ impl FollowableItem for Editor {
             return None;
         };
 
-        let buffer = project.update(cx, |project, cx| {
-            project.open_buffer_by_id(state.buffer_id, cx)
+        let replica_id = project.read(cx).replica_id();
+        let buffer_ids = state
+            .excerpts
+            .iter()
+            .map(|excerpt| excerpt.buffer_id)
+            .collect::<HashSet<_>>();
+        let buffers = project.update(cx, |project, cx| {
+            buffer_ids
+                .iter()
+                .map(|id| project.open_buffer_by_id(*id, cx))
+                .collect::<Vec<_>>()
         });
+
         Some(cx.spawn(|mut cx| async move {
-            let buffer = buffer.await?;
-            let editor = pane
-                .read_with(&cx, |pane, cx| {
-                    pane.items_of_type::<Self>().find(|editor| {
-                        editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
+            let mut buffers = futures::future::try_join_all(buffers).await?;
+            let editor = pane.read_with(&cx, |pane, cx| {
+                let mut editors = pane.items_of_type::<Self>();
+                if state.singleton && buffers.len() == 1 {
+                    editors.find(|editor| {
+                        editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0])
                     })
-                })
-                .unwrap_or_else(|| {
-                    pane.update(&mut cx, |_, cx| {
-                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx))
+                } else if let Some(title) = &state.title {
+                    editors.find(|editor| {
+                        editor.read(cx).buffer().read(cx).title(cx).as_ref() == title
                     })
-                });
+                } else {
+                    None
+                }
+            });
+
+            let editor = editor.unwrap_or_else(|| {
+                pane.update(&mut cx, |_, cx| {
+                    let multibuffer = cx.add_model(|cx| {
+                        let mut multibuffer;
+                        if state.singleton && buffers.len() == 1 {
+                            multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+                        } else {
+                            multibuffer = MultiBuffer::new(replica_id);
+                            let mut excerpts = state.excerpts.into_iter().peekable();
+                            while let Some(excerpt) = excerpts.peek() {
+                                let buffer_id = excerpt.buffer_id;
+                                let buffer_excerpts = iter::from_fn(|| {
+                                    let excerpt = excerpts.peek()?;
+                                    (excerpt.buffer_id == buffer_id)
+                                        .then(|| excerpts.next().unwrap())
+                                });
+                                let buffer =
+                                    buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+                                if let Some(buffer) = buffer {
+                                    multibuffer.push_excerpts(
+                                        buffer.clone(),
+                                        buffer_excerpts.filter_map(deserialize_excerpt_range),
+                                        cx,
+                                    );
+                                }
+                            }
+                        };
+
+                        if let Some(title) = &state.title {
+                            multibuffer = multibuffer.with_title(title.clone())
+                        }
+
+                        multibuffer
+                    });
+
+                    cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
+                })
+            });
+
             editor.update(&mut cx, |editor, cx| {
                 let buffer = editor.buffer.read(cx).read(cx);
                 let selections = state
@@ -90,8 +146,9 @@ impl FollowableItem for Editor {
                     );
                 }
 
-                Ok::<_, anyhow::Error>(())
+                anyhow::Ok(())
             })?;
+
             Ok(editor)
         }))
     }
@@ -122,9 +179,30 @@ impl FollowableItem for Editor {
     }
 
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
-        let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
+        let buffer = self.buffer.read(cx);
+        let excerpts = buffer
+            .read(cx)
+            .excerpts()
+            .map(|(id, buffer, range)| proto::Excerpt {
+                id: id.to_proto(),
+                buffer_id: buffer.remote_id(),
+                context_start: Some(serialize_text_anchor(&range.context.start)),
+                context_end: Some(serialize_text_anchor(&range.context.end)),
+                primary_start: range
+                    .primary
+                    .as_ref()
+                    .map(|range| serialize_text_anchor(&range.start)),
+                primary_end: range
+                    .primary
+                    .as_ref()
+                    .map(|range| serialize_text_anchor(&range.end)),
+            })
+            .collect();
+
         Some(proto::view::Variant::Editor(proto::view::Editor {
-            buffer_id,
+            singleton: buffer.is_singleton(),
+            title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
+            excerpts,
             scroll_top_anchor: Some(serialize_anchor(&self.scroll_top_anchor)),
             scroll_x: self.scroll_position.x(),
             scroll_y: self.scroll_position.y(),
@@ -141,13 +219,39 @@ impl FollowableItem for Editor {
         &self,
         event: &Self::Event,
         update: &mut Option<proto::update_view::Variant>,
-        _: &AppContext,
+        cx: &AppContext,
     ) -> bool {
         let update =
             update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
 
         match update {
             proto::update_view::Variant::Editor(update) => match event {
+                Event::ExcerptsAdded {
+                    buffer,
+                    predecessor,
+                    excerpts,
+                } => {
+                    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),
+                        });
+                        update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
+                            proto::ExcerptInsertion {
+                                previous_excerpt_id: None,
+                                excerpt: serialize_excerpt(buffer, id, range, cx),
+                            }
+                        }))
+                    }
+                    true
+                }
+                Event::ExcerptsRemoved { ids } => {
+                    update
+                        .deleted_excerpts
+                        .extend(ids.iter().map(ExcerptId::to_proto));
+                    true
+                }
                 Event::ScrollPositionChanged { .. } => {
                     update.scroll_top_anchor = Some(serialize_anchor(&self.scroll_top_anchor));
                     update.scroll_x = self.scroll_position.x();
@@ -213,6 +317,28 @@ impl FollowableItem for Editor {
     }
 }
 
+fn serialize_excerpt(
+    buffer: &ModelHandle<Buffer>,
+    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(),
+        context_start: Some(serialize_text_anchor(&range.context.start)),
+        context_end: Some(serialize_text_anchor(&range.context.end)),
+        primary_start: range
+            .primary
+            .as_ref()
+            .map(|r| serialize_text_anchor(&r.start)),
+        primary_end: range
+            .primary
+            .as_ref()
+            .map(|r| serialize_text_anchor(&r.end)),
+    })
+}
+
 fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
     proto::Selection {
         id: selection.id as u64,
@@ -225,10 +351,27 @@ fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
 fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
     proto::EditorAnchor {
         excerpt_id: anchor.excerpt_id.to_proto(),
-        anchor: Some(language::proto::serialize_anchor(&anchor.text_anchor)),
+        anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
     }
 }
 
+fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
+    let context = {
+        let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
+        let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
+        start..end
+    };
+    let primary = excerpt
+        .primary_start
+        .zip(excerpt.primary_end)
+        .and_then(|(start, end)| {
+            let start = language::proto::deserialize_anchor(start)?;
+            let end = language::proto::deserialize_anchor(end)?;
+            Some(start..end)
+        });
+    Some(ExcerptRange { context, primary })
+}
+
 fn deserialize_selection(
     buffer: &MultiBufferSnapshot,
     selection: proto::Selection,

crates/editor/src/multi_buffer.rs 🔗

@@ -50,6 +50,26 @@ pub struct MultiBuffer {
     title: Option<String>,
 }
 
+#[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,
+}
+
 #[derive(Clone)]
 struct History {
     next_transaction_id: TransactionId,
@@ -1650,26 +1670,6 @@ 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 = Event;
 }
@@ -2517,6 +2517,14 @@ impl MultiBufferSnapshot {
         }
     }
 
+    pub fn excerpts(
+        &self,
+    ) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
+        self.excerpts
+            .iter()
+            .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
+    }
+
     pub fn excerpt_boundaries_in_range<R, T>(
         &self,
         range: R,

crates/rpc/proto/zed.proto 🔗

@@ -847,10 +847,12 @@ message UpdateView {
     }
 
     message Editor {
-        repeated Selection selections = 1;
-        EditorAnchor scroll_top_anchor = 2;
-        float scroll_x = 3;
-        float scroll_y = 4;
+        repeated ExcerptInsertion inserted_excerpts = 1;
+        repeated uint64 deleted_excerpts = 2;
+        repeated Selection selections = 3;
+        EditorAnchor scroll_top_anchor = 4;
+        float scroll_x = 5;
+        float scroll_y = 6;
     }
 }
 
@@ -863,11 +865,13 @@ message View {
     }
 
     message Editor {
-        uint64 buffer_id = 1;
-        repeated Selection selections = 2;
-        EditorAnchor scroll_top_anchor = 3;
-        float scroll_x = 4;
-        float scroll_y = 5;
+        bool singleton = 1;
+        optional string title = 2;
+        repeated Excerpt excerpts = 3;
+        repeated Selection selections = 4;
+        EditorAnchor scroll_top_anchor = 5;
+        float scroll_x = 6;
+        float scroll_y = 7;
     }
 }
 
@@ -939,6 +943,20 @@ enum CursorShape {
     CursorHollow = 3;
 }
 
+message ExcerptInsertion {
+    Excerpt excerpt = 1;
+    optional uint64 previous_excerpt_id = 2;
+}
+
+message Excerpt {
+    uint64 id = 1;
+    uint64 buffer_id = 2;
+    Anchor context_start = 3;
+    Anchor context_end = 4;
+    Anchor primary_start = 5;
+    Anchor primary_end = 6;
+}
+
 message Anchor {
     uint32 replica_id = 1;
     uint32 local_timestamp = 2;

crates/search/src/project_search.rs 🔗

@@ -388,7 +388,7 @@ impl ProjectSearchView {
         });
         // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
         cx.subscribe(&query_editor, |_, _, event, cx| {
-            cx.emit(ViewEvent::EditorEvent(*event))
+            cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
         .detach();
 
@@ -405,7 +405,7 @@ impl ProjectSearchView {
                 this.update_match_index(cx);
             }
             // Reraise editor events for workspace item activation purposes
-            cx.emit(ViewEvent::EditorEvent(*event));
+            cx.emit(ViewEvent::EditorEvent(event.clone()));
         })
         .detach();
 

crates/text/src/text.rs 🔗

@@ -1496,6 +1496,10 @@ impl BufferSnapshot {
         &self.visible_text
     }
 
+    pub fn remote_id(&self) -> u64 {
+        self.remote_id
+    }
+
     pub fn replica_id(&self) -> ReplicaId {
         self.replica_id
     }