ssh remoting: Show error message if project path does not exist (#18343)

Thorsten Ball , Bennet , and Conrad created

This now shows an error message if you try open a project over SSH that
doesn't exist. If it's a possible file-path though, it acts like Zed's
`cli` and opens the file so that it can be created.

- Works: `cargo run ssh://127.0.0.1/~/folder-exists/file-does-not-exist`
— this will open `file-does-not-exist`
- Shows error: `cargo run
ssh://127.0.0.1/~/folder-does-not-exist/file-does-not-exist` — this will
show an error

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>

Change summary

crates/project/src/worktree_store.rs         |  4 +-
crates/remote_server/Cargo.toml              |  1 
crates/remote_server/src/headless_project.rs | 25 ++++++++++++++++
crates/workspace/src/workspace.rs            | 33 +++++++++++++++++----
4 files changed, 53 insertions(+), 10 deletions(-)

Detailed changes

crates/project/src/worktree_store.rs 🔗

@@ -18,7 +18,7 @@ use gpui::{
 use postage::oneshot;
 use rpc::{
     proto::{self, SSH_PROJECT_ID},
-    AnyProtoClient, TypedEnvelope,
+    AnyProtoClient, ErrorExt, TypedEnvelope,
 };
 use smol::{
     channel::{Receiver, Sender},
@@ -207,7 +207,7 @@ impl WorktreeStore {
         cx.background_executor().spawn(async move {
             match task.await {
                 Ok(worktree) => Ok(worktree),
-                Err(err) => Err(anyhow!("{}", err)),
+                Err(err) => Err((*err).cloned()),
             }
         })
     }

crates/remote_server/Cargo.toml 🔗

@@ -22,6 +22,7 @@ test-support = ["fs/test-support"]
 
 [dependencies]
 anyhow.workspace = true
+client.workspace = true
 env_logger.workspace = true
 fs.workspace = true
 futures.workspace = true

crates/remote_server/src/headless_project.rs 🔗

@@ -189,11 +189,34 @@ impl HeadlessProject {
         message: TypedEnvelope<proto::AddWorktree>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::AddWorktreeResponse> {
+        use client::ErrorCodeExt;
         let path = shellexpand::tilde(&message.payload.path).to_string();
+
+        let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?;
+        let path = PathBuf::from(path);
+
+        let canonicalized = match fs.canonicalize(&path).await {
+            Ok(path) => path,
+            Err(e) => {
+                let mut parent = path
+                    .parent()
+                    .ok_or(e)
+                    .map_err(|_| anyhow!("{:?} does not exist", path))?;
+                if parent == Path::new("") {
+                    parent = util::paths::home_dir();
+                }
+                let parent = fs.canonicalize(parent).await.map_err(|_| {
+                    anyhow!(proto::ErrorCode::DevServerProjectPathDoesNotExist
+                        .with_tag("path", &path.to_string_lossy().as_ref()))
+                })?;
+                parent.join(path.file_name().unwrap())
+            }
+        };
+
         let worktree = this
             .update(&mut cx.clone(), |this, _| {
                 Worktree::local(
-                    Path::new(&path),
+                    Arc::from(canonicalized),
                     true,
                     this.fs.clone(),
                     this.next_entry_id.clone(),

crates/workspace/src/workspace.rs 🔗

@@ -5544,12 +5544,21 @@ pub fn open_ssh_project(
             )
         })?;
 
+        let mut project_paths_to_open = vec![];
+        let mut project_path_errors = vec![];
+
         for path in paths {
-            project
-                .update(&mut cx, |project, cx| {
-                    project.find_or_create_worktree(&path, true, cx)
-                })?
-                .await?;
+            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 serialized_workspace =
@@ -5576,11 +5585,21 @@ pub fn open_ssh_project(
             .update(&mut cx, |_, cx| {
                 cx.activate_window();
 
-                open_items(serialized_workspace, vec![], app_state, cx)
+                open_items(serialized_workspace, project_paths_to_open, app_state, cx)
             })?
             .await?;
 
-        Ok(())
+        window.update(&mut 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)
+                }
+            }
+        })
     })
 }