workspace: Fix SSH remote restore on second open + Fix panel not opening automatically on new SSH remote (#27830)

Smit Barmase created

Closes #26902

- We used to serialize SSH remote only when opened via recent entries,
and not on first time. This broke restore, when opening same folder for
second time from recent entries. Once opened for second time, restoring
used to. work correctly. This PR fixes this by serializing when opened
for first time.

- We didn't handle window replace post worktree creation in first time
flow, this resulted in project panel not opening automatically like it
does with recent entries, or local projects. This PR fixes it by
following same flow as recent entries.

Release Notes:

- Fixed SSH remote not restoring when opening for second time.
- Fixed project panel not opening when opening new SSH remote folder.

Change summary

crates/recent_projects/src/remote_servers.rs  |  51 ++---
crates/recent_projects/src/ssh_connections.rs |   2 
crates/workspace/src/workspace.rs             | 158 +++++++++++++-------
3 files changed, 127 insertions(+), 84 deletions(-)

Detailed changes

crates/recent_projects/src/remote_servers.rs 🔗

@@ -32,7 +32,10 @@ use util::ResultExt;
 use workspace::OpenOptions;
 use workspace::Toast;
 use workspace::notifications::NotificationId;
-use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
+use workspace::{
+    ModalView, Workspace, notifications::DetachAndPromptErr,
+    open_ssh_project_with_existing_connection,
+};
 
 use crate::OpenRemote;
 use crate::ssh_connections::RemoteSettingsContent;
@@ -157,13 +160,8 @@ impl ProjectPicker {
                     let app_state = workspace
                         .update(cx, |workspace, _| workspace.app_state().clone())
                         .ok()?;
-                    let options = cx
-                        .update(|_, cx| (app_state.build_window_options)(None, cx))
-                        .log_err()?;
-
-                    cx.open_window(options, |window, cx| {
-                        window.activate_window();
 
+                    cx.update(|_, cx| {
                         let fs = app_state.fs.clone();
                         update_settings_file::<SshSettings>(fs, cx, {
                             let paths = paths
@@ -180,32 +178,27 @@ impl ProjectPicker {
                                 }
                             }
                         });
+                    })
+                    .log_err();
 
-                        let tasks = paths
-                            .into_iter()
-                            .map(|path| {
-                                project.update(cx, |project, cx| {
-                                    project.find_or_create_worktree(&path, true, cx)
-                                })
-                            })
-                            .collect::<Vec<_>>();
-                        window
-                            .spawn(cx, async move |_| {
-                                for task in tasks {
-                                    task.await?;
-                                }
-                                Ok(())
+                    let options = cx
+                        .update(|_, cx| (app_state.build_window_options)(None, cx))
+                        .log_err()?;
+                    let window = cx
+                        .open_window(options, |window, cx| {
+                            cx.new(|cx| {
+                                telemetry::event!("SSH Project Created");
+                                Workspace::new(None, project.clone(), app_state.clone(), window, cx)
                             })
-                            .detach_and_prompt_err("Failed to open path", window, cx, |_, _, _| {
-                                None
-                            });
-
-                        cx.new(|cx| {
-                            telemetry::event!("SSH Project Created");
-                            Workspace::new(None, project.clone(), app_state.clone(), window, cx)
                         })
-                    })
+                        .log_err()?;
+
+                    open_ssh_project_with_existing_connection(
+                        connection, project, paths, app_state, window, cx,
+                    )
+                    .await
                     .log_err();
+
                     this.update(cx, |_, cx| {
                         cx.emit(DismissEvent);
                     })

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -599,7 +599,7 @@ pub async fn open_ssh_project(
 
         let did_open_ssh_project = cx
             .update(|cx| {
-                workspace::open_ssh_project(
+                workspace::open_ssh_project_with_new_connection(
                     window,
                     connection_options.clone(),
                     cancel_rx,

crates/workspace/src/workspace.rs 🔗

@@ -6279,7 +6279,7 @@ pub fn create_and_open_local_file(
     })
 }
 
-pub fn open_ssh_project(
+pub fn open_ssh_project_with_new_connection(
     window: WindowHandle<Workspace>,
     connection_options: SshConnectionOptions,
     cancel_rx: oneshot::Receiver<()>,
@@ -6320,68 +6320,118 @@ pub fn open_ssh_project(
             )
         })?;
 
-        let toolchains = DB.toolchains(workspace_id).await?;
-        for (toolchain, worktree_id, path) in toolchains {
-            project
-                .update(cx, |this, cx| {
-                    this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
-                })?
-                .await;
-        }
-        let mut project_paths_to_open = vec![];
-        let mut project_path_errors = vec![];
+        open_ssh_project_inner(
+            project,
+            paths,
+            serialized_ssh_project,
+            workspace_id,
+            serialized_workspace,
+            app_state,
+            window,
+            cx,
+        )
+        .await
+    })
+}
 
-        for path in paths {
-            let result = cx
-                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
-                .await;
-            match result {
-                Ok((_, project_path)) => {
-                    project_paths_to_open.push((path.clone(), Some(project_path)));
-                }
-                Err(error) => {
-                    project_path_errors.push(error);
-                }
-            };
-        }
+pub fn open_ssh_project_with_existing_connection(
+    connection_options: SshConnectionOptions,
+    project: Entity<Project>,
+    paths: Vec<PathBuf>,
+    app_state: Arc<AppState>,
+    window: WindowHandle<Workspace>,
+    cx: &mut AsyncApp,
+) -> Task<Result<()>> {
+    cx.spawn(async move |cx| {
+        let (serialized_ssh_project, workspace_id, serialized_workspace) =
+            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
 
-        if project_paths_to_open.is_empty() {
-            return Err(project_path_errors
-                .pop()
-                .unwrap_or_else(|| anyhow!("no paths given")));
-        }
+        open_ssh_project_inner(
+            project,
+            paths,
+            serialized_ssh_project,
+            workspace_id,
+            serialized_workspace,
+            app_state,
+            window,
+            cx,
+        )
+        .await
+    })
+}
+
+async fn open_ssh_project_inner(
+    project: Entity<Project>,
+    paths: Vec<PathBuf>,
+    serialized_ssh_project: SerializedSshProject,
+    workspace_id: WorkspaceId,
+    serialized_workspace: Option<SerializedWorkspace>,
+    app_state: Arc<AppState>,
+    window: WindowHandle<Workspace>,
+    cx: &mut AsyncApp,
+) -> Result<()> {
+    let toolchains = DB.toolchains(workspace_id).await?;
+    for (toolchain, worktree_id, path) in toolchains {
+        project
+            .update(cx, |this, cx| {
+                this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
+            })?
+            .await;
+    }
+    let mut project_paths_to_open = vec![];
+    let mut project_path_errors = vec![];
 
-        cx.update_window(window.into(), |_, window, cx| {
-            window.replace_root(cx, |window, cx| {
-                telemetry::event!("SSH Project Opened");
+    for path in paths {
+        let result = cx
+            .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
+            .await;
+        match result {
+            Ok((_, project_path)) => {
+                project_paths_to_open.push((path.clone(), Some(project_path)));
+            }
+            Err(error) => {
+                project_path_errors.push(error);
+            }
+        };
+    }
 
-                let mut workspace =
-                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
-                workspace.set_serialized_ssh_project(serialized_ssh_project);
-                workspace
-            });
-        })?;
+    if project_paths_to_open.is_empty() {
+        return Err(project_path_errors
+            .pop()
+            .unwrap_or_else(|| anyhow!("no paths given")));
+    }
 
-        window
-            .update(cx, |_, window, cx| {
-                window.activate_window();
+    cx.update_window(window.into(), |_, window, cx| {
+        window.replace_root(cx, |window, cx| {
+            telemetry::event!("SSH Project Opened");
 
-                open_items(serialized_workspace, project_paths_to_open, window, cx)
-            })?
-            .await?;
+            let mut workspace =
+                Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
+            workspace.set_serialized_ssh_project(serialized_ssh_project);
+            workspace
+        });
+    })?;
 
-        window.update(cx, |workspace, _, cx| {
-            for error in project_path_errors {
-                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
-                    if let Some(path) = error.error_tag("path") {
-                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
-                    }
-                } else {
-                    workspace.show_error(&error, cx)
+    window
+        .update(cx, |_, window, cx| {
+            window.activate_window();
+            open_items(serialized_workspace, project_paths_to_open, window, cx)
+        })?
+        .await?;
+
+    window.update(cx, |workspace, _, cx| {
+        for error in project_path_errors {
+            if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
+                if let Some(path) = error.error_tag("path") {
+                    workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
                 }
+            } else {
+                workspace.show_error(&error, cx)
             }
-        })
-    })
+        }
+    })?;
+
+    Ok(())
 }
 
 fn serialize_ssh_project(