diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 90bcf999a2a3052f6cbbf78bbf5247c00fb69e07..8d3b10d38e4054573b469898f2a0107796d58c6d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/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, - user_store: &ModelHandle, - cx: &mut ViewContext, - ) -> Self { + pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> 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) { 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::::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::( peer_id.as_u64() as usize, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 784e398653bf88b215f2f11f1dbf8e19ce54a015..093392dd9640e25c84bbdb3c9daeb87592952792 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/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, cx: &mut AppContext) { +pub fn init(app_state: &Arc, 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, 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::()) - .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); -} diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 8cbba6030d29a5dc9b6ca30ab80abb6c7b053048..fffe90716e38c81afd060564d410ef20be470c5a 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/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, list_state: ListState, project: ModelHandle, + workspace: WeakViewHandle, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, @@ -169,11 +170,7 @@ pub struct ContactList { } impl ContactList { - pub fn new( - project: ModelHandle, - user_store: ModelHandle, - cx: &mut ViewContext, - ) -> Self { + pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> 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, ) -> AnyElement { + 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() diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index cbef4bfa88b2421dc55c7d70b0dfbff866934c65..60f0bf0e73b706ddf2db1e651c7e46c1d1902723 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/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, user_store: ModelHandle, + workspace: WeakViewHandle, _subscription: Option, } impl ContactsPopover { - pub fn new( - project: ModelHandle, - user_store: ModelHandle, - cx: &mut ViewContext, - ) -> Self { + pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> 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) { - 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(); + } } } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index d0cf893a0faed5019e1e378c4ce2ba41b91c572a..ffcf1b8630b4a8d1560628dd83446de683b96f25 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/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, 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, } impl IncomingCallNotification { - pub fn new(call: IncomingCall) -> Self { - Self { call } + pub fn new(call: IncomingCall, app_state: Weak) -> Self { + Self { call, app_state } } fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext) { @@ -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(()) }) diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 4cdb18d97c9166430852bf77102ebd6b5857ab73..1304688ca174d388d1019719568880697dfe637e 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/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, 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, owner: Arc, + app_state: Weak, } impl ProjectSharedNotification { - fn new(owner: Arc, project_id: u64, worktree_root_names: Vec) -> Self { + fn new( + owner: Arc, + project_id: u64, + worktree_root_names: Vec, + app_state: Weak, + ) -> Self { Self { project_id, worktree_root_names, owner, + app_state, } } - fn join(&mut self, _: &JoinProject, cx: &mut ViewContext) { + fn join(&mut self, cx: &mut ViewContext) { 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) { + fn dismiss(&mut self, cx: &mut ViewContext) { 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::::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), ) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 5198ba1417a0377990e36ff5b15791662ac84757..33cd833019eb7a473c6cee8aeaa329f45bad93fa 100644 --- a/crates/workspace/src/dock.rs +++ b/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, ) }); diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index b30107c726f45f67b556b9181e541b4968e8ac08..38e6b92b344abacec22f641ee325d44cd213d354 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -365,7 +365,7 @@ impl ItemHandle for ViewHandle { 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 ItemHandle for ViewHandle { 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, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 49dd8db6060b69e20d4ecc33cd22bba3eccf7c68..55032b4bc1e79bf40e75c5be5a0a4b7e47f3e836 100644 --- a/crates/workspace/src/pane_group.rs +++ b/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>, active_pane: &ViewHandle, + app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { 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>, active_pane: &ViewHandle, + app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { 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::::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>, active_pane: &ViewHandle, + app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { let last_member_ix = self.members.len() - 1; @@ -370,8 +383,15 @@ impl PaneAxis { flex = cx.global::().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; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 43e56d2f24b0651b46cf3c9f8ad0cb183772dad4..3dbb94f4b896f020ed659a41b4898819058ba529 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, - client: Arc, - user_store: ModelHandle, remote_entity_subscription: Option, - fs: Arc, modal: Option, center: PaneGroup, left_sidebar: ViewHandle, @@ -548,7 +537,7 @@ pub struct Workspace { active_call: Option<(ModelHandle, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, - background_actions: BackgroundActions, + app_state: Arc, _window_subscriptions: [Subscription; 3], _apply_leader_updates: Task>, _observe_current_user: Task>, @@ -578,8 +567,7 @@ impl Workspace { serialized_workspace: Option, workspace_id: WorkspaceId, project: ModelHandle, - dock_default_factory: DockDefaultItemFactory, - background_actions: BackgroundActions, + app_state: Arc, cx: &mut ViewContext, ) -> 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(¢er_pane, move |this, _, event, cx| { this.handle_pane_event(pane_id, event, cx) @@ -625,14 +613,15 @@ impl Workspace { .detach(); cx.focus(¢er_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 { + &self.app_state + } + pub fn user_store(&self) -> &ModelHandle { - &self.user_store + &self.app_state.user_store } pub fn project(&self) -> &ModelHandle { @@ -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) { @@ -1183,7 +1172,7 @@ impl Workspace { visible: bool, cx: &mut ViewContext, ) -> Task, 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) -> ViewHandle { - 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, cx: &mut ViewContext) { 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 { 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, cx: &mut ViewContext) -> 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, + cx: &mut AppContext, +) -> Task> { + 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::()) + .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 { 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)]) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index db19c93cfa00ae6a1aad4d3dd7d0aa379f9a0ee2..679fd39e2f9455d461b946507bd800c23110bf27 100644 --- a/crates/zed/src/main.rs +++ b/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); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4325835cdbe09d8573dff129ac9112c1b8323f75..40c30f7560e5d18304ae4a12771f0943b50c85a3 100644 --- a/crates/zed/src/zed.rs +++ b/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);