Send worktree info only when sharing worktree

Max Brunsfeld , Antonio Scandurra , and Nathan Sobo created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/contacts_panel/src/contacts_panel.rs | 107 ++++------------
crates/diagnostics/src/diagnostics.rs       |   2 
crates/gpui/src/executor.rs                 |   8 +
crates/project/src/project.rs               |  88 ++++++++-----
crates/project/src/worktree.rs              | 146 ++++++++++++----------
crates/project_panel/src/project_panel.rs   |   5 
crates/rpc/proto/zed.proto                  |   7 
crates/rpc/src/proto.rs                     |   3 
crates/theme/src/theme.rs                   |   8 
crates/workspace/src/workspace.rs           |  34 ++--
10 files changed, 200 insertions(+), 208 deletions(-)

Detailed changes

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -11,16 +11,10 @@ use postage::watch;
 use theme::Theme;
 use workspace::{Settings, Workspace};
 
-action!(JoinWorktree, u64);
-action!(LeaveWorktree, u64);
-action!(ShareWorktree, u64);
-action!(UnshareWorktree, u64);
+action!(JoinProject, u64);
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(ContactsPanel::share_worktree);
-    cx.add_action(ContactsPanel::unshare_worktree);
-    cx.add_action(ContactsPanel::join_worktree);
-    cx.add_action(ContactsPanel::leave_worktree);
+    cx.add_action(ContactsPanel::join_project);
 }
 
 pub struct ContactsPanel {
@@ -63,44 +57,8 @@ impl ContactsPanel {
         }
     }
 
-    fn share_worktree(
-        workspace: &mut Workspace,
-        action: &ShareWorktree,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        workspace
-            .project()
-            .update(cx, |p, cx| p.share_worktree(action.0, cx));
-    }
-
-    fn unshare_worktree(
-        workspace: &mut Workspace,
-        action: &UnshareWorktree,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        workspace
-            .project()
-            .update(cx, |p, cx| p.unshare_worktree(action.0, cx));
-    }
-
-    fn join_worktree(
-        workspace: &mut Workspace,
-        action: &JoinWorktree,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        workspace
-            .project()
-            .update(cx, |p, cx| p.add_remote_worktree(action.0, cx).detach());
-    }
-
-    fn leave_worktree(
-        workspace: &mut Workspace,
-        action: &LeaveWorktree,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        workspace
-            .project()
-            .update(cx, |p, cx| p.close_remote_worktree(action.0, cx));
+    fn join_project(_: &mut Workspace, _: &JoinProject, _: &mut ViewContext<Workspace>) {
+        todo!();
     }
 
     fn update_contacts(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
@@ -116,16 +74,12 @@ impl ContactsPanel {
         cx: &mut LayoutContext,
     ) -> ElementBox {
         let theme = &theme.contacts_panel;
-        let worktree_count = collaborator.worktrees.len();
+        let project_count = collaborator.projects.len();
         let font_cache = cx.font_cache();
-        let line_height = theme.unshared_worktree.name.text.line_height(font_cache);
-        let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache);
-        let baseline_offset = theme
-            .unshared_worktree
-            .name
-            .text
-            .baseline_offset(font_cache)
-            + (theme.unshared_worktree.height - line_height) / 2.;
+        let line_height = theme.unshared_project.name.text.line_height(font_cache);
+        let cap_height = theme.unshared_project.name.text.cap_height(font_cache);
+        let baseline_offset = theme.unshared_project.name.text.baseline_offset(font_cache)
+            + (theme.unshared_project.height - line_height) / 2.;
         let tree_branch_width = theme.tree_branch_width;
         let tree_branch_color = theme.tree_branch_color;
         let host_avatar_height = theme
@@ -161,11 +115,11 @@ impl ContactsPanel {
             )
             .with_children(
                 collaborator
-                    .worktrees
+                    .projects
                     .iter()
                     .enumerate()
-                    .map(|(ix, worktree)| {
-                        let worktree_id = worktree.id;
+                    .map(|(ix, project)| {
+                        let project_id = project.id;
 
                         Flex::row()
                             .with_child(
@@ -182,7 +136,7 @@ impl ContactsPanel {
                                             vec2f(start_x, start_y),
                                             vec2f(
                                                 start_x + tree_branch_width,
-                                                if ix + 1 == worktree_count {
+                                                if ix + 1 == project_count {
                                                     end_y
                                                 } else {
                                                     bounds.max_y()
@@ -210,28 +164,27 @@ impl ContactsPanel {
                             .with_child({
                                 let is_host = Some(collaborator.user.id) == current_user_id;
                                 let is_guest = !is_host
-                                    && worktree
+                                    && project
                                         .guests
                                         .iter()
                                         .any(|guest| Some(guest.id) == current_user_id);
-                                let is_shared = worktree.is_shared;
+                                let is_shared = project.is_shared;
 
                                 MouseEventHandler::new::<ContactsPanel, _, _, _>(
-                                    worktree_id as usize,
+                                    project_id as usize,
                                     cx,
                                     |mouse_state, _| {
-                                        let style = match (worktree.is_shared, mouse_state.hovered)
-                                        {
-                                            (false, false) => &theme.unshared_worktree,
-                                            (false, true) => &theme.hovered_unshared_worktree,
-                                            (true, false) => &theme.shared_worktree,
-                                            (true, true) => &theme.hovered_shared_worktree,
+                                        let style = match (project.is_shared, mouse_state.hovered) {
+                                            (false, false) => &theme.unshared_project,
+                                            (false, true) => &theme.hovered_unshared_project,
+                                            (true, false) => &theme.shared_project,
+                                            (true, true) => &theme.hovered_shared_project,
                                         };
 
                                         Flex::row()
                                             .with_child(
                                                 Label::new(
-                                                    worktree.root_name.clone(),
+                                                    project.worktree_root_names.join(", "),
                                                     style.name.text.clone(),
                                                 )
                                                 .aligned()
@@ -240,7 +193,7 @@ impl ContactsPanel {
                                                 .with_style(style.name.container)
                                                 .boxed(),
                                             )
-                                            .with_children(worktree.guests.iter().filter_map(
+                                            .with_children(project.guests.iter().filter_map(
                                                 |participant| {
                                                     participant.avatar.clone().map(|avatar| {
                                                         Image::new(avatar)
@@ -268,23 +221,15 @@ impl ContactsPanel {
                                     CursorStyle::Arrow
                                 })
                                 .on_click(move |cx| {
-                                    if is_shared {
-                                        if is_host {
-                                            cx.dispatch_action(UnshareWorktree(worktree_id));
-                                        } else if is_guest {
-                                            cx.dispatch_action(LeaveWorktree(worktree_id));
-                                        } else {
-                                            cx.dispatch_action(JoinWorktree(worktree_id))
-                                        }
-                                    } else if is_host {
-                                        cx.dispatch_action(ShareWorktree(worktree_id));
+                                    if !is_host && !is_guest {
+                                        cx.dispatch_action(JoinProject(project_id))
                                     }
                                 })
                                 .expanded(1.0)
                                 .boxed()
                             })
                             .constrained()
-                            .with_height(theme.unshared_worktree.height)
+                            .with_height(theme.unshared_project.height)
                             .boxed()
                     }),
             )

crates/diagnostics/src/diagnostics.rs 🔗

@@ -213,7 +213,7 @@ impl workspace::Item for ProjectDiagnostics {
         })
         .detach();
 
-        ProjectDiagnosticsEditor::new(project.read(cx).replica_id(cx), settings, cx)
+        ProjectDiagnosticsEditor::new(project.read(cx).replica_id(), settings, cx)
     }
 
     fn project_path(&self) -> Option<project::ProjectPath> {

crates/gpui/src/executor.rs 🔗

@@ -54,6 +54,7 @@ type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
 
 #[must_use]
 pub enum Task<T> {
+    Ready(Option<T>),
     Local {
         any_task: AnyLocalTask,
         result_type: PhantomData<T>,
@@ -594,6 +595,10 @@ pub fn deterministic(seed: u64) -> (Rc<Foreground>, Arc<Background>) {
 }
 
 impl<T> Task<T> {
+    pub fn ready(value: T) -> Self {
+        Self::Ready(Some(value))
+    }
+
     fn local(any_task: AnyLocalTask) -> Self {
         Self::Local {
             any_task,
@@ -603,6 +608,7 @@ impl<T> Task<T> {
 
     pub fn detach(self) {
         match self {
+            Task::Ready(_) => {}
             Task::Local { any_task, .. } => any_task.detach(),
             Task::Send { any_task, .. } => any_task.detach(),
         }
@@ -621,6 +627,7 @@ impl<T: Send> Task<T> {
 impl<T: fmt::Debug> fmt::Debug for Task<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
+            Task::Ready(value) => value.fmt(f),
             Task::Local { any_task, .. } => any_task.fmt(f),
             Task::Send { any_task, .. } => any_task.fmt(f),
         }
@@ -632,6 +639,7 @@ impl<T: 'static> Future for Task<T> {
 
     fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
         match unsafe { self.get_unchecked_mut() } {
+            Task::Ready(value) => Poll::Ready(value.take().unwrap()),
             Task::Local { any_task, .. } => {
                 any_task.poll(cx).map(|value| *value.downcast().unwrap())
             }

crates/project/src/project.rs 🔗

@@ -213,7 +213,8 @@ impl Project {
             subscriptions: vec![
                 client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
                 client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
-                client.subscribe_to_entity(remote_id, cx, Self::handle_register_worktree),
+                client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree),
+                client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree),
                 client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
                 client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
                 client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
@@ -231,14 +232,6 @@ impl Project {
             *remote_id_tx.borrow_mut() = remote_id;
         }
 
-        for worktree in &self.worktrees {
-            worktree.update(cx, |worktree, _| {
-                if let Some(worktree) = worktree.as_local_mut() {
-                    worktree.set_project_remote_id(remote_id);
-                }
-            });
-        }
-
         self.subscriptions.clear();
         if let Some(remote_id) = remote_id {
             self.subscriptions.extend([
@@ -307,7 +300,11 @@ impl Project {
             this.update(&mut cx, |this, cx| {
                 for worktree in &this.worktrees {
                     worktree.update(cx, |worktree, cx| {
-                        worktree.as_local_mut().unwrap().share(cx).detach();
+                        worktree
+                            .as_local_mut()
+                            .unwrap()
+                            .share(remote_id, cx)
+                            .detach();
                     });
                 }
             });
@@ -327,6 +324,13 @@ impl Project {
         }
     }
 
+    fn is_shared(&self) -> bool {
+        match &self.client_state {
+            ProjectClientState::Local { is_shared, .. } => *is_shared,
+            ProjectClientState::Remote { .. } => false,
+        }
+    }
+
     pub fn add_local_worktree(
         &mut self,
         abs_path: &Path,
@@ -337,32 +341,35 @@ impl Project {
         let user_store = self.user_store.clone();
         let languages = self.languages.clone();
         let path = Arc::from(abs_path);
-        cx.spawn(|this, mut cx| async move {
+        cx.spawn(|project, mut cx| async move {
             let worktree =
                 Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx)
                     .await?;
-            this.update(&mut cx, |this, cx| {
-                if let Some(project_id) = this.remote_id() {
-                    worktree.update(cx, |worktree, cx| {
-                        let worktree = worktree.as_local_mut().unwrap();
-                        worktree.set_project_remote_id(Some(project_id));
-                        let serialized_worktree = worktree.to_proto(cx);
-                        let authorized_logins = worktree.authorized_logins();
-                        cx.foreground()
-                            .spawn(async move {
-                                client
-                                    .request(proto::RegisterWorktree {
-                                        project_id,
-                                        worktree: Some(serialized_worktree),
-                                        authorized_logins,
-                                    })
-                                    .log_err();
-                            })
-                            .detach();
-                    });
-                }
-                this.add_worktree(worktree.clone(), cx);
+
+            let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| {
+                project.add_worktree(worktree.clone(), cx);
+                (project.remote_id(), project.is_shared())
             });
+
+            if let Some(project_id) = remote_project_id {
+                let register_message = worktree.update(&mut cx, |worktree, _| {
+                    let worktree = worktree.as_local_mut().unwrap();
+                    proto::RegisterWorktree {
+                        project_id,
+                        root_name: worktree.root_name().to_string(),
+                        authorized_logins: worktree.authorized_logins(),
+                    }
+                });
+                client.request(register_message).await?;
+                if is_shared {
+                    worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_local_mut().unwrap().share(project_id, cx)
+                        })
+                        .await?;
+                }
+            }
+
             Ok(worktree)
         })
     }
@@ -476,9 +483,9 @@ impl Project {
         Ok(())
     }
 
-    fn handle_register_worktree(
+    fn handle_share_worktree(
         &mut self,
-        envelope: TypedEnvelope<proto::RegisterWorktree>,
+        envelope: TypedEnvelope<proto::ShareWorktree>,
         client: Arc<Client>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
@@ -505,6 +512,19 @@ impl Project {
         Ok(())
     }
 
+    fn handle_unregister_worktree(
+        &mut self,
+        envelope: TypedEnvelope<proto::UnregisterWorktree>,
+        _: Arc<Client>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        self.worktrees.retain(|worktree| {
+            worktree.read(cx).as_remote().unwrap().remote_id() != envelope.payload.worktree_id
+        });
+        cx.notify();
+        Ok(())
+    }
+
     fn handle_update_worktree(
         &mut self,
         envelope: TypedEnvelope<proto::UpdateWorktree>,

crates/project/src/worktree.rs 🔗

@@ -208,7 +208,7 @@ impl Worktree {
                 }
 
                 Worktree::Remote(RemoteWorktree {
-                    project_remote_id,
+                    project_id: project_remote_id,
                     remote_id,
                     replica_id,
                     snapshot,
@@ -236,6 +236,14 @@ impl Worktree {
         }
     }
 
+    pub fn as_remote(&self) -> Option<&RemoteWorktree> {
+        if let Worktree::Remote(worktree) = self {
+            Some(worktree)
+        } else {
+            None
+        }
+    }
+
     pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> {
         if let Worktree::Local(worktree) = self {
             Some(worktree)
@@ -483,8 +491,10 @@ impl Worktree {
         let sender_id = envelope.original_sender_id()?;
         let this = self.as_local().unwrap();
         let project_id = this
-            .project_remote_id
-            .ok_or_else(|| anyhow!("can't save buffer while disconnected"))?;
+            .share
+            .as_ref()
+            .ok_or_else(|| anyhow!("can't save buffer while disconnected"))?
+            .project_id;
 
         let buffer = this
             .shared_buffers
@@ -756,13 +766,12 @@ impl Worktree {
         operation: Operation,
         cx: &mut ModelContext<Self>,
     ) {
-        if let Some((rpc, project_id)) = match self {
+        if let Some((project_id, rpc)) = match self {
             Worktree::Local(worktree) => worktree
-                .project_remote_id
-                .map(|id| (worktree.client.clone(), id)),
-            Worktree::Remote(worktree) => {
-                Some((worktree.client.clone(), worktree.project_remote_id))
-            }
+                .share
+                .as_ref()
+                .map(|share| (share.project_id, worktree.client.clone())),
+            Worktree::Remote(worktree) => Some((worktree.project_id, worktree.client.clone())),
         } {
             cx.spawn(|worktree, mut cx| async move {
                 if let Err(error) = rpc
@@ -809,7 +818,6 @@ pub struct LocalWorktree {
     background_snapshot: Arc<Mutex<Snapshot>>,
     last_scan_state_rx: watch::Receiver<ScanState>,
     _background_scanner_task: Option<Task<()>>,
-    project_remote_id: Option<u64>,
     poll_task: Option<Task<()>>,
     share: Option<ShareState>,
     loading_buffers: LoadingBuffers,
@@ -826,11 +834,12 @@ pub struct LocalWorktree {
 }
 
 struct ShareState {
+    project_id: u64,
     snapshots_tx: Sender<Snapshot>,
 }
 
 pub struct RemoteWorktree {
-    project_remote_id: u64,
+    project_id: u64,
     remote_id: u64,
     snapshot: Snapshot,
     snapshot_rx: watch::Receiver<Snapshot>,
@@ -913,7 +922,6 @@ impl LocalWorktree {
             let tree = Self {
                 snapshot: snapshot.clone(),
                 config,
-                project_remote_id: None,
                 background_snapshot: Arc::new(Mutex::new(snapshot)),
                 last_scan_state_rx,
                 _background_scanner_task: None,
@@ -965,10 +973,6 @@ impl LocalWorktree {
         Ok((tree, scan_states_tx))
     }
 
-    pub fn set_project_remote_id(&mut self, id: Option<u64>) {
-        self.project_remote_id = id;
-    }
-
     pub fn authorized_logins(&self) -> Vec<String> {
         self.config.collaborators.clone()
     }
@@ -1244,63 +1248,54 @@ impl LocalWorktree {
         })
     }
 
-    pub fn share(&mut self, cx: &mut ModelContext<Worktree>) -> Task<anyhow::Result<()>> {
+    pub fn share(
+        &mut self,
+        project_id: u64,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<anyhow::Result<()>> {
+        if self.share.is_some() {
+            return Task::ready(Ok(()));
+        }
+
         let snapshot = self.snapshot();
         let rpc = self.client.clone();
-        let project_id = self.project_remote_id;
         let worktree_id = cx.model_id() as u64;
-        cx.spawn(|this, mut cx| async move {
-            let project_id = project_id.ok_or_else(|| anyhow!("no project id"))?;
+        let (snapshots_to_send_tx, snapshots_to_send_rx) = smol::channel::unbounded::<Snapshot>();
+        self.share = Some(ShareState {
+            project_id,
+            snapshots_tx: snapshots_to_send_tx,
+        });
 
-            let (snapshots_to_send_tx, snapshots_to_send_rx) =
-                smol::channel::unbounded::<Snapshot>();
-            cx.background()
-                .spawn({
-                    let rpc = rpc.clone();
-                    async move {
-                        let mut prev_snapshot = snapshot;
-                        while let Ok(snapshot) = snapshots_to_send_rx.recv().await {
-                            let message = snapshot.build_update(
-                                &prev_snapshot,
-                                project_id,
-                                worktree_id,
-                                false,
-                            );
-                            match rpc.send(message).await {
-                                Ok(()) => prev_snapshot = snapshot,
-                                Err(err) => log::error!("error sending snapshot diff {}", err),
-                            }
+        cx.background()
+            .spawn({
+                let rpc = rpc.clone();
+                let snapshot = snapshot.clone();
+                async move {
+                    let mut prev_snapshot = snapshot;
+                    while let Ok(snapshot) = snapshots_to_send_rx.recv().await {
+                        let message =
+                            snapshot.build_update(&prev_snapshot, project_id, worktree_id, false);
+                        match rpc.send(message).await {
+                            Ok(()) => prev_snapshot = snapshot,
+                            Err(err) => log::error!("error sending snapshot diff {}", err),
                         }
                     }
-                })
-                .detach();
+                }
+            })
+            .detach();
 
-            this.update(&mut cx, |worktree, _| {
-                let worktree = worktree.as_local_mut().unwrap();
-                worktree.share = Some(ShareState {
-                    snapshots_tx: snapshots_to_send_tx,
-                });
-            });
+        let share_message = cx.background().spawn(async move {
+            proto::ShareWorktree {
+                project_id,
+                worktree: Some(snapshot.to_proto()),
+            }
+        });
 
+        cx.foreground().spawn(async move {
+            rpc.request(share_message.await).await?;
             Ok(())
         })
     }
-
-    pub fn to_proto(&self, cx: &mut ModelContext<Worktree>) -> proto::Worktree {
-        let id = cx.model_id() as u64;
-        let snapshot = self.snapshot();
-        let root_name = self.root_name.clone();
-        proto::Worktree {
-            id,
-            root_name,
-            entries: snapshot
-                .entries_by_path
-                .cursor::<()>()
-                .filter(|e| !e.is_ignored)
-                .map(Into::into)
-                .collect(),
-        }
-    }
 }
 
 fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
@@ -1339,6 +1334,10 @@ impl fmt::Debug for LocalWorktree {
 }
 
 impl RemoteWorktree {
+    pub fn remote_id(&self) -> u64 {
+        self.remote_id
+    }
+
     fn get_open_buffer(
         &mut self,
         path: &Path,
@@ -1368,7 +1367,7 @@ impl RemoteWorktree {
     ) -> Task<Result<ModelHandle<Buffer>>> {
         let rpc = self.client.clone();
         let replica_id = self.replica_id;
-        let project_id = self.project_remote_id;
+        let project_id = self.project_id;
         let remote_worktree_id = self.remote_id;
         let root_path = self.snapshot.abs_path.clone();
         let path: Arc<Path> = Arc::from(path);
@@ -1481,6 +1480,20 @@ impl Snapshot {
         self.id
     }
 
+    pub fn to_proto(&self) -> proto::Worktree {
+        let root_name = self.root_name.clone();
+        proto::Worktree {
+            id: self.id as u64,
+            root_name,
+            entries: self
+                .entries_by_path
+                .cursor::<()>()
+                .filter(|e| !e.is_ignored)
+                .map(Into::into)
+                .collect(),
+        }
+    }
+
     pub fn build_update(
         &self,
         other: &Self,
@@ -1540,6 +1553,7 @@ impl Snapshot {
         proto::UpdateWorktree {
             project_id,
             worktree_id,
+            root_name: self.root_name().to_string(),
             updated_entries,
             removed_entries,
         }
@@ -1902,7 +1916,7 @@ impl language::File for File {
         self.worktree.update(cx, |worktree, cx| match worktree {
             Worktree::Local(worktree) => {
                 let rpc = worktree.client.clone();
-                let project_id = worktree.project_remote_id;
+                let project_id = worktree.share.as_ref().map(|share| share.project_id);
                 let save = worktree.save(self.path.clone(), text, cx);
                 cx.background().spawn(async move {
                     let entry = save.await?;
@@ -1921,7 +1935,7 @@ impl language::File for File {
             }
             Worktree::Remote(worktree) => {
                 let rpc = worktree.client.clone();
-                let project_id = worktree.project_remote_id;
+                let project_id = worktree.project_id;
                 cx.foreground().spawn(async move {
                     let response = rpc
                         .request(proto::SaveBuffer {
@@ -1961,7 +1975,7 @@ impl language::File for File {
         let worktree_id = self.worktree.id() as u64;
         self.worktree.update(cx, |worktree, cx| {
             if let Worktree::Remote(worktree) = worktree {
-                let project_id = worktree.project_remote_id;
+                let project_id = worktree.project_id;
                 let rpc = worktree.client.clone();
                 cx.background()
                     .spawn(async move {

crates/project_panel/src/project_panel.rs 🔗

@@ -617,12 +617,13 @@ mod tests {
         )
         .await;
 
-        let project = cx.add_model(|_| {
-            Project::new(
+        let project = cx.add_model(|cx| {
+            Project::local(
                 params.languages.clone(),
                 params.client.clone(),
                 params.user_store.clone(),
                 params.fs.clone(),
+                cx,
             )
         });
         let root1 = project

crates/rpc/proto/zed.proto 🔗

@@ -96,7 +96,7 @@ message LeaveProject {
 
 message RegisterWorktree {
     uint64 project_id = 1;
-    Worktree worktree = 2;
+    string root_name = 2;
     repeated string authorized_logins = 3;
 }
 
@@ -113,8 +113,9 @@ message ShareWorktree {
 message UpdateWorktree {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
-    repeated Entry updated_entries = 3;
-    repeated uint64 removed_entries = 4;
+    string root_name = 3;
+    repeated Entry updated_entries = 4;
+    repeated uint64 removed_entries = 5;
 }
 
 message AddProjectCollaborator {

crates/rpc/src/proto.rs 🔗

@@ -152,6 +152,7 @@ messages!(
     ShareWorktree,
     UnregisterProject,
     UnregisterWorktree,
+    UnshareProject,
     UpdateBuffer,
     UpdateContacts,
     UpdateWorktree,
@@ -183,7 +184,7 @@ entity_messages!(
     OpenBuffer,
     CloseBuffer,
     SaveBuffer,
-    RegisterWorktree,
+    ShareWorktree,
     UnregisterWorktree,
     UpdateBuffer,
     UpdateWorktree,

crates/theme/src/theme.rs 🔗

@@ -155,10 +155,10 @@ pub struct ContactsPanel {
     pub host_username: ContainedText,
     pub tree_branch_width: f32,
     pub tree_branch_color: Color,
-    pub shared_worktree: WorktreeRow,
-    pub hovered_shared_worktree: WorktreeRow,
-    pub unshared_worktree: WorktreeRow,
-    pub hovered_unshared_worktree: WorktreeRow,
+    pub shared_project: WorktreeRow,
+    pub hovered_shared_project: WorktreeRow,
+    pub unshared_project: WorktreeRow,
+    pub hovered_unshared_project: WorktreeRow,
 }
 
 #[derive(Deserialize, Default)]

crates/workspace/src/workspace.rs 🔗

@@ -358,12 +358,13 @@ pub struct Workspace {
 
 impl Workspace {
     pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
-        let project = cx.add_model(|_| {
-            Project::new(
+        let project = cx.add_model(|cx| {
+            Project::local(
                 params.languages.clone(),
                 params.client.clone(),
                 params.user_store.clone(),
                 params.fs.clone(),
+                cx,
             )
         });
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -988,24 +989,25 @@ impl Workspace {
     }
 
     fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
-        let mut elements = Vec::new();
-        if let Some(active_worktree) = self.project.read(cx).active_worktree() {
-            let collaborators = active_worktree
-                .read(cx)
-                .collaborators()
-                .values()
-                .cloned()
-                .collect::<Vec<_>>();
-            for collaborator in collaborators {
-                elements.push(self.render_avatar(
+        let mut collaborators = self
+            .project
+            .read(cx)
+            .collaborators()
+            .values()
+            .cloned()
+            .collect::<Vec<_>>();
+        collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
+        collaborators
+            .into_iter()
+            .map(|collaborator| {
+                self.render_avatar(
                     Some(&collaborator.user),
                     Some(collaborator.replica_id),
                     theme,
                     cx,
-                ));
-            }
-        }
-        elements
+                )
+            })
+            .collect()
     }
 
     fn render_avatar(