Remove `JoinProject` internal action

Antonio Scandurra created

Change summary

crates/collab_ui/src/collab_titlebar_item.rs        |  30 -
crates/collab_ui/src/collab_ui.rs                   |  97 ------
crates/collab_ui/src/contact_list.rs                |  42 +-
crates/collab_ui/src/contacts_popover.rs            |  40 +-
crates/collab_ui/src/incoming_call_notification.rs  |  33 +
crates/collab_ui/src/project_shared_notification.rs |  46 +-
crates/workspace/src/dock.rs                        |  34 +
crates/workspace/src/item.rs                        |   4 
crates/workspace/src/pane_group.rs                  |  34 +
crates/workspace/src/workspace.rs                   | 224 +++++++++-----
crates/zed/src/main.rs                              |   2 
crates/zed/src/zed.rs                               |   3 
12 files changed, 302 insertions(+), 287 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -23,7 +23,7 @@ use settings::Settings;
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
-use workspace::{FollowNextCollaborator, JoinProject, Workspace};
+use workspace::{FollowNextCollaborator, Workspace};
 
 actions!(
     collab,
@@ -134,21 +134,18 @@ impl View for CollabTitlebarItem {
 }
 
 impl CollabTitlebarItem {
-    pub fn new(
-        workspace: &ViewHandle<Workspace>,
-        user_store: &ModelHandle<UserStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
         let active_call = ActiveCall::global(cx);
+        let user_store = workspace.read(cx).user_store().clone();
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
         subscriptions.push(cx.observe_window_activation(|this, active, cx| {
             this.window_activation_changed(active, cx)
         }));
-        subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
+        subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
         subscriptions.push(
-            cx.subscribe(user_store, move |this, user_store, event, cx| {
+            cx.subscribe(&user_store, move |this, user_store, event, cx| {
                 if let Some(workspace) = this.workspace.upgrade(cx) {
                     workspace.update(cx, |workspace, cx| {
                         if let client::Event::Contact { user, kind } = event {
@@ -257,9 +254,7 @@ impl CollabTitlebarItem {
     pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
         if self.contacts_popover.take().is_none() {
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                let project = workspace.read(cx).project().clone();
-                let user_store = workspace.read(cx).user_store().clone();
-                let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+                let view = cx.add_view(|cx| ContactsPopover::new(&workspace, cx));
                 cx.subscribe(&view, |this, _, event, cx| {
                     match event {
                         contacts_popover::Event::Dismissed => {
@@ -776,6 +771,8 @@ impl CollabTitlebarItem {
                 )
                 .into_any();
             } else if let ParticipantLocation::SharedProject { project_id } = location {
+                enum JoinProject {}
+
                 let user_id = user.id;
                 content = MouseEventHandler::<JoinProject, Self>::new(
                     peer_id.as_u64() as usize,
@@ -783,11 +780,12 @@ impl CollabTitlebarItem {
                     move |_, _| content,
                 )
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(JoinProject {
-                        project_id,
-                        follow_user_id: user_id,
-                    })
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                        let app_state = workspace.read(cx).app_state().clone();
+                        workspace::join_remote_project(project_id, user_id, app_state, cx)
+                            .detach_and_log_err(cx);
+                    }
                 })
                 .with_tooltip::<JoinProject>(
                     peer_id.as_u64() as usize,

crates/collab_ui/src/collab_ui.rs 🔗

@@ -10,29 +10,25 @@ mod notifications;
 mod project_shared_notification;
 mod sharing_status_indicator;
 
-use anyhow::anyhow;
 use call::ActiveCall;
 pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
 use gpui::{actions, AppContext, Task};
 use std::sync::Arc;
-use workspace::{AppState, JoinProject, Workspace};
+use workspace::AppState;
 
 actions!(collab, [ToggleScreenSharing]);
 
-pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     collab_titlebar_item::init(cx);
     contact_notification::init(cx);
     contact_list::init(cx);
     contact_finder::init(cx);
     contacts_popover::init(cx);
-    incoming_call_notification::init(cx);
-    project_shared_notification::init(cx);
+    incoming_call_notification::init(&app_state, cx);
+    project_shared_notification::init(&app_state, cx);
     sharing_status_indicator::init(cx);
 
     cx.add_global_action(toggle_screen_sharing);
-    cx.add_global_action(move |action: &JoinProject, cx| {
-        join_project(action, app_state.clone(), cx);
-    });
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -47,88 +43,3 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
         toggle_screen_sharing.detach_and_log_err(cx);
     }
 }
-
-fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut AppContext) {
-    let project_id = action.project_id;
-    let follow_user_id = action.follow_user_id;
-    cx.spawn(|mut cx| async move {
-        let existing_workspace = cx.update(|cx| {
-            cx.window_ids()
-                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
-                .find(|workspace| {
-                    workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
-                })
-        });
-
-        let workspace = if let Some(existing_workspace) = existing_workspace {
-            existing_workspace.downgrade()
-        } else {
-            let active_call = cx.read(ActiveCall::global);
-            let room = active_call
-                .read_with(&cx, |call, _| call.room().cloned())
-                .ok_or_else(|| anyhow!("not in a call"))?;
-            let project = room
-                .update(&mut cx, |room, cx| {
-                    room.join_project(
-                        project_id,
-                        app_state.languages.clone(),
-                        app_state.fs.clone(),
-                        cx,
-                    )
-                })
-                .await?;
-
-            let (_, workspace) = cx.add_window(
-                (app_state.build_window_options)(None, None, cx.platform().as_ref()),
-                |cx| {
-                    let mut workspace = Workspace::new(
-                        Default::default(),
-                        0,
-                        project,
-                        app_state.dock_default_item_factory,
-                        app_state.background_actions,
-                        cx,
-                    );
-                    (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                    workspace
-                },
-            );
-            workspace.downgrade()
-        };
-
-        cx.activate_window(workspace.window_id());
-        cx.platform().activate(true);
-
-        workspace.update(&mut cx, |workspace, cx| {
-            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-                let follow_peer_id = room
-                    .read(cx)
-                    .remote_participants()
-                    .iter()
-                    .find(|(_, participant)| participant.user.id == follow_user_id)
-                    .map(|(_, p)| p.peer_id)
-                    .or_else(|| {
-                        // If we couldn't follow the given user, follow the host instead.
-                        let collaborator = workspace
-                            .project()
-                            .read(cx)
-                            .collaborators()
-                            .values()
-                            .find(|collaborator| collaborator.replica_id == 0)?;
-                        Some(collaborator.peer_id)
-                    });
-
-                if let Some(follow_peer_id) = follow_peer_id {
-                    if !workspace.is_being_followed(follow_peer_id) {
-                        workspace
-                            .toggle_follow(follow_peer_id, cx)
-                            .map(|follow| follow.detach_and_log_err(cx));
-                    }
-                }
-            }
-        })?;
-
-        anyhow::Ok(())
-    })
-    .detach_and_log_err(cx);
-}

crates/collab_ui/src/contact_list.rs 🔗

@@ -11,7 +11,7 @@ use gpui::{
     impl_actions, impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
-    AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+    AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::Project;
@@ -19,7 +19,7 @@ use serde::Deserialize;
 use settings::Settings;
 use std::{mem, sync::Arc};
 use theme::IconButton;
-use workspace::{JoinProject, OpenSharedScreen};
+use workspace::{OpenSharedScreen, Workspace};
 
 impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
 impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
@@ -161,6 +161,7 @@ pub struct ContactList {
     match_candidates: Vec<StringMatchCandidate>,
     list_state: ListState<Self>,
     project: ModelHandle<Project>,
+    workspace: WeakViewHandle<Workspace>,
     user_store: ModelHandle<UserStore>,
     filter_editor: ViewHandle<Editor>,
     collapsed_sections: Vec<Section>,
@@ -169,11 +170,7 @@ pub struct ContactList {
 }
 
 impl ContactList {
-    pub fn new(
-        project: ModelHandle<Project>,
-        user_store: ModelHandle<UserStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
         let filter_editor = cx.add_view(|cx| {
             let mut editor = Editor::single_line(
                 Some(Arc::new(|theme| {
@@ -278,6 +275,7 @@ impl ContactList {
         });
 
         let active_call = ActiveCall::global(cx);
+        let user_store = workspace.read(cx).user_store().clone();
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
@@ -290,7 +288,8 @@ impl ContactList {
             match_candidates: Default::default(),
             filter_editor,
             _subscriptions: subscriptions,
-            project,
+            project: workspace.read(cx).project().clone(),
+            workspace: workspace.downgrade(),
             user_store,
         };
         this.update_entries(cx);
@@ -422,10 +421,16 @@ impl ContactList {
                         host_user_id,
                         ..
                     } => {
-                        cx.dispatch_global_action(JoinProject {
-                            project_id: *project_id,
-                            follow_user_id: *host_user_id,
-                        });
+                        if let Some(workspace) = self.workspace.upgrade(cx) {
+                            let app_state = workspace.read(cx).app_state().clone();
+                            workspace::join_remote_project(
+                                *project_id,
+                                *host_user_id,
+                                app_state,
+                                cx,
+                            )
+                            .detach_and_log_err(cx);
+                        }
                     }
                     ContactEntry::ParticipantScreen { peer_id, .. } => {
                         cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
@@ -798,6 +803,8 @@ impl ContactList {
         theme: &theme::ContactList,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
+        enum JoinProject {}
+
         let font_cache = cx.font_cache();
         let host_avatar_height = theme
             .contact_avatar
@@ -873,12 +880,13 @@ impl ContactList {
         } else {
             CursorStyle::Arrow
         })
-        .on_click(MouseButton::Left, move |_, _, cx| {
+        .on_click(MouseButton::Left, move |_, this, cx| {
             if !is_current {
-                cx.dispatch_global_action(JoinProject {
-                    project_id,
-                    follow_user_id: host_user_id,
-                });
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    let app_state = workspace.read(cx).app_state().clone();
+                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                        .detach_and_log_err(cx);
+                }
             }
         })
         .into_any()

crates/collab_ui/src/contacts_popover.rs 🔗

@@ -6,11 +6,11 @@ use crate::{
 use client::UserStore;
 use gpui::{
     actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
-    ViewContext, ViewHandle,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use picker::PickerEvent;
-use project::Project;
 use settings::Settings;
+use workspace::Workspace;
 
 actions!(contacts_popover, [ToggleContactFinder]);
 
@@ -29,23 +29,17 @@ enum Child {
 
 pub struct ContactsPopover {
     child: Child,
-    project: ModelHandle<Project>,
     user_store: ModelHandle<UserStore>,
+    workspace: WeakViewHandle<Workspace>,
     _subscription: Option<gpui::Subscription>,
 }
 
 impl ContactsPopover {
-    pub fn new(
-        project: ModelHandle<Project>,
-        user_store: ModelHandle<UserStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
         let mut this = Self {
-            child: Child::ContactList(
-                cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
-            ),
-            project,
-            user_store,
+            child: Child::ContactList(cx.add_view(|cx| ContactList::new(workspace, cx))),
+            user_store: workspace.read(cx).user_store().clone(),
+            workspace: workspace.downgrade(),
             _subscription: None,
         };
         this.show_contact_list(String::new(), cx);
@@ -74,16 +68,16 @@ impl ContactsPopover {
     }
 
     fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
-        let child = cx.add_view(|cx| {
-            ContactList::new(self.project.clone(), self.user_store.clone(), cx)
-                .with_editor_text(editor_text, cx)
-        });
-        cx.focus(&child);
-        self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
-            crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
-        }));
-        self.child = Child::ContactList(child);
-        cx.notify();
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let child = cx
+                .add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx));
+            cx.focus(&child);
+            self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
+                crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
+            }));
+            self.child = Child::ContactList(child);
+            cx.notify();
+        }
     }
 }
 

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -1,3 +1,5 @@
+use std::sync::{Arc, Weak};
+
 use call::{ActiveCall, IncomingCall};
 use client::proto;
 use futures::StreamExt;
@@ -10,13 +12,14 @@ use gpui::{
 };
 use settings::Settings;
 use util::ResultExt;
-use workspace::JoinProject;
+use workspace::AppState;
 
 impl_internal_actions!(incoming_call_notification, [RespondToCall]);
 
-pub fn init(cx: &mut AppContext) {
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     cx.add_action(IncomingCallNotification::respond_to_call);
 
+    let app_state = Arc::downgrade(app_state);
     let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
     cx.spawn(|mut cx| async move {
         let mut notification_windows = Vec::new();
@@ -48,7 +51,7 @@ pub fn init(cx: &mut AppContext) {
                             is_movable: false,
                             screen: Some(screen),
                         },
-                        |_| IncomingCallNotification::new(incoming_call.clone()),
+                        |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
                     );
 
                     notification_windows.push(window_id);
@@ -66,11 +69,12 @@ struct RespondToCall {
 
 pub struct IncomingCallNotification {
     call: IncomingCall,
+    app_state: Weak<AppState>,
 }
 
 impl IncomingCallNotification {
-    pub fn new(call: IncomingCall) -> Self {
-        Self { call }
+    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+        Self { call, app_state }
     }
 
     fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
@@ -79,15 +83,20 @@ impl IncomingCallNotification {
             let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
             let caller_user_id = self.call.calling_user.id;
             let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
-            cx.spawn(|_, mut cx| async move {
+            cx.spawn(|this, mut cx| async move {
                 join.await?;
                 if let Some(project_id) = initial_project_id {
-                    cx.update(|cx| {
-                        cx.dispatch_global_action(JoinProject {
-                            project_id,
-                            follow_user_id: caller_user_id,
-                        })
-                    });
+                    this.update(&mut cx, |this, cx| {
+                        if let Some(app_state) = this.app_state.upgrade() {
+                            workspace::join_remote_project(
+                                project_id,
+                                caller_user_id,
+                                app_state,
+                                cx,
+                            )
+                            .detach_and_log_err(cx);
+                        }
+                    })?;
                 }
                 anyhow::Ok(())
             })

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -2,22 +2,17 @@ use call::{room, ActiveCall};
 use client::User;
 use collections::HashMap;
 use gpui::{
-    actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
     AppContext, Entity, View, ViewContext,
 };
 use settings::Settings;
-use std::sync::Arc;
-use workspace::JoinProject;
-
-actions!(project_shared_notification, [DismissProject]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ProjectSharedNotification::join);
-    cx.add_action(ProjectSharedNotification::dismiss);
+use std::sync::{Arc, Weak};
+use workspace::AppState;
 
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    let app_state = Arc::downgrade(app_state);
     let active_call = ActiveCall::global(cx);
     let mut notification_windows = HashMap::default();
     cx.subscribe(&active_call, move |_, event, cx| match event {
@@ -50,6 +45,7 @@ pub fn init(cx: &mut AppContext) {
                             owner.clone(),
                             *project_id,
                             worktree_root_names.clone(),
+                            app_state.clone(),
                         )
                     },
                 );
@@ -82,23 +78,33 @@ pub struct ProjectSharedNotification {
     project_id: u64,
     worktree_root_names: Vec<String>,
     owner: Arc<User>,
+    app_state: Weak<AppState>,
 }
 
 impl ProjectSharedNotification {
-    fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
+    fn new(
+        owner: Arc<User>,
+        project_id: u64,
+        worktree_root_names: Vec<String>,
+        app_state: Weak<AppState>,
+    ) -> Self {
         Self {
             project_id,
             worktree_root_names,
             owner,
+            app_state,
         }
     }
 
-    fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
+    fn join(&mut self, cx: &mut ViewContext<Self>) {
         cx.remove_window();
-        cx.propagate_action();
+        if let Some(app_state) = self.app_state.upgrade() {
+            workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
+                .detach_and_log_err(cx);
+        }
     }
 
-    fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
         cx.remove_window();
     }
 
@@ -161,9 +167,6 @@ impl ProjectSharedNotification {
         enum Open {}
         enum Dismiss {}
 
-        let project_id = self.project_id;
-        let owner_user_id = self.owner.id;
-
         Flex::column()
             .with_child(
                 MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| {
@@ -174,12 +177,7 @@ impl ProjectSharedNotification {
                         .with_style(theme.open_button.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    cx.dispatch_action(JoinProject {
-                        project_id,
-                        follow_user_id: owner_user_id,
-                    });
-                })
+                .on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
                 .flex(1., true),
             )
             .with_child(
@@ -191,8 +189,8 @@ impl ProjectSharedNotification {
                         .with_style(theme.dismiss_button.container)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    cx.dispatch_action(DismissProject);
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.dismiss(cx);
                 })
                 .flex(1., true),
             )

crates/workspace/src/dock.rs 🔗

@@ -404,11 +404,13 @@ mod tests {
     use std::{
         ops::{Deref, DerefMut},
         path::PathBuf,
+        sync::Arc,
     };
 
     use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
     use project::{FakeFs, Project};
     use settings::Settings;
+    use theme::ThemeRegistry;
 
     use super::*;
     use crate::{
@@ -419,7 +421,7 @@ mod tests {
         },
         register_deserializable_item,
         sidebar::Sidebar,
-        ItemHandle, Workspace,
+        AppState, ItemHandle, Workspace,
     };
 
     pub fn default_item_factory(
@@ -467,8 +469,17 @@ mod tests {
                 Some(serialized_workspace),
                 0,
                 project.clone(),
-                default_item_factory,
-                || &[],
+                Arc::new(AppState {
+                    languages: project.read(cx).languages().clone(),
+                    themes: ThemeRegistry::new((), cx.font_cache().clone()),
+                    client: project.read(cx).client(),
+                    user_store: project.read(cx).user_store(),
+                    fs: project.read(cx).fs().clone(),
+                    build_window_options: |_, _, _| Default::default(),
+                    initialize_workspace: |_, _, _| {},
+                    dock_default_item_factory: default_item_factory,
+                    background_actions: || &[],
+                }),
                 cx,
             )
         });
@@ -598,11 +609,20 @@ mod tests {
             let project = Project::test(fs, [], cx).await;
             let (window_id, workspace) = cx.add_window(|cx| {
                 Workspace::new(
-                    Default::default(),
+                    None,
                     0,
-                    project,
-                    default_item_factory,
-                    || &[],
+                    project.clone(),
+                    Arc::new(AppState {
+                        languages: project.read(cx).languages().clone(),
+                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
+                        client: project.read(cx).client(),
+                        user_store: project.read(cx).user_store(),
+                        fs: project.read(cx).fs().clone(),
+                        build_window_options: |_, _, _| Default::default(),
+                        initialize_workspace: |_, _, _| {},
+                        dock_default_item_factory: default_item_factory,
+                        background_actions: || &[],
+                    }),
                     cx,
                 )
             });

crates/workspace/src/item.rs 🔗

@@ -365,7 +365,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                 workspace.update_followers(
                     proto::update_followers::Variant::CreateView(proto::View {
                         id: followed_item
-                            .remote_id(&workspace.client, cx)
+                            .remote_id(&workspace.app_state.client, cx)
                             .map(|id| id.to_proto()),
                         variant: Some(message),
                         leader_id: workspace.leader_for_pane(&pane),
@@ -421,7 +421,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                         proto::update_followers::Variant::UpdateView(
                                             proto::UpdateView {
                                                 id: item
-                                                    .remote_id(&this.client, cx)
+                                                    .remote_id(&this.app_state.client, cx)
                                                     .map(|id| id.to_proto()),
                                                 variant: pending_update.borrow_mut().take(),
                                                 leader_id,

crates/workspace/src/pane_group.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
+use std::sync::Arc;
+
+use crate::{AppState, FollowerStatesByLeader, Pane, Workspace};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
@@ -70,6 +72,7 @@ impl PaneGroup {
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
+        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
         self.root.render(
@@ -78,6 +81,7 @@ impl PaneGroup {
             follower_states,
             active_call,
             active_pane,
+            app_state,
             cx,
         )
     }
@@ -131,6 +135,7 @@ impl Member {
         follower_states: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
+        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
         enum FollowIntoExternalProject {}
@@ -175,6 +180,7 @@ impl Member {
                             } else {
                                 let leader_user = leader.user.clone();
                                 let leader_user_id = leader.user.id;
+                                let app_state = Arc::downgrade(app_state);
                                 Some(
                                     MouseEventHandler::<FollowIntoExternalProject, _>::new(
                                         pane.id(),
@@ -199,10 +205,15 @@ impl Member {
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
                                     .on_click(MouseButton::Left, move |_, _, cx| {
-                                        cx.dispatch_action(JoinProject {
-                                            project_id: leader_project_id,
-                                            follow_user_id: leader_user_id,
-                                        })
+                                        if let Some(app_state) = app_state.upgrade() {
+                                            crate::join_remote_project(
+                                                leader_project_id,
+                                                leader_user_id,
+                                                app_state,
+                                                cx,
+                                            )
+                                            .detach_and_log_err(cx);
+                                        }
                                     })
                                     .aligned()
                                     .bottom()
@@ -257,6 +268,7 @@ impl Member {
                 follower_states,
                 active_call,
                 active_pane,
+                app_state,
                 cx,
             ),
         }
@@ -360,6 +372,7 @@ impl PaneAxis {
         follower_state: &FollowerStatesByLeader,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
+        app_state: &Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) -> AnyElement<Workspace> {
         let last_member_ix = self.members.len() - 1;
@@ -370,8 +383,15 @@ impl PaneAxis {
                     flex = cx.global::<Settings>().active_pane_magnification;
                 }
 
-                let mut member =
-                    member.render(project, theme, follower_state, active_call, active_pane, cx);
+                let mut member = member.render(
+                    project,
+                    theme,
+                    follower_state,
+                    active_call,
+                    active_pane,
+                    app_state,
+                    cx,
+                );
                 if ix < last_member_ix {
                     let mut border = theme.workspace.pane_divider;
                     border.left = false;

crates/workspace/src/workspace.rs 🔗

@@ -25,7 +25,6 @@ use client::{
 use collections::{hash_map, HashMap, HashSet};
 use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
-use fs::{self, Fs};
 use futures::{
     channel::{mpsc, oneshot},
     future::try_join_all,
@@ -135,12 +134,6 @@ pub struct OpenPaths {
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePane(pub usize);
 
-#[derive(Clone, PartialEq)]
-pub struct JoinProject {
-    pub project_id: u64,
-    pub follow_user_id: u64,
-}
-
 #[derive(Clone, PartialEq)]
 pub struct OpenSharedScreen {
     pub peer_id: PeerId,
@@ -216,7 +209,6 @@ pub type WorkspaceId = i64;
 impl_internal_actions!(
     workspace,
     [
-        JoinProject,
         OpenSharedScreen,
         RemoveWorktreeFromProject,
         SplitWithItem,
@@ -524,10 +516,7 @@ pub enum Event {
 
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
-    client: Arc<Client>,
-    user_store: ModelHandle<client::UserStore>,
     remote_entity_subscription: Option<client::Subscription>,
-    fs: Arc<dyn Fs>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_sidebar: ViewHandle<Sidebar>,
@@ -548,7 +537,7 @@ pub struct Workspace {
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
-    background_actions: BackgroundActions,
+    app_state: Arc<AppState>,
     _window_subscriptions: [Subscription; 3],
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<Result<()>>,
@@ -578,8 +567,7 @@ impl Workspace {
         serialized_workspace: Option<SerializedWorkspace>,
         workspace_id: WorkspaceId,
         project: ModelHandle<Project>,
-        dock_default_factory: DockDefaultItemFactory,
-        background_actions: BackgroundActions,
+        app_state: Arc<AppState>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -616,8 +604,8 @@ impl Workspace {
 
         let weak_handle = cx.weak_handle();
 
-        let center_pane =
-            cx.add_view(|cx| Pane::new(weak_handle.clone(), None, background_actions, cx));
+        let center_pane = cx
+            .add_view(|cx| Pane::new(weak_handle.clone(), None, app_state.background_actions, cx));
         let pane_id = center_pane.id();
         cx.subscribe(&center_pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -625,14 +613,15 @@ impl Workspace {
         .detach();
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
-        let dock = Dock::new(dock_default_factory, background_actions, cx);
+        let dock = Dock::new(
+            app_state.dock_default_item_factory,
+            app_state.background_actions,
+            cx,
+        );
         let dock_pane = dock.pane().clone();
 
-        let fs = project.read(cx).fs().clone();
-        let user_store = project.read(cx).user_store();
-        let client = project.read(cx).client();
-        let mut current_user = user_store.read(cx).watch_current_user();
-        let mut connection_status = client.status();
+        let mut current_user = app_state.user_store.read(cx).watch_current_user();
+        let mut connection_status = app_state.client.status();
         let _observe_current_user = cx.spawn(|this, mut cx| async move {
             current_user.recv().await;
             connection_status.recv().await;
@@ -725,10 +714,7 @@ impl Workspace {
             status_bar,
             titlebar_item: None,
             notifications: Default::default(),
-            client,
             remote_entity_subscription: None,
-            user_store,
-            fs,
             left_sidebar,
             right_sidebar,
             project: project.clone(),
@@ -738,7 +724,7 @@ impl Workspace {
             window_edited: false,
             active_call,
             database_id: workspace_id,
-            background_actions,
+            app_state,
             _observe_current_user,
             _apply_leader_updates,
             leader_updates_tx,
@@ -827,8 +813,7 @@ impl Workspace {
                         serialized_workspace,
                         workspace_id,
                         project_handle.clone(),
-                        app_state.dock_default_item_factory,
-                        app_state.background_actions,
+                        app_state.clone(),
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -938,8 +923,12 @@ impl Workspace {
         &self.status_bar
     }
 
+    pub fn app_state(&self) -> &Arc<AppState> {
+        &self.app_state
+    }
+
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
-        &self.user_store
+        &self.app_state.user_store
     }
 
     pub fn project(&self) -> &ModelHandle<Project> {
@@ -947,7 +936,7 @@ impl Workspace {
     }
 
     pub fn client(&self) -> &Client {
-        &self.client
+        &self.app_state.client
     }
 
     pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -1183,7 +1172,7 @@ impl Workspace {
         visible: bool,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
-        let fs = self.fs.clone();
+        let fs = self.app_state.fs.clone();
 
         // Sort the paths to ensure we add worktrees for parents before their children.
         abs_paths.sort_unstable();
@@ -1494,8 +1483,14 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane =
-            cx.add_view(|cx| Pane::new(self.weak_handle(), None, self.background_actions, cx));
+        let pane = cx.add_view(|cx| {
+            Pane::new(
+                self.weak_handle(),
+                None,
+                self.app_state.background_actions,
+                cx,
+            )
+        });
         let pane_id = pane.id();
         cx.subscribe(&pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -1688,7 +1683,7 @@ impl Workspace {
             proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
                 id: self.active_item(cx).and_then(|item| {
                     item.to_followable_item_handle(cx)?
-                        .remote_id(&self.client, cx)
+                        .remote_id(&self.app_state.client, cx)
                         .map(|id| id.to_proto())
                 }),
                 leader_id: self.leader_for_pane(&pane),
@@ -1857,8 +1852,11 @@ impl Workspace {
 
     fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
         if let Some(remote_id) = remote_id {
-            self.remote_entity_subscription =
-                Some(self.client.add_view_for_remote_entity(remote_id, cx));
+            self.remote_entity_subscription = Some(
+                self.app_state
+                    .client
+                    .add_view_for_remote_entity(remote_id, cx),
+            );
         } else {
             self.remote_entity_subscription.take();
         }
@@ -1898,7 +1896,7 @@ impl Workspace {
         cx.notify();
 
         let project_id = self.project.read(cx).remote_id()?;
-        let request = self.client.request(proto::Follow {
+        let request = self.app_state.client.request(proto::Follow {
             project_id,
             leader_id: Some(leader_id),
         });
@@ -1977,7 +1975,8 @@ impl Workspace {
                 if states_by_pane.is_empty() {
                     self.follower_states_by_leader.remove(&leader_id);
                     if let Some(project_id) = self.project.read(cx).remote_id() {
-                        self.client
+                        self.app_state
+                            .client
                             .send(proto::Unfollow {
                                 project_id,
                                 leader_id: Some(leader_id),
@@ -2158,7 +2157,7 @@ impl Workspace {
         mut cx: AsyncAppContext,
     ) -> Result<proto::FollowResponse> {
         this.update(&mut cx, |this, cx| {
-            let client = &this.client;
+            let client = &this.app_state.client;
             this.leader_state
                 .followers
                 .insert(envelope.original_sender_id()?);
@@ -2366,7 +2365,8 @@ impl Workspace {
     ) -> Option<()> {
         let project_id = self.project.read(cx).remote_id()?;
         if !self.leader_state.followers.is_empty() {
-            self.client
+            self.app_state
+                .client
                 .send(proto::UpdateFollowers {
                     project_id,
                     follower_ids: self.leader_state.followers.iter().copied().collect(),
@@ -2698,7 +2698,18 @@ impl Workspace {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-        Self::new(None, 0, project, |_, _| None, || &[], cx)
+        let app_state = Arc::new(AppState {
+            languages: project.read(cx).languages().clone(),
+            themes: ThemeRegistry::new((), cx.font_cache().clone()),
+            client: project.read(cx).client(),
+            user_store: project.read(cx).user_store(),
+            fs: project.read(cx).fs().clone(),
+            build_window_options: |_, _, _| Default::default(),
+            initialize_workspace: |_, _, _| {},
+            dock_default_item_factory: |_, _| None,
+            background_actions: || &[],
+        });
+        Self::new(None, 0, project, app_state, cx)
     }
 }
 
@@ -2784,6 +2795,7 @@ impl View for Workspace {
                                                         &self.follower_states_by_leader,
                                                         self.active_call(),
                                                         self.active_pane(),
+                                                        &self.app_state,
                                                         cx,
                                                     ))
                                                     .flex(1., true),
@@ -3015,6 +3027,87 @@ pub fn open_new(
     })
 }
 
+pub fn join_remote_project(
+    project_id: u64,
+    follow_user_id: u64,
+    app_state: Arc<AppState>,
+    cx: &mut AppContext,
+) -> Task<Result<()>> {
+    cx.spawn(|mut cx| async move {
+        let existing_workspace = cx.update(|cx| {
+            cx.window_ids()
+                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
+                .find(|workspace| {
+                    workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
+                })
+        });
+
+        let workspace = if let Some(existing_workspace) = existing_workspace {
+            existing_workspace.downgrade()
+        } else {
+            let active_call = cx.read(ActiveCall::global);
+            let room = active_call
+                .read_with(&cx, |call, _| call.room().cloned())
+                .ok_or_else(|| anyhow!("not in a call"))?;
+            let project = room
+                .update(&mut cx, |room, cx| {
+                    room.join_project(
+                        project_id,
+                        app_state.languages.clone(),
+                        app_state.fs.clone(),
+                        cx,
+                    )
+                })
+                .await?;
+
+            let (_, workspace) = cx.add_window(
+                (app_state.build_window_options)(None, None, cx.platform().as_ref()),
+                |cx| {
+                    let mut workspace =
+                        Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
+                    (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+                    workspace
+                },
+            );
+            workspace.downgrade()
+        };
+
+        cx.activate_window(workspace.window_id());
+        cx.platform().activate(true);
+
+        workspace.update(&mut cx, |workspace, cx| {
+            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+                let follow_peer_id = room
+                    .read(cx)
+                    .remote_participants()
+                    .iter()
+                    .find(|(_, participant)| participant.user.id == follow_user_id)
+                    .map(|(_, p)| p.peer_id)
+                    .or_else(|| {
+                        // If we couldn't follow the given user, follow the host instead.
+                        let collaborator = workspace
+                            .project()
+                            .read(cx)
+                            .collaborators()
+                            .values()
+                            .find(|collaborator| collaborator.replica_id == 0)?;
+                        Some(collaborator.peer_id)
+                    });
+
+                if let Some(follow_peer_id) = follow_peer_id {
+                    if !workspace.is_being_followed(follow_peer_id) {
+                        workspace
+                            .toggle_follow(follow_peer_id, cx)
+                            .map(|follow| follow.detach_and_log_err(cx));
+                    }
+                }
+            }
+        })?;
+
+        anyhow::Ok(())
+    })
+}
+
 fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
     let mut parts = value.split(',');
     let width: usize = parts.next()?.parse().ok()?;
@@ -3041,16 +3134,7 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| None,
-                || &[],
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // Adding an item with no ambiguity renders the tab without detail.
         let item1 = cx.add_view(&workspace, |_| {
@@ -3114,16 +3198,7 @@ mod tests {
         .await;
 
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| None,
-                || &[],
-                cx,
-            )
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
@@ -3213,16 +3288,7 @@ mod tests {
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
         let project = Project::test(fs, ["root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| None,
-                || &[],
-                cx,
-            )
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // When there are no dirty items, there's nothing to do.
         let item1 = cx.add_view(&workspace, |_| TestItem::new());
@@ -3257,9 +3323,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let item1 = cx.add_view(&workspace, |cx| {
             TestItem::new()
@@ -3366,9 +3430,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Create several workspace items with single project entries, and two
         // workspace items with multiple project entries.
@@ -3475,9 +3537,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let item = cx.add_view(&workspace, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
@@ -3594,9 +3654,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let item = cx.add_view(&workspace, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])

crates/zed/src/main.rs 🔗

@@ -199,7 +199,7 @@ fn main() {
         language_selector::init(app_state.clone(), cx);
         theme_selector::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
-        collab_ui::init(app_state.clone(), cx);
+        collab_ui::init(&app_state, cx);
         feedback::init(app_state.clone(), cx);
         welcome::init(cx);
 

crates/zed/src/zed.rs 🔗

@@ -302,8 +302,7 @@ pub fn initialize_workspace(
     cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
     cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
 
-    let collab_titlebar_item =
-        cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
+    let collab_titlebar_item = cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, cx));
     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 
     let project_panel = ProjectPanel::new(workspace.project().clone(), cx);