remote projects: Allow reusing window (#11058)

Bennet Bo Fenner , Conrad , and Remco created

Release Notes:

- Allow reusing the window when opening a remote project from the recent
projects picker
- Fixed an issue, which would not let you rejoin a remote project after
disconnecting from it for the first time

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Remco <djsmits12@gmail.com>

Change summary

crates/collab/src/tests/dev_server_tests.rs   |  9 ++++
crates/recent_projects/src/recent_projects.rs | 39 +++++++++++++++++---
crates/recent_projects/src/remote_projects.rs |  2 
crates/semantic_index/src/semantic_index.rs   | 27 +++++++++++---
crates/workspace/src/workspace.rs             | 26 +++++++++----
5 files changed, 80 insertions(+), 23 deletions(-)

Detailed changes

crates/collab/src/tests/dev_server_tests.rs 🔗

@@ -70,6 +70,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
             workspace::join_remote_project(
                 projects[0].project_id.unwrap(),
                 client.app_state.clone(),
+                None,
                 cx,
             )
         })
@@ -205,7 +206,12 @@ async fn create_remote_project(
             let projects = store.remote_projects();
             assert_eq!(projects.len(), 1);
             assert_eq!(projects[0].path, "/remote");
-            workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
+            workspace::join_remote_project(
+                projects[0].project_id.unwrap(),
+                client_app_state,
+                None,
+                cx,
+            )
         })
         .await
         .unwrap();
@@ -301,6 +307,7 @@ async fn test_dev_server_reconnect(
             workspace::join_remote_project(
                 projects[0].project_id.unwrap(),
                 client2.app_state.clone(),
+                None,
                 cx,
             )
         })

crates/recent_projects/src/recent_projects.rs 🔗

@@ -310,7 +310,6 @@ impl PickerDelegate for RecentProjectsDelegate {
                                     workspace.open_workspace_for_paths(false, paths, cx)
                                 }
                             }
-                            //TODO support opening remote projects in the same window
                             SerializedWorkspaceLocation::Remote(remote_project) => {
                                 let store = ::remote_projects::Store::global(cx).read(cx);
                                 let Some(project_id) = store
@@ -338,12 +337,38 @@ impl PickerDelegate for RecentProjectsDelegate {
                                     })
                                 };
                                 if let Some(app_state) = AppState::global(cx).upgrade() {
-                                    let task =
-                                        workspace::join_remote_project(project_id, app_state, cx);
-                                    cx.spawn(|_, _| async move {
-                                        task.await?;
-                                        Ok(())
-                                    })
+                                    let handle = if replace_current_window {
+                                        cx.window_handle().downcast::<Workspace>()
+                                    } else {
+                                        None
+                                    };
+
+                                    if let Some(handle) = handle {
+                                            cx.spawn(move |workspace, mut cx| async move {
+                                                let continue_replacing = workspace
+                                                    .update(&mut cx, |workspace, cx| {
+                                                        workspace.
+                                                            prepare_to_close(true, cx)
+                                                    })?
+                                                    .await?;
+                                                if continue_replacing {
+                                                    workspace
+                                                        .update(&mut cx, |_workspace, cx| {
+                                                            workspace::join_remote_project(project_id, app_state, Some(handle), cx)
+                                                        })?
+                                                        .await?;
+                                                }
+                                                Ok(())
+                                            })
+                                        }
+                                    else {
+                                        let task =
+                                            workspace::join_remote_project(project_id, app_state, None, cx);
+                                        cx.spawn(|_, _| async move {
+                                            task.await?;
+                                            Ok(())
+                                        })
+                                    }
                                 } else {
                                     Task::ready(Err(anyhow::anyhow!("App state not found")))
                                 }

crates/recent_projects/src/remote_projects.rs 🔗

@@ -386,7 +386,7 @@ impl RemoteProjects {
             .on_click(cx.listener(move |_, _, cx| {
                 if let Some(project_id) = project_id {
                     if let Some(app_state) = AppState::global(cx).upgrade() {
-                        workspace::join_remote_project(project_id, app_state, cx)
+                        workspace::join_remote_project(project_id, app_state, None, cx)
                             .detach_and_prompt_err("Could not join project", cx, |_, _| None)
                     }
                 } else {

crates/semantic_index/src/semantic_index.rs 🔗

@@ -9,8 +9,8 @@ use fs::Fs;
 use futures::stream::StreamExt;
 use futures_batch::ChunksTimeoutStreamExt;
 use gpui::{
-    AppContext, AsyncAppContext, Context, EntityId, EventEmitter, Global, Model, ModelContext,
-    Subscription, Task, WeakModel,
+    AppContext, AsyncAppContext, BorrowAppContext, Context, Entity, EntityId, EventEmitter, Global,
+    Model, ModelContext, Subscription, Task, WeakModel,
 };
 use heed::types::{SerdeBincode, Str};
 use language::LanguageRegistry;
@@ -68,6 +68,18 @@ impl SemanticIndex {
         project: Model<Project>,
         cx: &mut AppContext,
     ) -> Model<ProjectIndex> {
+        let project_weak = project.downgrade();
+        project.update(cx, move |_, cx| {
+            cx.on_release(move |_, cx| {
+                if cx.has_global::<SemanticIndex>() {
+                    cx.update_global::<SemanticIndex, _>(|this, _| {
+                        this.project_indices.remove(&project_weak);
+                    })
+                }
+            })
+            .detach();
+        });
+
         self.project_indices
             .entry(project.downgrade())
             .or_insert_with(|| {
@@ -86,7 +98,7 @@ impl SemanticIndex {
 
 pub struct ProjectIndex {
     db_connection: heed::Env,
-    project: Model<Project>,
+    project: WeakModel<Project>,
     worktree_indices: HashMap<EntityId, WorktreeIndexHandle>,
     language_registry: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
@@ -116,7 +128,7 @@ impl ProjectIndex {
         let fs = project.read(cx).fs().clone();
         let mut this = ProjectIndex {
             db_connection,
-            project: project.clone(),
+            project: project.downgrade(),
             worktree_indices: HashMap::default(),
             language_registry,
             fs,
@@ -143,8 +155,11 @@ impl ProjectIndex {
     }
 
     fn update_worktree_indices(&mut self, cx: &mut ModelContext<Self>) {
-        let worktrees = self
-            .project
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+
+        let worktrees = project
             .read(cx)
             .visible_worktrees(cx)
             .filter_map(|worktree| {

crates/workspace/src/workspace.rs 🔗

@@ -4785,6 +4785,7 @@ pub fn join_hosted_project(
 pub fn join_remote_project(
     project_id: ProjectId,
     app_state: Arc<AppState>,
+    window_to_replace: Option<WindowHandle<Workspace>>,
     cx: &mut AppContext,
 ) -> Task<Result<WindowHandle<Workspace>>> {
     let windows = cx.windows();
@@ -4816,16 +4817,25 @@ pub fn join_remote_project(
             )
             .await?;
 
-            let window_bounds_override = window_bounds_env_override();
-            cx.update(|cx| {
-                let mut options = (app_state.build_window_options)(None, cx);
-                options.bounds = window_bounds_override;
-                cx.open_window(options, |cx| {
-                    cx.new_view(|cx| {
+            if let Some(window_to_replace) = window_to_replace {
+                cx.update_window(window_to_replace.into(), |_, cx| {
+                    cx.replace_root_view(|cx| {
                         Workspace::new(Default::default(), project, app_state.clone(), cx)
+                    });
+                })?;
+                window_to_replace
+            } else {
+                let window_bounds_override = window_bounds_env_override();
+                cx.update(|cx| {
+                    let mut options = (app_state.build_window_options)(None, cx);
+                    options.bounds = window_bounds_override;
+                    cx.open_window(options, |cx| {
+                        cx.new_view(|cx| {
+                            Workspace::new(Default::default(), project, app_state.clone(), cx)
+                        })
                     })
-                })
-            })?
+                })?
+            }
         };
 
         workspace.update(&mut cx, |_, cx| {