Set up logic for starting following

Max Brunsfeld and Antonio Scandurra created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

crates/rpc/proto/zed.proto        |  22 ++++-
crates/rpc/src/proto.rs           |   8 ++
crates/workspace/src/pane.rs      |  24 ++++++
crates/workspace/src/workspace.rs | 116 ++++++++++++++++++++++++++------
4 files changed, 142 insertions(+), 28 deletions(-)

Detailed changes

crates/rpc/proto/zed.proto 🔗

@@ -83,7 +83,8 @@ message Envelope {
 
         Follow follow = 72;
         FollowResponse follow_response = 73;
-        UpdateFollower update_follower = 74;
+        UpdateFollowers update_followers = 74;
+        Unfollow unfollow = 75;
     }
 }
 
@@ -537,16 +538,27 @@ message UpdateDiagnostics {
     repeated Diagnostic diagnostics = 3;
 }
 
-message Follow {}
+message Follow {
+    uint64 project_id = 1;
+    uint32 leader_id = 2;
+}
 
 message FollowResponse {
     uint64 current_view_id = 1;
     repeated View views = 2;
 }
 
-message UpdateFollower {
-    uint64 current_view_id = 1;
-    repeated ViewUpdate view_updates = 2;
+message UpdateFollowers {
+    uint64 project_id = 1;
+    uint64 current_view_id = 2;
+    repeated View created_views = 3;
+    repeated ViewUpdate updated_views = 4;
+    repeated uint32 follower_ids = 5;
+}
+
+message Unfollow {
+    uint64 project_id = 1;
+    uint32 leader_id = 2;
 }
 
 // Entities

crates/rpc/src/proto.rs 🔗

@@ -147,6 +147,8 @@ messages!(
     (BufferSaved, Foreground),
     (ChannelMessageSent, Foreground),
     (Error, Foreground),
+    (Follow, Foreground),
+    (FollowResponse, Foreground),
     (FormatBuffers, Foreground),
     (FormatBuffersResponse, Foreground),
     (GetChannelMessages, Foreground),
@@ -196,6 +198,7 @@ messages!(
     (SendChannelMessageResponse, Foreground),
     (ShareProject, Foreground),
     (Test, Foreground),
+    (Unfollow, Foreground),
     (UnregisterProject, Foreground),
     (UnregisterWorktree, Foreground),
     (UnshareProject, Foreground),
@@ -203,6 +206,7 @@ messages!(
     (UpdateBufferFile, Foreground),
     (UpdateContacts, Foreground),
     (UpdateDiagnosticSummary, Foreground),
+    (UpdateFollowers, Foreground),
     (UpdateWorktree, Foreground),
 );
 
@@ -212,6 +216,7 @@ request_messages!(
         ApplyCompletionAdditionalEdits,
         ApplyCompletionAdditionalEditsResponse
     ),
+    (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannels, GetChannelsResponse),
@@ -248,6 +253,7 @@ entity_messages!(
     ApplyCompletionAdditionalEdits,
     BufferReloaded,
     BufferSaved,
+    Follow,
     FormatBuffers,
     GetCodeActions,
     GetCompletions,
@@ -266,11 +272,13 @@ entity_messages!(
     SaveBuffer,
     SearchProject,
     StartLanguageServer,
+    Unfollow,
     UnregisterWorktree,
     UnshareProject,
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
+    UpdateFollowers,
     UpdateLanguageServer,
     RegisterWorktree,
     UpdateWorktree,

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,7 @@
 use super::{ItemHandle, SplitDirection};
 use crate::{Item, Settings, WeakItemHandle, Workspace};
+use anyhow::{anyhow, Result};
+use client::PeerId;
 use collections::{HashMap, VecDeque};
 use gpui::{
     action,
@@ -105,6 +107,13 @@ pub struct Pane {
     active_toolbar_visible: bool,
 }
 
+pub(crate) struct FollowerState {
+    pub(crate) leader_id: PeerId,
+    pub(crate) current_view_id: usize,
+    pub(crate) items_by_leader_view_id:
+        HashMap<usize, (Option<ProjectEntryId>, Box<dyn ItemHandle>)>,
+}
+
 pub trait Toolbar: View {
     fn active_item_changed(
         &mut self,
@@ -313,6 +322,21 @@ impl Pane {
         cx.notify();
     }
 
+    pub(crate) fn set_follow_state(
+        &mut self,
+        follower_state: FollowerState,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        let current_view_id = follower_state.current_view_id as usize;
+        let (project_entry_id, item) = follower_state
+            .items_by_leader_view_id
+            .get(&current_view_id)
+            .ok_or_else(|| anyhow!("invalid current view id"))?
+            .clone();
+        self.add_item(project_entry_id, item, cx);
+        Ok(())
+    }
+
     pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
         self.items.iter().map(|(_, view)| view)
     }

crates/workspace/src/workspace.rs 🔗

@@ -7,7 +7,7 @@ pub mod sidebar;
 mod status_bar;
 
 use anyhow::{anyhow, Result};
-use client::{Authenticate, ChannelList, Client, User, UserStore};
+use client::{proto, Authenticate, ChannelList, Client, PeerId, User, UserStore};
 use clock::ReplicaId;
 use collections::HashMap;
 use gpui::{
@@ -42,16 +42,18 @@ use std::{
 };
 use theme::{Theme, ThemeRegistry};
 
-type ItemBuilders = HashMap<
+type ProjectItemBuilders = HashMap<
     TypeId,
-    Arc<
-        dyn Fn(
-            usize,
-            ModelHandle<Project>,
-            AnyModelHandle,
-            &mut MutableAppContext,
-        ) -> Box<dyn ItemHandle>,
-    >,
+    fn(usize, ModelHandle<Project>, AnyModelHandle, &mut MutableAppContext) -> Box<dyn ItemHandle>,
+>;
+
+type FollowedItemBuilders = Vec<
+    fn(
+        ViewHandle<Pane>,
+        ModelHandle<Project>,
+        &mut Option<proto::view::Variant>,
+        &mut MutableAppContext,
+    ) -> Option<Task<Result<(Option<ProjectEntryId>, Box<dyn ItemHandle>)>>>,
 >;
 
 action!(Open, Arc<AppState>);
@@ -108,18 +110,18 @@ pub fn init(cx: &mut MutableAppContext) {
     ]);
 }
 
-pub fn register_project_item<V>(cx: &mut MutableAppContext)
-where
-    V: ProjectItem,
-{
-    cx.update_default_global(|builders: &mut ItemBuilders, _| {
-        builders.insert(
-            TypeId::of::<V::Item>(),
-            Arc::new(move |window_id, project, model, cx| {
-                let item = model.downcast::<V::Item>().unwrap();
-                Box::new(cx.add_view(window_id, |cx| V::for_project_item(project, item, cx)))
-            }),
-        );
+pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
+    cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
+        builders.insert(TypeId::of::<I::Item>(), |window_id, project, model, cx| {
+            let item = model.downcast::<I::Item>().unwrap();
+            Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx)))
+        });
+    });
+}
+
+pub fn register_followed_item<I: FollowedItem>(cx: &mut MutableAppContext) {
+    cx.update_default_global(|builders: &mut FollowedItemBuilders, _| {
+        builders.push(I::for_state_message)
     });
 }
 
@@ -214,6 +216,17 @@ pub trait ProjectItem: Item {
     ) -> Self;
 }
 
+pub trait FollowedItem: Item {
+    type UpdateMessage;
+
+    fn for_state_message(
+        pane: ViewHandle<Pane>,
+        project: ModelHandle<Project>,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<(Option<ProjectEntryId>, Box<dyn ItemHandle>)>>>;
+}
+
 pub trait ItemHandle: 'static {
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -840,7 +853,7 @@ impl Workspace {
         cx.as_mut().spawn(|mut cx| async move {
             let (project_entry_id, project_item) = project_item.await?;
             let build_item = cx.update(|cx| {
-                cx.default_global::<ItemBuilders>()
+                cx.default_global::<ProjectItemBuilders>()
                     .get(&project_item.model_type())
                     .ok_or_else(|| anyhow!("no item builder for project item"))
                     .cloned()
@@ -990,6 +1003,63 @@ impl Workspace {
         });
     }
 
+    pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+        if let Some(project_id) = self.project.read(cx).remote_id() {
+            let request = self.client.request(proto::Follow {
+                project_id,
+                leader_id: leader_id.0,
+            });
+            cx.spawn_weak(|this, mut cx| async move {
+                let mut response = request.await?;
+                if let Some(this) = this.upgrade(&cx) {
+                    let mut item_tasks = Vec::new();
+                    let (project, pane) = this.read_with(&cx, |this, _| {
+                        (this.project.clone(), this.active_pane().clone())
+                    });
+                    for view in &mut response.views {
+                        let variant = view
+                            .variant
+                            .take()
+                            .ok_or_else(|| anyhow!("missing variant"))?;
+                        cx.update(|cx| {
+                            let mut variant = Some(variant);
+                            for build_item in cx.default_global::<FollowedItemBuilders>().clone() {
+                                if let Some(task) =
+                                    build_item(pane.clone(), project.clone(), &mut variant, cx)
+                                {
+                                    item_tasks.push(task);
+                                    break;
+                                } else {
+                                    assert!(variant.is_some());
+                                }
+                            }
+                        });
+                    }
+
+                    let items = futures::future::try_join_all(item_tasks).await?;
+                    let mut items_by_leader_view_id = HashMap::default();
+                    for (view, item) in response.views.into_iter().zip(items) {
+                        items_by_leader_view_id.insert(view.id as usize, item);
+                    }
+
+                    pane.update(&mut cx, |pane, cx| {
+                        pane.set_follow_state(
+                            FollowerState {
+                                leader_id,
+                                current_view_id: response.current_view_id as usize,
+                                items_by_leader_view_id,
+                            },
+                            cx,
+                        )
+                    })?;
+                }
+                Ok(())
+            })
+        } else {
+            Task::ready(Err(anyhow!("project is not remote")))
+        }
+    }
+
     fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
         let theme = &cx.global::<Settings>().theme;
         match &*self.client.status().borrow() {