From b082481a7ac049f46863c4ed63403c7e55063fb2 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 22 Jan 2026 12:24:23 +0100 Subject: [PATCH] project_panel: Show Reveal in File Manager on wsl (#47288) 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 --- 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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 32fa91065b912adc74218a91ab4fefae895b52cb..2c8753c7c59a671bf7541dbb0e83d7e41d2ae973 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22248,8 +22248,12 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) { - 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); + } } } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 1eaaff416ed2415ae147bac361261dc8a3b8bf06..ae1d81da50511ecd61b3e6342ac71351feb1f41b 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/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" }, diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 946be32c9717b9563853e222f6e66b340ad3ab81..d497c16336d9453bdcfed796251147f878041469 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/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)); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 452a436e79c1ba93f0b6a64e12d3a07a0f751fb8..9a8f9e5ee40b6d9be1db3fc00e093fe4804a9d97 100644 --- a/crates/project/src/project.rs +++ b/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) { + #[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 { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 260c1b1dec3a0ae9824894342bbd33c90173e491..b9e12695ca3d8c860147d3ae7bf4e046232b272b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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, ) { 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)); } } diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 4513fb02e5b2311c015ce73156e874a7f610d55c..d3b093cdb80664f8509d30b3ae198456046c420f 100644 --- a/crates/remote/src/remote.rs +++ b/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::{ diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index f3ff273f2045ba01b9b41b8ba966e17dd77ed055..9c4b5867de651a4bb7c7644b72db9129ed89c365 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/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> + 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> { async move { let output = command