ssh remoting: Fix cmd-o (#18308)

Conrad Irwin and Mikayla created

Release Notes:

- ssh-remoting: Cmd-O now correctly opens files on the remote host

---------

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/assistant/src/context_store.rs                         |  6 
crates/collab/src/tests/random_project_collaboration_tests.rs | 11 
crates/editor/src/editor.rs                                   |  2 
crates/feedback/src/feedback_modal.rs                         | 69 ++--
crates/file_finder/src/file_finder.rs                         |  2 
crates/language_tools/src/lsp_log.rs                          |  4 
crates/outline_panel/src/outline_panel.rs                     |  2 
crates/project/src/project.rs                                 | 46 +-
crates/project_panel/src/project_panel.rs                     |  5 
crates/tasks_ui/src/lib.rs                                    |  2 
crates/tasks_ui/src/modal.rs                                  |  2 
crates/terminal_view/src/terminal_panel.rs                    |  2 
crates/title_bar/src/collab.rs                                |  8 
crates/workspace/src/workspace.rs                             | 12 
crates/zed/src/zed.rs                                         |  2 
15 files changed, 84 insertions(+), 91 deletions(-)

Detailed changes

crates/assistant/src/context_store.rs 🔗

@@ -357,9 +357,6 @@ impl ContextStore {
         let Some(project_id) = project.remote_id() else {
             return Task::ready(Err(anyhow!("project was not remote")));
         };
-        if project.is_local_or_ssh() {
-            return Task::ready(Err(anyhow!("cannot create remote contexts as the host")));
-        }
 
         let replica_id = project.replica_id();
         let capability = project.capability();
@@ -488,9 +485,6 @@ impl ContextStore {
         let Some(project_id) = project.remote_id() else {
             return Task::ready(Err(anyhow!("project was not remote")));
         };
-        if project.is_local_or_ssh() {
-            return Task::ready(Err(anyhow!("cannot open remote contexts as the host")));
-        }
 
         if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
             return Task::ready(Ok(context));

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

@@ -298,8 +298,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                                 continue;
                             };
                             let project_root_name = root_name_for_project(&project, cx);
-                            let is_local =
-                                project.read_with(cx, |project, _| project.is_local_or_ssh());
+                            let is_local = project.read_with(cx, |project, _| project.is_local());
                             let worktree = project.read_with(cx, |project, cx| {
                                 project
                                     .worktrees(cx)
@@ -335,7 +334,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                         continue;
                     };
                     let project_root_name = root_name_for_project(&project, cx);
-                    let is_local = project.read_with(cx, |project, _| project.is_local_or_ssh());
+                    let is_local = project.read_with(cx, |project, _| project.is_local());
 
                     match rng.gen_range(0..100_u32) {
                         // Manipulate an existing buffer
@@ -1256,7 +1255,7 @@ impl RandomizedTest for ProjectCollaborationTest {
             let buffers = client.buffers().clone();
             for (guest_project, guest_buffers) in &buffers {
                 let project_id = if guest_project.read_with(client_cx, |project, _| {
-                    project.is_local_or_ssh() || project.is_disconnected()
+                    project.is_local() || project.is_disconnected()
                 }) {
                     continue;
                 } else {
@@ -1560,9 +1559,7 @@ async fn ensure_project_shared(
     let first_root_name = root_name_for_project(project, cx);
     let active_call = cx.read(ActiveCall::global);
     if active_call.read_with(cx, |call, _| call.room().is_some())
-        && project.read_with(cx, |project, _| {
-            project.is_local_or_ssh() && !project.is_shared()
-        })
+        && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
     {
         match active_call
             .update(cx, |call, cx| call.share_project(project.clone(), cx))

crates/editor/src/editor.rs 🔗

@@ -11819,7 +11819,7 @@ impl Editor {
                             .filter_map(|buffer| {
                                 let buffer = buffer.read(cx);
                                 let language = buffer.language()?;
-                                if project.is_local_or_ssh()
+                                if project.is_local()
                                     && project.language_servers_for_buffer(buffer, cx).count() == 0
                                 {
                                     None

crates/feedback/src/feedback_modal.rs 🔗

@@ -18,8 +18,7 @@ use regex::Regex;
 use serde_derive::Serialize;
 use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
 use util::ResultExt;
-use workspace::notifications::NotificationId;
-use workspace::{DismissDecision, ModalView, Toast, Workspace};
+use workspace::{DismissDecision, ModalView, Workspace};
 
 use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
 
@@ -120,44 +119,34 @@ impl FeedbackModal {
     pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
         let _handle = cx.view().downgrade();
         workspace.register_action(move |workspace, _: &GiveFeedback, cx| {
-            let markdown = workspace
-                .app_state()
-                .languages
-                .language_for_name("Markdown");
-
-            let project = workspace.project().clone();
-            let is_local_project = project.read(cx).is_local_or_ssh();
-
-            if !is_local_project {
-                struct FeedbackInRemoteProject;
-
-                workspace.show_toast(
-                    Toast::new(
-                        NotificationId::unique::<FeedbackInRemoteProject>(),
-                        "You can only submit feedback in your own project.",
-                    ),
-                    cx,
-                );
-                return;
-            }
-
-            let system_specs = SystemSpecs::new(cx);
-            cx.spawn(|workspace, mut cx| async move {
-                let markdown = markdown.await.log_err();
-                let buffer = project.update(&mut cx, |project, cx| {
-                    project.create_local_buffer("", markdown, cx)
-                })?;
-                let system_specs = system_specs.await;
-
-                workspace.update(&mut cx, |workspace, cx| {
-                    workspace.toggle_modal(cx, move |cx| {
-                        FeedbackModal::new(system_specs, project, buffer, cx)
-                    });
-                })?;
-
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
+            workspace
+                .with_local_workspace(cx, |workspace, cx| {
+                    let markdown = workspace
+                        .app_state()
+                        .languages
+                        .language_for_name("Markdown");
+
+                    let project = workspace.project().clone();
+
+                    let system_specs = SystemSpecs::new(cx);
+                    cx.spawn(|workspace, mut cx| async move {
+                        let markdown = markdown.await.log_err();
+                        let buffer = project.update(&mut cx, |project, cx| {
+                            project.create_local_buffer("", markdown, cx)
+                        })?;
+                        let system_specs = system_specs.await;
+
+                        workspace.update(&mut cx, |workspace, cx| {
+                            workspace.toggle_modal(cx, move |cx| {
+                                FeedbackModal::new(system_specs, project, buffer, cx)
+                            });
+                        })?;
+
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                })
+                .detach_and_log_err(cx);
         });
     }
 

crates/file_finder/src/file_finder.rs 🔗

@@ -884,7 +884,7 @@ impl PickerDelegate for FileFinderDelegate {
                         project
                             .worktree_for_id(history_item.project.worktree_id, cx)
                             .is_some()
-                            || (project.is_local_or_ssh() && history_item.absolute.is_some())
+                            || (project.is_local() && history_item.absolute.is_some())
                     }),
                     self.currently_opened_path.as_ref(),
                     None,

crates/language_tools/src/lsp_log.rs 🔗

@@ -184,7 +184,7 @@ pub fn init(cx: &mut AppContext) {
 
     cx.observe_new_views(move |workspace: &mut Workspace, cx| {
         let project = workspace.project();
-        if project.read(cx).is_local_or_ssh() {
+        if project.read(cx).is_local() {
             log_store.update(cx, |store, cx| {
                 store.add_project(project, cx);
             });
@@ -193,7 +193,7 @@ pub fn init(cx: &mut AppContext) {
         let log_store = log_store.clone();
         workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
             let project = workspace.project().read(cx);
-            if project.is_local_or_ssh() {
+            if project.is_local() {
                 workspace.add_item_to_active_pane(
                     Box::new(cx.new_view(|cx| {
                         LspLogView::new(workspace.project().clone(), log_store.clone(), cx)

crates/outline_panel/src/outline_panel.rs 🔗

@@ -3909,7 +3909,7 @@ impl Render for OutlinePanel {
             .when(project.is_local(), |el| {
                 el.on_action(cx.listener(Self::reveal_in_finder))
             })
-            .when(project.is_local_or_ssh(), |el| {
+            .when(project.is_local() || project.is_via_ssh(), |el| {
                 el.on_action(cx.listener(Self::open_in_terminal))
             })
             .on_mouse_down(

crates/project/src/project.rs 🔗

@@ -487,7 +487,7 @@ impl DirectoryLister {
     pub fn is_local(&self, cx: &AppContext) -> bool {
         match self {
             DirectoryLister::Local(_) => true,
-            DirectoryLister::Project(project) => project.read(cx).is_local_or_ssh(),
+            DirectoryLister::Project(project) => project.read(cx).is_local(),
         }
     }
 
@@ -1199,7 +1199,13 @@ impl Project {
         self.dev_server_project_id
     }
 
-    pub fn supports_remote_terminal(&self, cx: &AppContext) -> bool {
+    pub fn supports_terminal(&self, cx: &AppContext) -> bool {
+        if self.is_local() {
+            return true;
+        }
+        if self.is_via_ssh() {
+            return true;
+        }
         let Some(id) = self.dev_server_project_id else {
             return false;
         };
@@ -1213,10 +1219,6 @@ impl Project {
     }
 
     pub fn ssh_connection_string(&self, cx: &ModelContext<Self>) -> Option<SharedString> {
-        if self.is_local_or_ssh() {
-            return None;
-        }
-
         let dev_server_id = self.dev_server_project_id()?;
         dev_server_projects::Store::global(cx)
             .read(cx)
@@ -1643,13 +1645,6 @@ impl Project {
         }
     }
 
-    pub fn is_local_or_ssh(&self) -> bool {
-        match &self.client_state {
-            ProjectClientState::Local | ProjectClientState::Shared { .. } => true,
-            ProjectClientState::Remote { .. } => false,
-        }
-    }
-
     pub fn is_via_ssh(&self) -> bool {
         match &self.client_state {
             ProjectClientState::Local | ProjectClientState::Shared { .. } => {
@@ -1735,7 +1730,7 @@ impl Project {
     ) -> Task<Result<Model<Buffer>>> {
         if let Some(buffer) = self.buffer_for_id(id, cx) {
             Task::ready(Ok(buffer))
-        } else if self.is_local_or_ssh() {
+        } else if self.is_local() || self.is_via_ssh() {
             Task::ready(Err(anyhow!("buffer {} does not exist", id)))
         } else if let Some(project_id) = self.remote_id() {
             let request = self.client.request(proto::OpenBufferById {
@@ -1857,7 +1852,7 @@ impl Project {
         let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
 
         while let Some(changes) = changes.next().await {
-            let is_local = this.update(&mut cx, |this, _| this.is_local_or_ssh())?;
+            let is_local = this.update(&mut cx, |this, _| this.is_local())?;
 
             for change in changes {
                 match change {
@@ -2001,7 +1996,7 @@ impl Project {
                 language_server_id,
                 message,
             } => {
-                if self.is_local_or_ssh() {
+                if self.is_local() {
                     self.enqueue_buffer_ordered_message(
                         BufferOrderedMessage::LanguageServerUpdate {
                             language_server_id: *language_server_id,
@@ -3039,8 +3034,19 @@ impl Project {
         query: String,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<PathBuf>>> {
-        if self.is_local_or_ssh() {
+        if self.is_local() {
             DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
+        } else if let Some(session) = self.ssh_session.as_ref() {
+            let request = proto::ListRemoteDirectory {
+                dev_server_id: SSH_PROJECT_ID,
+                path: query,
+            };
+
+            let response = session.request(request);
+            cx.background_executor().spawn(async move {
+                let response = response.await?;
+                Ok(response.entries.into_iter().map(PathBuf::from).collect())
+            })
         } else if let Some(dev_server) = self.dev_server_project_id().and_then(|id| {
             dev_server_projects::Store::global(cx)
                 .read(cx)
@@ -3317,7 +3323,7 @@ impl Project {
         mut cx: AsyncAppContext,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
-            if this.is_local_or_ssh() {
+            if this.is_local() || this.is_via_ssh() {
                 this.unshare(cx)?;
             } else {
                 this.disconnected_from_host(cx);
@@ -3995,7 +4001,7 @@ impl Project {
         location: Location,
         cx: &mut ModelContext<'_, Project>,
     ) -> Task<Option<TaskContext>> {
-        if self.is_local_or_ssh() {
+        if self.is_local() {
             let (worktree_id, worktree_abs_path) = if let Some(worktree) = self.task_worktree(cx) {
                 (
                     Some(worktree.read(cx).id()),
@@ -4081,7 +4087,7 @@ impl Project {
         location: Option<Location>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<(TaskSourceKind, TaskTemplate)>>> {
-        if self.is_local_or_ssh() {
+        if self.is_local() {
             let (file, language) = location
                 .map(|location| {
                     let buffer = location.buffer.read(cx);

crates/project_panel/src/project_panel.rs 🔗

@@ -2722,11 +2722,14 @@ impl Render for ProjectPanel {
                             }
                         }))
                 })
-                .when(project.is_local_or_ssh(), |el| {
+                .when(project.is_local(), |el| {
                     el.on_action(cx.listener(Self::reveal_in_finder))
                         .on_action(cx.listener(Self::open_system))
                         .on_action(cx.listener(Self::open_in_terminal))
                 })
+                .when(project.is_via_ssh(), |el| {
+                    el.on_action(cx.listener(Self::open_in_terminal))
+                })
                 .on_mouse_down(
                     MouseButton::Right,
                     cx.listener(move |this, event: &MouseDownEvent, cx| {

crates/tasks_ui/src/lib.rs 🔗

@@ -94,7 +94,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>)
         workspace
             .update(&mut cx, |workspace, cx| {
                 if workspace.project().update(cx, |project, cx| {
-                    project.is_local_or_ssh() || project.ssh_connection_string(cx).is_some()
+                    project.is_local() || project.ssh_connection_string(cx).is_some()
                 }) {
                     workspace.toggle_modal(cx, |cx| {
                         TasksModal::new(project, task_context, workspace_handle, cx)

crates/tasks_ui/src/modal.rs 🔗

@@ -225,7 +225,7 @@ impl PickerDelegate for TasksModalDelegate {
                                     if project.is_via_collab() && ssh_connection_string.is_none() {
                                         Task::ready((Vec::new(), Vec::new()))
                                     } else {
-                                        let remote_templates = if project.is_local_or_ssh() {
+                                        let remote_templates = if project.is_local() {
                                             None
                                         } else {
                                             project

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -144,7 +144,7 @@ impl TerminalPanel {
             cx.subscribe(&pane, Self::handle_pane_event),
         ];
         let project = workspace.project().read(cx);
-        let enabled = project.is_local_or_ssh() || project.supports_remote_terminal(cx);
+        let enabled = project.supports_terminal(cx);
         let this = Self {
             pane,
             fs: workspace.app_state().fs.clone(),

crates/title_bar/src/collab.rs 🔗

@@ -284,14 +284,14 @@ impl TitleBar {
 
         let room = room.read(cx);
         let project = self.project.read(cx);
-        let is_local = project.is_local_or_ssh();
         let is_dev_server_project = project.dev_server_project_id().is_some();
-        let is_shared = (is_local || is_dev_server_project) && project.is_shared();
+        let is_shared = project.is_shared();
         let is_muted = room.is_muted();
         let is_deafened = room.is_deafened().unwrap_or(false);
         let is_screen_sharing = room.is_screen_sharing();
         let can_use_microphone = room.can_use_microphone();
-        let can_share_projects = room.can_share_projects();
+        let can_share_projects = room.can_share_projects()
+            && (is_dev_server_project || project.is_local() || project.is_via_ssh());
         let platform_supported = match self.platform_style {
             PlatformStyle::Mac => true,
             PlatformStyle::Linux | PlatformStyle::Windows => false,
@@ -299,7 +299,7 @@ impl TitleBar {
 
         let mut children = Vec::new();
 
-        if (is_local || is_dev_server_project) && can_share_projects {
+        if can_share_projects {
             children.push(
                 Button::new(
                     "toggle_sharing",

crates/workspace/src/workspace.rs 🔗

@@ -1891,7 +1891,11 @@ impl Workspace {
                 directories: true,
                 multiple: true,
             },
-            DirectoryLister::Local(self.app_state.fs.clone()),
+            if self.project.read(cx).is_via_ssh() {
+                DirectoryLister::Project(self.project.clone())
+            } else {
+                DirectoryLister::Local(self.app_state.fs.clone())
+            },
             cx,
         );
 
@@ -3956,7 +3960,7 @@ impl Workspace {
     fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
         let project = self.project().read(cx);
 
-        if project.is_local_or_ssh() {
+        if project.is_local() {
             Some(
                 project
                     .visible_worktrees(cx)
@@ -5160,7 +5164,7 @@ async fn join_channel_internal(
                         return None;
                     }
 
-                    if (project.is_local_or_ssh() || is_dev_server)
+                    if (project.is_local() || project.is_via_ssh() || is_dev_server)
                         && project.visible_worktrees(cx).any(|tree| {
                             tree.read(cx)
                                 .root_entry()
@@ -5314,7 +5318,7 @@ pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>>
         .filter(|workspace| {
             workspace
                 .read(cx)
-                .is_ok_and(|workspace| workspace.project.read(cx).is_local_or_ssh())
+                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
         })
         .collect()
 }

crates/zed/src/zed.rs 🔗

@@ -230,7 +230,7 @@ pub fn initialize_workspace(
 
         let project = workspace.project().clone();
         if project.update(cx, |project, cx| {
-            project.is_local_or_ssh() || project.ssh_connection_string(cx).is_some()
+            project.is_local() || project.is_via_ssh() || project.ssh_connection_string(cx).is_some()
         }) {
             project.update(cx, |project, cx| {
                 let fs = app_state.fs.clone();