Serialize initial follow state in leader and reflect it in follower

Antonio Scandurra created

Change summary

crates/client/src/client.rs       | 40 +++++++++++++++-
crates/editor/src/editor.rs       |  1 
crates/editor/src/items.rs        | 59 ++++++++++++++++++++++---
crates/language/src/proto.rs      | 19 ++++---
crates/project/src/project.rs     | 77 ++++++++++++++++++++++++++------
crates/rpc/proto/zed.proto        |  2 
crates/server/src/rpc.rs          |  8 +++
crates/text/src/selection.rs      |  6 ++
crates/workspace/src/pane.rs      | 28 ++++++++---
crates/workspace/src/workspace.rs | 69 +++++++++++++++++++++-------
10 files changed, 246 insertions(+), 63 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -494,14 +494,13 @@ impl Client {
             message_type_id,
             Arc::new(move |handle, envelope, cx| {
                 if let Some(client) = client.upgrade() {
-                    let model = handle.downcast::<E>().unwrap();
-                    let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
                     let handle = if let AnyEntityHandle::Model(handle) = handle {
                         handle
                     } else {
                         unreachable!();
                     };
-
+                    let model = handle.downcast::<E>().unwrap();
+                    let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
                     handler(model, *envelope, client.clone(), cx).boxed_local()
                 } else {
                     async move { Ok(()) }.boxed_local()
@@ -513,7 +512,7 @@ impl Client {
         }
     }
 
-    pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage + RequestMessage,
         E: Entity,
@@ -546,6 +545,39 @@ impl Client {
         })
     }
 
+    pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage + RequestMessage,
+        E: View,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<M::Response>>,
+    {
+        self.add_view_message_handler(move |view, envelope, client, cx| {
+            let receipt = envelope.receipt();
+            let response = handler(view, envelope, client.clone(), cx);
+            async move {
+                match response.await {
+                    Ok(response) => {
+                        client.respond(receipt, response)?;
+                        Ok(())
+                    }
+                    Err(error) => {
+                        client.respond_with_error(
+                            receipt,
+                            proto::Error {
+                                message: error.to_string(),
+                            },
+                        )?;
+                        Err(error)
+                    }
+                }
+            }
+        })
+    }
+
     pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
         read_credentials_from_keychain(cx).is_some()
     }

crates/editor/src/editor.rs 🔗

@@ -341,6 +341,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Editor::find_all_references);
 
     workspace::register_project_item::<Editor>(cx);
+    workspace::register_followed_item::<Editor>(cx);
 }
 
 trait SelectionExt {

crates/editor/src/items.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _};
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View,
-    ViewContext, ViewHandle,
+    elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription,
+    Task, View, ViewContext, ViewHandle,
 };
 use language::{Bias, Buffer, Diagnostic, File as _};
 use project::{File, Project, ProjectEntryId, ProjectPath};
@@ -19,13 +19,58 @@ impl FollowedItem for Editor {
         pane: ViewHandle<workspace::Pane>,
         project: ModelHandle<Project>,
         state: &mut Option<proto::view::Variant>,
-        cx: &mut gpui::MutableAppContext,
+        cx: &mut MutableAppContext,
     ) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
-        todo!()
+        let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
+            if let Some(proto::view::Variant::Editor(state)) = state.take() {
+                state
+            } else {
+                unreachable!()
+            }
+        } else {
+            return None;
+        };
+
+        let buffer = project.update(cx, |project, cx| {
+            project.open_buffer_by_id(state.buffer_id, cx)
+        });
+        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)
+                    })
+                })
+                .unwrap_or_else(|| {
+                    cx.add_view(pane.window_id(), |cx| {
+                        Editor::for_buffer(buffer, Some(project), cx)
+                    })
+                });
+            Ok(Box::new(editor) as Box<_>)
+        }))
     }
 
-    fn to_state_message(&self, cx: &mut gpui::MutableAppContext) -> proto::view::Variant {
-        todo!()
+    fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant {
+        let buffer_id = self
+            .buffer
+            .read(cx)
+            .as_singleton()
+            .unwrap()
+            .read(cx)
+            .remote_id();
+        let selection = self.newest_anchor_selection();
+        let selection = Selection {
+            id: selection.id,
+            start: selection.start.text_anchor.clone(),
+            end: selection.end.text_anchor.clone(),
+            reversed: selection.reversed,
+            goal: Default::default(),
+        };
+        proto::view::Variant::Editor(proto::view::Editor {
+            buffer_id,
+            newest_selection: Some(language::proto::serialize_selection(&selection)),
+        })
     }
 }
 

crates/language/src/proto.rs 🔗

@@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
 }
 
 pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
-    selections
-        .iter()
-        .map(|selection| proto::Selection {
-            id: selection.id as u64,
-            start: Some(serialize_anchor(&selection.start)),
-            end: Some(serialize_anchor(&selection.end)),
-            reversed: selection.reversed,
-        })
-        .collect()
+    selections.iter().map(serialize_selection).collect()
+}
+
+pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(serialize_anchor(&selection.start)),
+        end: Some(serialize_anchor(&selection.end)),
+        reversed: selection.reversed,
+    }
 }
 
 pub fn serialize_diagnostics<'a>(

crates/project/src/project.rs 🔗

@@ -267,21 +267,22 @@ impl Project {
         client.add_model_message_handler(Self::handle_update_buffer);
         client.add_model_message_handler(Self::handle_update_diagnostic_summary);
         client.add_model_message_handler(Self::handle_update_worktree);
-        client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion);
-        client.add_entity_request_handler(Self::handle_apply_code_action);
-        client.add_entity_request_handler(Self::handle_format_buffers);
-        client.add_entity_request_handler(Self::handle_get_code_actions);
-        client.add_entity_request_handler(Self::handle_get_completions);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
-        client.add_entity_request_handler(Self::handle_search_project);
-        client.add_entity_request_handler(Self::handle_get_project_symbols);
-        client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
-        client.add_entity_request_handler(Self::handle_open_buffer_by_path);
-        client.add_entity_request_handler(Self::handle_save_buffer);
+        client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
+        client.add_model_request_handler(Self::handle_apply_code_action);
+        client.add_model_request_handler(Self::handle_format_buffers);
+        client.add_model_request_handler(Self::handle_get_code_actions);
+        client.add_model_request_handler(Self::handle_get_completions);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
+        client.add_model_request_handler(Self::handle_search_project);
+        client.add_model_request_handler(Self::handle_get_project_symbols);
+        client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
+        client.add_model_request_handler(Self::handle_open_buffer_by_id);
+        client.add_model_request_handler(Self::handle_open_buffer_by_path);
+        client.add_model_request_handler(Self::handle_save_buffer);
     }
 
     pub fn local(
@@ -488,7 +489,6 @@ impl Project {
         cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
         self.opened_buffers
             .get(&remote_id)
@@ -981,6 +981,32 @@ impl Project {
         })
     }
 
+    pub fn open_buffer_by_id(
+        &mut self,
+        id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        if let Some(buffer) = self.buffer_for_id(id, cx) {
+            Task::ready(Ok(buffer))
+        } else if self.is_local() {
+            Task::ready(Err(anyhow!("buffer {} does not exist", id)))
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self
+                .client
+                .request(proto::OpenBufferById { project_id, id });
+            cx.spawn(|this, mut cx| async move {
+                let buffer = request
+                    .await?
+                    .buffer
+                    .ok_or_else(|| anyhow!("invalid buffer"))?;
+                this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+                    .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
+        }
+    }
+
     pub fn save_buffer_as(
         &mut self,
         buffer: ModelHandle<Buffer>,
@@ -3889,6 +3915,25 @@ impl Project {
         hasher.finalize().as_slice().try_into().unwrap()
     }
 
+    async fn handle_open_buffer_by_id(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferById>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let buffer = this
+            .update(&mut cx, |this, cx| {
+                this.open_buffer_by_id(envelope.payload.id, cx)
+            })
+            .await?;
+        this.update(&mut cx, |this, cx| {
+            Ok(proto::OpenBufferResponse {
+                buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
+            })
+        })
+    }
+
     async fn handle_open_buffer_by_path(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::OpenBufferByPath>,

crates/rpc/proto/zed.proto 🔗

@@ -544,7 +544,7 @@ message Follow {
 }
 
 message FollowResponse {
-    uint64 current_view_id = 1;
+    optional uint64 current_view_id = 1;
     repeated View views = 2;
 }
 

crates/server/src/rpc.rs 🔗

@@ -92,6 +92,7 @@ impl Server {
             .add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
             .add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
             .add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
+            .add_request_handler(Server::forward_project_request::<proto::OpenBufferById>)
             .add_request_handler(Server::forward_project_request::<proto::OpenBufferByPath>)
             .add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
             .add_request_handler(
@@ -4240,6 +4241,13 @@ mod tests {
             })
             .await
             .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .project_path(cx)),
+            Some((worktree_id, "2.txt").into())
+        );
     }
 
     #[gpui::test(iterations = 100)]

crates/text/src/selection.rs 🔗

@@ -18,6 +18,12 @@ pub struct Selection<T> {
     pub goal: SelectionGoal,
 }
 
+impl Default for SelectionGoal {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
 impl<T: Clone> Selection<T> {
     pub fn head(&self) -> T {
         if self.reversed {

crates/workspace/src/pane.rs 🔗

@@ -109,7 +109,7 @@ pub struct Pane {
 
 pub(crate) struct FollowerState {
     pub(crate) leader_id: PeerId,
-    pub(crate) current_view_id: usize,
+    pub(crate) current_view_id: Option<usize>,
     pub(crate) items_by_leader_view_id: HashMap<usize, Box<dyn ItemHandle>>,
 }
 
@@ -308,6 +308,11 @@ impl Pane {
     }
 
     pub(crate) fn add_item(&mut self, mut item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        // Prevent adding the same item to the pane more than once.
+        if self.items.iter().any(|i| i.id() == item.id()) {
+            return;
+        }
+
         item.set_nav_history(self.nav_history.clone(), cx);
         item.added_to_pane(cx);
         let item_idx = cmp::min(self.active_item_index + 1, self.items.len());
@@ -321,13 +326,14 @@ impl Pane {
         follower_state: FollowerState,
         cx: &mut ViewContext<Self>,
     ) -> Result<()> {
-        let current_view_id = follower_state.current_view_id as usize;
-        let item = follower_state
-            .items_by_leader_view_id
-            .get(&current_view_id)
-            .ok_or_else(|| anyhow!("invalid current view id"))?
-            .clone();
-        self.add_item(item, cx);
+        if let Some(current_view_id) = follower_state.current_view_id {
+            let item = follower_state
+                .items_by_leader_view_id
+                .get(&current_view_id)
+                .ok_or_else(|| anyhow!("invalid current view id"))?
+                .clone();
+            self.add_item(item, cx);
+        }
         Ok(())
     }
 
@@ -335,6 +341,12 @@ impl Pane {
         self.items.iter()
     }
 
+    pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
+        self.items
+            .iter()
+            .filter_map(|item| item.to_any().downcast())
+    }
+
     pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
         self.items.get(self.active_item_index).cloned()
     }

crates/workspace/src/workspace.rs 🔗

@@ -111,8 +111,8 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
         ),
     ]);
 
-    client.add_entity_request_handler(Workspace::handle_follow);
-    client.add_model_message_handler(Workspace::handle_unfollow);
+    client.add_view_request_handler(Workspace::handle_follow);
+    client.add_view_message_handler(Workspace::handle_unfollow);
 }
 
 pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
@@ -235,7 +235,7 @@ pub trait FollowedItem {
     where
         Self: Sized;
 
-    fn to_state_message(&self, cx: &mut MutableAppContext) -> proto::view::Variant;
+    fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant;
 }
 
 pub trait ItemHandle: 'static {
@@ -262,6 +262,8 @@ pub trait ItemHandle: 'static {
         cx: &mut MutableAppContext,
     ) -> Task<Result<()>>;
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
+    fn can_be_followed(&self, cx: &AppContext) -> bool;
+    fn to_state_message(&self, cx: &AppContext) -> Option<proto::view::Variant>;
 }
 
 pub trait WeakItemHandle {
@@ -297,11 +299,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
-    fn clone_on_split(
-        &self,
-        // nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Option<Box<dyn ItemHandle>> {
+    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
             cx.add_option_view(|cx| item.clone_on_split(cx))
         })
@@ -381,6 +379,16 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
         self.read(cx).act_as_type(type_id, self, cx)
     }
+
+    fn can_be_followed(&self, cx: &AppContext) -> bool {
+        self.read(cx).as_followed().is_some()
+    }
+
+    fn to_state_message(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        self.read(cx)
+            .as_followed()
+            .map(|item| item.to_state_message(cx))
+    }
 }
 
 impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
@@ -709,6 +717,13 @@ impl Workspace {
         }
     }
 
+    pub fn items<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
+        self.panes.iter().flat_map(|pane| pane.read(cx).items())
+    }
+
     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
         self.items_of_type(cx).max_by_key(|item| item.id())
     }
@@ -717,11 +732,9 @@ impl Workspace {
         &'a self,
         cx: &'a AppContext,
     ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
-        self.panes.iter().flat_map(|pane| {
-            pane.read(cx)
-                .items()
-                .filter_map(|item| item.to_any().downcast())
-        })
+        self.panes
+            .iter()
+            .flat_map(|pane| pane.read(cx).items_of_type())
     }
 
     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
@@ -1085,7 +1098,7 @@ impl Workspace {
                         pane.set_follow_state(
                             FollowerState {
                                 leader_id,
-                                current_view_id: response.current_view_id as usize,
+                                current_view_id: response.current_view_id.map(|id| id as usize),
                                 items_by_leader_view_id,
                             },
                             cx,
@@ -1310,13 +1323,33 @@ impl Workspace {
 
     async fn handle_follow(
         this: ViewHandle<Self>,
-        envelope: TypedEnvelope<proto::Follow>,
+        _: TypedEnvelope<proto::Follow>,
         _: Arc<Client>,
         cx: AsyncAppContext,
     ) -> Result<proto::FollowResponse> {
-        Ok(proto::FollowResponse {
-            current_view_id: 0,
-            views: Default::default(),
+        this.read_with(&cx, |this, cx| {
+            let current_view_id = if let Some(active_item) = this.active_item(cx) {
+                if active_item.can_be_followed(cx) {
+                    Some(active_item.id() as u64)
+                } else {
+                    None
+                }
+            } else {
+                None
+            };
+            Ok(proto::FollowResponse {
+                current_view_id,
+                views: this
+                    .items(cx)
+                    .filter_map(|item| {
+                        let variant = item.to_state_message(cx)?;
+                        Some(proto::View {
+                            id: item.id() as u64,
+                            variant: Some(variant),
+                        })
+                    })
+                    .collect(),
+            })
         })
     }