Allow opening of remote projects via the contacts panel

Antonio Scandurra created

Change summary

crates/contacts_panel/src/contacts_panel.rs |  51 +++-----
crates/server/src/rpc.rs                    |   1 
crates/workspace/src/workspace.rs           | 131 +++++++++++++++++-----
crates/zed/src/main.rs                      |   1 
crates/zed/src/zed.rs                       |  58 ++++++---
5 files changed, 156 insertions(+), 86 deletions(-)

Detailed changes

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -1,21 +1,15 @@
+use std::sync::Arc;
+
 use client::{Contact, UserStore};
 use gpui::{
-    action,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     platform::CursorStyle,
-    Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
-    Subscription, View, ViewContext,
+    Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
+    ViewContext,
 };
 use postage::watch;
-use theme::Theme;
-use workspace::{Settings, Workspace};
-
-action!(JoinProject, u64);
-
-pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(ContactsPanel::join_project);
-}
+use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
 
 pub struct ContactsPanel {
     contacts: ListState,
@@ -25,42 +19,33 @@ pub struct ContactsPanel {
 }
 
 impl ContactsPanel {
-    pub fn new(
-        user_store: ModelHandle<UserStore>,
-        settings: watch::Receiver<Settings>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
         Self {
             contacts: ListState::new(
-                user_store.read(cx).contacts().len(),
+                app_state.user_store.read(cx).contacts().len(),
                 Orientation::Top,
                 1000.,
                 {
-                    let user_store = user_store.clone();
-                    let settings = settings.clone();
+                    let app_state = app_state.clone();
                     move |ix, cx| {
-                        let user_store = user_store.read(cx);
+                        let user_store = app_state.user_store.read(cx);
                         let contacts = user_store.contacts().clone();
                         let current_user_id = user_store.current_user().map(|user| user.id);
                         Self::render_collaborator(
                             &contacts[ix],
                             current_user_id,
-                            &settings.borrow().theme,
+                            app_state.clone(),
                             cx,
                         )
                     }
                 },
             ),
-            _maintain_contacts: cx.observe(&user_store, Self::update_contacts),
-            user_store,
-            settings,
+            _maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts),
+            user_store: app_state.user_store.clone(),
+            settings: app_state.settings.clone(),
         }
     }
 
-    fn join_project(_: &mut Workspace, _: &JoinProject, _: &mut ViewContext<Workspace>) {
-        todo!();
-    }
-
     fn update_contacts(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
         self.contacts
             .reset(self.user_store.read(cx).contacts().len());
@@ -70,10 +55,10 @@ impl ContactsPanel {
     fn render_collaborator(
         collaborator: &Contact,
         current_user_id: Option<u64>,
-        theme: &Theme,
+        app_state: Arc<AppState>,
         cx: &mut LayoutContext,
     ) -> ElementBox {
-        let theme = &theme.contacts_panel;
+        let theme = &app_state.settings.borrow().theme.contacts_panel;
         let project_count = collaborator.projects.len();
         let font_cache = cx.font_cache();
         let line_height = theme.unshared_project.name.text.line_height(font_cache);
@@ -169,6 +154,7 @@ impl ContactsPanel {
                                         .iter()
                                         .any(|guest| Some(guest.id) == current_user_id);
                                 let is_shared = project.is_shared;
+                                let app_state = app_state.clone();
 
                                 MouseEventHandler::new::<ContactsPanel, _, _, _>(
                                     project_id as usize,
@@ -222,7 +208,10 @@ impl ContactsPanel {
                                 })
                                 .on_click(move |cx| {
                                     if !is_host && !is_guest {
-                                        cx.dispatch_action(JoinProject(project_id))
+                                        cx.dispatch_global_action(JoinProject(JoinProjectParams {
+                                            project_id,
+                                            app_state: app_state.clone(),
+                                        }));
                                     }
                                 })
                                 .expanded(1.0)

crates/server/src/rpc.rs 🔗

@@ -1165,7 +1165,6 @@ mod tests {
 
     #[gpui::test]
     async fn test_unshare_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
-        cx_b.update(zed::contacts_panel::init);
         let lang_registry = Arc::new(LanguageRegistry::new());
         let fs = Arc::new(FakeFs::new());
         cx_a.foreground().forbid_parking();

crates/workspace/src/workspace.rs 🔗

@@ -40,17 +40,21 @@ use theme::{Theme, ThemeRegistry};
 action!(Open, Arc<AppState>);
 action!(OpenNew, Arc<AppState>);
 action!(OpenPaths, OpenParams);
+action!(JoinProject, JoinProjectParams);
 action!(Save);
 action!(DebugElements);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_global_action(open);
     cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
-        open_paths(&action.0.paths, &action.0.app_state, cx).detach()
+        open_paths(&action.0.paths, &action.0.app_state, cx).detach();
     });
     cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
         open_new(&action.0, cx)
     });
+    cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
+        join_project(action.0.project_id, &action.0.app_state, cx).detach();
+    });
 
     cx.add_action(Workspace::save_active_item);
     cx.add_action(Workspace::debug_elements);
@@ -90,8 +94,11 @@ pub struct AppState {
     pub channel_list: ModelHandle<client::ChannelList>,
     pub entry_openers: Arc<[Box<dyn EntryOpener>]>,
     pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>,
-    pub build_workspace:
-        &'static dyn Fn(&WorkspaceParams, &mut ViewContext<Workspace>) -> Workspace,
+    pub build_workspace: &'static dyn Fn(
+        ModelHandle<Project>,
+        &Arc<AppState>,
+        &mut ViewContext<Workspace>,
+    ) -> Workspace,
 }
 
 #[derive(Clone)]
@@ -100,6 +107,12 @@ pub struct OpenParams {
     pub app_state: Arc<AppState>,
 }
 
+#[derive(Clone)]
+pub struct JoinProjectParams {
+    pub project_id: u64,
+    pub app_state: Arc<AppState>,
+}
+
 pub trait EntryOpener {
     fn open(
         &self,
@@ -338,6 +351,7 @@ impl Clone for Box<dyn ItemHandle> {
 
 #[derive(Clone)]
 pub struct WorkspaceParams {
+    pub project: ModelHandle<Project>,
     pub client: Arc<Client>,
     pub fs: Arc<dyn Fs>,
     pub languages: Arc<LanguageRegistry>,
@@ -350,7 +364,8 @@ pub struct WorkspaceParams {
 impl WorkspaceParams {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut MutableAppContext) -> Self {
-        let languages = LanguageRegistry::new();
+        let fs = Arc::new(project::FakeFs::new());
+        let languages = Arc::new(LanguageRegistry::new());
         let client = Client::new();
         let http_client = client::test::FakeHttpClient::new(|_| async move {
             Ok(client::http::ServerResponse::new(404))
@@ -359,17 +374,45 @@ impl WorkspaceParams {
             gpui::fonts::with_font_cache(cx.font_cache().clone(), || theme::Theme::default());
         let settings = Settings::new("Courier", cx.font_cache(), Arc::new(theme)).unwrap();
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let project = Project::local(
+            client.clone(),
+            user_store.clone(),
+            languages.clone(),
+            fs.clone(),
+            cx,
+        );
         Self {
+            project,
             channel_list: cx
                 .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
             client,
-            fs: Arc::new(project::FakeFs::new()),
-            languages: Arc::new(languages),
+            fs,
+            languages,
             settings: watch::channel_with(settings).1,
             user_store,
             entry_openers: Arc::from([]),
         }
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn local(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Self {
+        Self {
+            project: Project::local(
+                app_state.client.clone(),
+                app_state.user_store.clone(),
+                app_state.languages.clone(),
+                app_state.fs.clone(),
+                cx,
+            ),
+            client: app_state.client.clone(),
+            fs: app_state.fs.clone(),
+            languages: app_state.languages.clone(),
+            settings: app_state.settings.clone(),
+            user_store: app_state.user_store.clone(),
+            channel_list: app_state.channel_list.clone(),
+            entry_openers: app_state.entry_openers.clone(),
+        }
+    }
 }
 
 pub struct Workspace {
@@ -392,14 +435,7 @@ pub struct Workspace {
 
 impl Workspace {
     pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
-        let project = Project::local(
-            params.client.clone(),
-            params.user_store.clone(),
-            params.languages.clone(),
-            params.fs.clone(),
-            cx,
-        );
-        cx.observe(&project, |_, _, cx| cx.notify()).detach();
+        cx.observe(&params.project, |_, _, cx| cx.notify()).detach();
 
         let pane = cx.add_view(|_| Pane::new(params.settings.clone()));
         let pane_id = pane.id();
@@ -445,7 +481,7 @@ impl Workspace {
             fs: params.fs.clone(),
             left_sidebar: Sidebar::new(Side::Left),
             right_sidebar: Sidebar::new(Side::Right),
-            project,
+            project: params.project.clone(),
             entry_openers: params.entry_openers.clone(),
             items: Default::default(),
             _observe_current_user,
@@ -1258,20 +1294,6 @@ impl std::fmt::Debug for OpenParams {
     }
 }
 
-impl<'a> From<&'a AppState> for WorkspaceParams {
-    fn from(state: &'a AppState) -> Self {
-        Self {
-            client: state.client.clone(),
-            fs: state.fs.clone(),
-            languages: state.languages.clone(),
-            settings: state.settings.clone(),
-            user_store: state.user_store.clone(),
-            channel_list: state.channel_list.clone(),
-            entry_openers: state.entry_openers.clone(),
-        }
-    }
-}
-
 fn open(action: &Open, cx: &mut MutableAppContext) {
     let app_state = action.0.clone();
     cx.prompt_for_paths(
@@ -1314,7 +1336,14 @@ pub fn open_paths(
 
     let workspace = existing.unwrap_or_else(|| {
         cx.add_window((app_state.build_window_options)(), |cx| {
-            (app_state.build_workspace)(&WorkspaceParams::from(app_state.as_ref()), cx)
+            let project = Project::local(
+                app_state.client.clone(),
+                app_state.user_store.clone(),
+                app_state.languages.clone(),
+                app_state.fs.clone(),
+                cx,
+            );
+            (app_state.build_workspace)(project, &app_state, cx)
         })
         .1
     });
@@ -1326,9 +1355,49 @@ pub fn open_paths(
     })
 }
 
+pub fn join_project(
+    project_id: u64,
+    app_state: &Arc<AppState>,
+    cx: &mut MutableAppContext,
+) -> Task<Result<ViewHandle<Workspace>>> {
+    for window_id in cx.window_ids().collect::<Vec<_>>() {
+        if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
+            if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
+                return Task::ready(Ok(workspace));
+            }
+        }
+    }
+
+    let app_state = app_state.clone();
+    cx.spawn(|mut cx| async move {
+        let project = Project::remote(
+            project_id,
+            app_state.client.clone(),
+            app_state.user_store.clone(),
+            app_state.languages.clone(),
+            app_state.fs.clone(),
+            &mut cx,
+        )
+        .await?;
+        let (_, workspace) = cx.update(|cx| {
+            cx.add_window((app_state.build_window_options)(), |cx| {
+                (app_state.build_workspace)(project, &app_state, cx)
+            })
+        });
+        Ok(workspace)
+    })
+}
+
 fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
     let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
-        (app_state.build_workspace)(&app_state.as_ref().into(), cx)
+        let project = Project::local(
+            app_state.client.clone(),
+            app_state.user_store.clone(),
+            app_state.languages.clone(),
+            app_state.fs.clone(),
+            cx,
+        );
+        (app_state.build_workspace)(project, &app_state, cx)
     });
     cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
 }

crates/zed/src/main.rs 🔗

@@ -58,7 +58,6 @@ fn main() {
         editor::init(cx, &mut entry_openers);
         go_to_line::init(cx);
         file_finder::init(cx);
-        contacts_panel::init(cx);
         chat_panel::init(cx);
         project_panel::init(cx);
         diagnostics::init(cx);

crates/zed/src/zed.rs 🔗

@@ -14,9 +14,10 @@ use gpui::{
     geometry::vector::vec2f,
     keymap::Binding,
     platform::{WindowBounds, WindowOptions},
-    ViewContext,
+    ModelHandle, ViewContext,
 };
 pub use lsp;
+use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use std::sync::Arc;
@@ -48,27 +49,39 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     ])
 }
 
-pub fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
-    let mut workspace = Workspace::new(params, cx);
+pub fn build_workspace(
+    project: ModelHandle<Project>,
+    app_state: &Arc<AppState>,
+    cx: &mut ViewContext<Workspace>,
+) -> Workspace {
+    let workspace_params = WorkspaceParams {
+        project,
+        client: app_state.client.clone(),
+        fs: app_state.fs.clone(),
+        languages: app_state.languages.clone(),
+        settings: app_state.settings.clone(),
+        user_store: app_state.user_store.clone(),
+        channel_list: app_state.channel_list.clone(),
+        entry_openers: app_state.entry_openers.clone(),
+    };
+    let mut workspace = Workspace::new(&workspace_params, cx);
     let project = workspace.project().clone();
     workspace.left_sidebar_mut().add_item(
         "icons/folder-tree-16.svg",
-        ProjectPanel::new(project, params.settings.clone(), cx).into(),
+        ProjectPanel::new(project, app_state.settings.clone(), cx).into(),
     );
     workspace.right_sidebar_mut().add_item(
         "icons/user-16.svg",
-        cx.add_view(|cx| {
-            ContactsPanel::new(params.user_store.clone(), params.settings.clone(), cx)
-        })
-        .into(),
+        cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx))
+            .into(),
     );
     workspace.right_sidebar_mut().add_item(
         "icons/comment-16.svg",
         cx.add_view(|cx| {
             ChatPanel::new(
-                params.client.clone(),
-                params.channel_list.clone(),
-                params.settings.clone(),
+                app_state.client.clone(),
+                app_state.channel_list.clone(),
+                app_state.settings.clone(),
                 cx,
             )
         })
@@ -76,9 +89,9 @@ pub fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>
     );
 
     let diagnostic =
-        cx.add_view(|_| editor::items::DiagnosticMessage::new(params.settings.clone()));
+        cx.add_view(|_| editor::items::DiagnosticMessage::new(app_state.settings.clone()));
     let cursor_position =
-        cx.add_view(|_| editor::items::CursorPosition::new(params.settings.clone()));
+        cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone()));
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic, cx);
         status_bar.add_right_item(cursor_position, cx);
@@ -225,8 +238,8 @@ mod tests {
                 }),
             )
             .await;
-
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         workspace
             .update(&mut cx, |workspace, cx| {
                 workspace.add_worktree(Path::new("/root"), cx)
@@ -340,7 +353,8 @@ mod tests {
         fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
         fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         workspace
             .update(&mut cx, |workspace, cx| {
                 workspace.add_worktree("/dir1".as_ref(), cx)
@@ -406,8 +420,8 @@ mod tests {
         let fs = app_state.fs.as_fake();
         fs.insert_tree("/root", json!({ "a.txt": "" })).await;
 
-        let (window_id, workspace) =
-            cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         workspace
             .update(&mut cx, |workspace, cx| {
                 workspace.add_worktree(Path::new("/root"), cx)
@@ -453,7 +467,7 @@ mod tests {
     async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
         let app_state = cx.update(test_app_state);
         app_state.fs.as_fake().insert_dir("/root").await.unwrap();
-        let params = app_state.as_ref().into();
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         workspace
             .update(&mut cx, |workspace, cx| {
@@ -570,7 +584,7 @@ mod tests {
     ) {
         let app_state = cx.update(test_app_state);
         app_state.fs.as_fake().insert_dir("/root").await.unwrap();
-        let params = app_state.as_ref().into();
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 
         // Create a new untitled buffer
@@ -628,8 +642,8 @@ mod tests {
             )
             .await;
 
-        let (window_id, workspace) =
-            cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
+        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         workspace
             .update(&mut cx, |workspace, cx| {
                 workspace.add_worktree(Path::new("/root"), cx)