project_panel: Show Reveal in File Manager on wsl (#47288)

Lukas Wirth and Max Malkin created

Assimilates https://github.com/zed-industries/zed/pull/46856 and adds
support for reveal file manager in the project panel on wsl. Closes
https://github.com/zed-industries/zed/pull/46856

Release Notes:

- Fixed "Reveal in File Manager" not working for WSL remote connections
on Windows.
- Show "Reveal in File Manager" in the project panel context menu on WSL

---------

Co-authored-by: Max Malkin <maxim_malkin@outlook.com>

Change summary

crates/editor/src/editor.rs               |  8 ++++++--
crates/editor/src/mouse_context_menu.rs   |  2 ++
crates/outline_panel/src/outline_panel.rs | 19 ++++++++++++-------
crates/project/src/project.rs             | 23 +++++++++++++++++++++++
crates/project_panel/src/project_panel.rs | 22 +++++++++++++++-------
crates/remote/src/remote.rs               |  2 ++
crates/remote/src/transport/wsl.rs        | 16 ++++++++++++++++
7 files changed, 76 insertions(+), 16 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -22248,8 +22248,12 @@ impl Editor {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some(target) = self.target_file(cx) {
-            cx.reveal_path(&target.abs_path(cx));
+        if let Some(path) = self.target_file_abs_path(cx) {
+            if let Some(project) = self.project() {
+                project.update(cx, |project, cx| project.reveal_path(&path, cx));
+            } else {
+                cx.reveal_path(&path);
+            }
         }
     }
 

crates/editor/src/mouse_context_menu.rs 🔗

@@ -265,6 +265,8 @@ pub fn deploy_context_menu(
                     !has_reveal_target,
                     if cfg!(target_os = "macos") {
                         "Reveal in Finder"
+                    } else if cfg!(target_os = "windows") {
+                        "Reveal in File Explorer"
                     } else {
                         "Reveal in File Manager"
                     },

crates/outline_panel/src/outline_panel.rs 🔗

@@ -1438,12 +1438,16 @@ impl OutlinePanel {
 
         let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
             menu.context(self.focus_handle.clone())
-                .when(cfg!(target_os = "macos"), |menu| {
-                    menu.action("Reveal in Finder", Box::new(RevealInFileManager))
-                })
-                .when(cfg!(not(target_os = "macos")), |menu| {
-                    menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
-                })
+                .action(
+                    if cfg!(target_os = "macos") {
+                        "Reveal in Finder"
+                    } else if cfg!(target_os = "windows") {
+                        "Reveal in File Explorer"
+                    } else {
+                        "Reveal in File Manager"
+                    },
+                    Box::new(RevealInFileManager),
+                )
                 .action("Open in Terminal", Box::new(OpenInTerminal))
                 .when(is_unfoldable, |menu| {
                     menu.action("Unfold Directory", Box::new(UnfoldDirectory))
@@ -2012,7 +2016,8 @@ impl OutlinePanel {
             .selected_entry()
             .and_then(|entry| self.abs_path(entry, cx))
         {
-            cx.reveal_path(&abs_path);
+            self.project
+                .update(cx, |project, cx| project.reveal_path(&abs_path, cx));
         }
     }
 

crates/project/src/project.rs 🔗

@@ -107,6 +107,8 @@ use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 pub use prettier_store::PrettierStore;
 use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
+#[cfg(target_os = "windows")]
+use remote::wsl_path_to_windows_path;
 use remote::{RemoteClient, RemoteConnectionOptions};
 use rpc::{
     AnyProtoClient, ErrorCode,
@@ -2136,6 +2138,27 @@ impl Project {
             .map(|remote| remote.read(cx).connection_options())
     }
 
+    /// Reveals the given path in the system file manager.
+    ///
+    /// On Windows with a WSL remote connection, this converts the POSIX path
+    /// to a Windows UNC path before revealing.
+    pub fn reveal_path(&self, path: &Path, cx: &mut Context<Self>) {
+        #[cfg(target_os = "windows")]
+        if let Some(RemoteConnectionOptions::Wsl(wsl_options)) = self.remote_connection_options(cx)
+        {
+            let path = path.to_path_buf();
+            cx.spawn(async move |_, cx| {
+                wsl_path_to_windows_path(&wsl_options, &path)
+                    .await
+                    .map(|windows_path| cx.update(|cx| cx.reveal_path(&windows_path)))
+            })
+            .detach_and_log_err(cx);
+            return;
+        }
+
+        cx.reveal_path(path);
+    }
+
     #[inline]
     pub fn replica_id(&self) -> ReplicaId {
         match self.client_state {

crates/project_panel/src/project_panel.rs 🔗

@@ -1088,7 +1088,7 @@ impl ProjectPanel {
             let is_read_only = project.is_read_only(cx);
             let is_remote = project.is_remote();
             let is_collab = project.is_via_collab();
-            let is_local = project.is_local();
+            let is_local = project.is_local() || project.is_via_wsl_with_host_interop(cx);
 
             let settings = ProjectPanelSettings::get_global(cx);
             let visible_worktrees_count = project.visible_worktrees(cx).count();
@@ -1119,11 +1119,17 @@ impl ProjectPanel {
                         menu.action("New File", Box::new(NewFile))
                             .action("New Folder", Box::new(NewDirectory))
                             .separator()
-                            .when(is_local && cfg!(target_os = "macos"), |menu| {
-                                menu.action("Reveal in Finder", Box::new(RevealInFileManager))
-                            })
-                            .when(is_local && cfg!(not(target_os = "macos")), |menu| {
-                                menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
+                            .when(is_local, |menu| {
+                                menu.action(
+                                    if cfg!(target_os = "macos") && !is_remote {
+                                        "Reveal in Finder"
+                                    } else if cfg!(target_os = "windows") && !is_remote {
+                                        "Reveal in File Explorer"
+                                    } else {
+                                        "Reveal in File Manager"
+                                    },
+                                    Box::new(RevealInFileManager),
+                                )
                             })
                             .when(is_local, |menu| {
                                 menu.action("Open in Default App", Box::new(OpenWithSystem))
@@ -3077,7 +3083,9 @@ impl ProjectPanel {
         cx: &mut Context<Self>,
     ) {
         if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
-            cx.reveal_path(&worktree.read(cx).absolutize(&entry.path));
+            let path = worktree.read(cx).absolutize(&entry.path);
+            self.project
+                .update(cx, |project, cx| project.reveal_path(&path, cx));
         }
     }
 

crates/remote/src/remote.rs 🔗

@@ -14,6 +14,8 @@ pub use remote_client::{
 pub use transport::docker::DockerConnectionOptions;
 pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
 pub use transport::wsl::WslConnectionOptions;
+#[cfg(target_os = "windows")]
+pub use transport::wsl::wsl_path_to_windows_path;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use transport::mock::{

crates/remote/src/transport/wsl.rs 🔗

@@ -579,6 +579,22 @@ async fn windows_path_to_wsl_path_impl(
     run_wsl_command_with_output_impl(options, "wslpath", &["-u", &source]).await
 }
 
+/// Converts a WSL/POSIX path to a Windows path using `wslpath -w`.
+///
+/// For example, `/home/user/project` becomes `\\wsl.localhost\Ubuntu\home\user\project`
+#[cfg(target_os = "windows")]
+pub fn wsl_path_to_windows_path(
+    options: &WslConnectionOptions,
+    wsl_path: &Path,
+) -> impl Future<Output = Result<PathBuf>> + use<> {
+    let wsl_path_str = wsl_path.to_string_lossy().to_string();
+    let command = wsl_command_impl(options, "wslpath", &["-w", &wsl_path_str], true);
+    async move {
+        let windows_path = run_wsl_command_impl(command).await?;
+        Ok(PathBuf::from(windows_path))
+    }
+}
+
 fn run_wsl_command_impl(mut command: process::Command) -> impl Future<Output = Result<String>> {
     async move {
         let output = command