diff --git a/Cargo.lock b/Cargo.lock index b80ba1c81d7c40d6e8e5756048b8902142c26f8c..d2b142c440c973fe7c247b770660e1d937740fc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13833,6 +13833,7 @@ dependencies = [ "prost 0.9.0", "release_channel", "rpc", + "schemars 1.0.4", "serde", "serde_json", "settings", diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 13013c9189749f77b8619ac19d59f96e5adb1e1d..db9721063d61de5d0d9ec1b4902a249ef8b0fd75 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -102,6 +102,33 @@ pub fn init(cx: &mut App) { }); }); + #[cfg(target_os = "windows")] + cx.on_action(|open_wsl: &remote::OpenWslPath, cx| { + let open_wsl = open_wsl.clone(); + with_active_or_new_workspace(cx, move |workspace, window, cx| { + let fs = workspace.project().read(cx).fs().clone(); + add_wsl_distro(fs, &open_wsl.distro, cx); + let open_options = OpenOptions { + replace_window: window.window_handle().downcast::(), + ..Default::default() + }; + + let app_state = workspace.app_state().clone(); + + cx.spawn_in(window, async move |_, cx| { + open_remote_project( + RemoteConnectionOptions::Wsl(open_wsl.distro.clone()), + open_wsl.paths, + app_state, + open_options, + cx, + ) + .await + }) + .detach(); + }); + }); + cx.on_action(|open_recent: &OpenRecent, cx| { let create_new_window = open_recent.create_new_window; with_active_or_new_workspace(cx, move |workspace, window, cx| { @@ -136,6 +163,38 @@ pub fn init(cx: &mut App) { cx.observe_new(DisconnectedOverlay::register).detach(); } +#[cfg(target_os = "windows")] +pub fn add_wsl_distro( + fs: Arc, + connection_options: &remote::WslConnectionOptions, + cx: &App, +) { + use gpui::ReadGlobal; + use settings::SettingsStore; + + let distro_name = SharedString::from(&connection_options.distro_name); + let user = connection_options.user.clone(); + SettingsStore::global(cx).update_settings_file(fs, move |setting, _| { + let connections = setting + .remote + .wsl_connections + .get_or_insert(Default::default()); + + if !connections + .iter() + .any(|conn| conn.distro_name == distro_name && conn.user == user) + { + use std::collections::BTreeSet; + + connections.push(settings::WslConnection { + distro_name, + user, + projects: BTreeSet::new(), + }) + } + }); +} + pub struct RecentProjects { pub picker: Entity>, rem_width: f32, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 2596a3d41604ac3710b9d5302718c18b2f948b4f..7c9f9fe96515a65fab95afff5f0d336102ef9223 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -794,28 +794,34 @@ impl RemoteServerProjects { let wsl_picker = picker.clone(); let creating = cx.spawn_in(window, async move |this, cx| { match connection.await { - Some(Some(client)) => this - .update_in(cx, |this, window, cx| { - telemetry::event!("WSL Distro Added"); - this.retained_connections.push(client); - this.add_wsl_distro(connection_options, cx); - this.mode = Mode::default_mode(&BTreeSet::new(), cx); - this.focus_handle(cx).focus(window); - cx.notify() - }) - .log_err(), - _ => this - .update(cx, |this, cx| { - this.mode = Mode::AddWslDistro(AddWslDistro { - picker: wsl_picker, - connection_prompt: None, - _creating: None, - }); - cx.notify() - }) - .log_err(), - }; - () + Some(Some(client)) => this.update_in(cx, |this, window, cx| { + telemetry::event!("WSL Distro Added"); + this.retained_connections.push(client); + let Some(fs) = this + .workspace + .read_with(cx, |workspace, cx| { + workspace.project().read(cx).fs().clone() + }) + .log_err() + else { + return; + }; + + crate::add_wsl_distro(fs, &connection_options, cx); + this.mode = Mode::default_mode(&BTreeSet::new(), cx); + this.focus_handle(cx).focus(window); + cx.notify(); + }), + _ => this.update(cx, |this, cx| { + this.mode = Mode::AddWslDistro(AddWslDistro { + picker: wsl_picker, + connection_prompt: None, + _creating: None, + }); + cx.notify(); + }), + } + .log_err(); }); self.mode = Mode::AddWslDistro(AddWslDistro { @@ -1415,31 +1421,6 @@ impl RemoteServerProjects { }); } - #[cfg(target_os = "windows")] - fn add_wsl_distro( - &mut self, - connection_options: remote::WslConnectionOptions, - cx: &mut Context, - ) { - self.update_settings_file(cx, move |setting, _| { - let connections = setting.wsl_connections.get_or_insert(Default::default()); - - let distro_name = SharedString::from(connection_options.distro_name); - let user = connection_options.user; - - if !connections - .iter() - .any(|conn| conn.distro_name == distro_name && conn.user == user) - { - connections.push(settings::WslConnection { - distro_name, - user, - projects: BTreeSet::new(), - }) - } - }); - } - fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context) { self.update_settings_file(cx, move |setting, _| { if let Some(connections) = setting.wsl_connections.as_mut() { diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index d1a91af9a5decc88b4c70c69001ba6dad18e4b8b..838723f3660558f93ac6f8066627a8b182a2cf24 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -31,6 +31,7 @@ paths.workspace = true prost.workspace = true release_channel.workspace = true rpc = { workspace = true, features = ["gpui"] } +schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 62fe40f7649b9a1f8e4697a5c6b4c7d1690715e4..783dde16acb350367ed82243e138e5c58f64224b 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -4,6 +4,8 @@ pub mod proxy; pub mod remote_client; mod transport; +#[cfg(target_os = "windows")] +pub use remote_client::OpenWslPath; pub use remote_client::{ ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, RemoteConnection, RemoteConnectionOptions, RemotePlatform, connect, diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index e9eafa25b0467f29d1dd12816aa17d65b94bf1d4..c5f511db5f94421b4e1c2872fdec4222381ba23a 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -1090,6 +1090,15 @@ impl From for RemoteConnectionOptions { } } +#[cfg(target_os = "windows")] +/// Open a wsl path (\\wsl.localhost\\path) +#[derive(Debug, Clone, PartialEq, Eq, gpui::Action)] +#[action(namespace = workspace, no_json, no_register)] +pub struct OpenWslPath { + pub distro: WslConnectionOptions, + pub paths: Vec, +} + #[async_trait(?Send)] pub trait RemoteConnection: Send + Sync { fn start_proxy( diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index d3d92b0f436f2a6ce1615426ac22916d4823a4fb..e6827347914cc35e266080dab7c83fd182e16a64 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -24,7 +24,7 @@ use util::{ shell::ShellKind, }; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, schemars::JsonSchema)] pub struct WslConnectionOptions { pub distro_name: String, pub user: Option, diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 20187bf7376861ebd03e02f7fb006428c1c51ec4..d8c84c4938a14386863cce3fd920ba61d1bb4644 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1087,6 +1087,68 @@ pub fn compare_paths( } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WslPath { + pub distro: String, + + // the reason this is an OsString and not any of the path types is that it needs to + // represent a unix path (with '/' separators) on windows. `from_path` does this by + // manually constructing it from the path components of a given windows path. + pub path: std::ffi::OsString, +} + +impl WslPath { + pub fn from_path>(path: P) -> Option { + if cfg!(not(target_os = "windows")) { + return None; + } + use std::{ + ffi::OsString, + path::{Component, Prefix}, + }; + + let mut components = path.as_ref().components(); + let Some(Component::Prefix(prefix)) = components.next() else { + return None; + }; + let (server, distro) = match prefix.kind() { + Prefix::UNC(server, distro) => (server, distro), + Prefix::VerbatimUNC(server, distro) => (server, distro), + _ => return None, + }; + let Some(Component::RootDir) = components.next() else { + return None; + }; + + let server_str = server.to_string_lossy(); + if server_str == "wsl.localhost" || server_str == "wsl$" { + let mut result = OsString::from(""); + for c in components { + use Component::*; + match c { + Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"), + RootDir => unreachable!("got root dir, but already stripped root"), + CurDir => continue, + ParentDir => result.push("/.."), + Normal(s) => { + result.push("/"); + result.push(s); + } + } + } + if result.is_empty() { + result.push("/"); + } + Some(WslPath { + distro: distro.to_string_lossy().to_string(), + path: result, + }) + } else { + None + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1921,4 +1983,45 @@ mod tests { let suffix = Path::new("app.tar.gz"); assert_eq!(strip_path_suffix(base, suffix), None); } + + #[cfg(target_os = "windows")] + #[test] + fn test_wsl_path() { + use super::WslPath; + let path = "/a/b/c"; + assert_eq!(WslPath::from_path(&path), None); + + let path = r"\\wsl.localhost"; + assert_eq!(WslPath::from_path(&path), None); + + let path = r"\\wsl.localhost\Distro"; + assert_eq!( + WslPath::from_path(&path), + Some(WslPath { + distro: "Distro".to_owned(), + path: "/".into(), + }) + ); + + let path = r"\\wsl.localhost\Distro\blue"; + assert_eq!( + WslPath::from_path(&path), + Some(WslPath { + distro: "Distro".to_owned(), + path: "/blue".into() + }) + ); + + let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt"; + assert_eq!( + WslPath::from_path(&path), + Some(WslPath { + distro: "archlinux".to_owned(), + path: "/tomato/paprika/../aubergine.txt".into() + }) + ); + + let path = r"\\windows.localhost\Distro\foo"; + assert_eq!(WslPath::from_path(&path), None); + } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 5803f193a838600d7c977931770bbc70df4fa92a..1ed3d3673a7c7fef0937649693a93d4de37ca461 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1348,28 +1348,10 @@ impl WorkspaceDb { } let has_wsl_path = if cfg!(windows) { - fn is_wsl_path(path: &PathBuf) -> bool { - use std::path::{Component, Prefix}; - - path.components() - .next() - .and_then(|component| match component { - Component::Prefix(prefix) => Some(prefix), - _ => None, - }) - .and_then(|prefix| match prefix.kind() { - Prefix::UNC(server, _) => Some(server), - Prefix::VerbatimUNC(server, _) => Some(server), - _ => None, - }) - .map(|server| { - let server_str = server.to_string_lossy(); - server_str == "wsl.localhost" || server_str == "wsl$" - }) - .unwrap_or(false) - } - - paths.paths().iter().any(|path| is_wsl_path(path)) + paths + .paths() + .iter() + .any(|path| util::paths::WslPath::from_path(path).is_some()) } else { false }; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 65b2d63b229ea4b394825a91131fc267c85b7f8b..12d53df26b75b71603dd1b0787c5772e2263713c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7318,6 +7318,10 @@ pub fn open_paths( let mut existing = None; let mut best_match = None; let mut open_visible = OpenVisible::All; + #[cfg(target_os = "windows")] + let wsl_path = abs_paths + .iter() + .find_map(|p| util::paths::WslPath::from_path(p)); cx.spawn(async move |cx| { if open_options.open_new_workspace != Some(true) { @@ -7381,7 +7385,7 @@ pub fn open_paths( } } - if let Some(existing) = existing { + let result = if let Some(existing) = existing { let open_task = existing .update(cx, |workspace, window, cx| { window.activate_window(); @@ -7418,7 +7422,37 @@ pub fn open_paths( ) })? .await - } + }; + + #[cfg(target_os = "windows")] + if let Some(util::paths::WslPath{distro, path}) = wsl_path + && let Ok((workspace, _)) = &result + { + workspace + .update(cx, move |workspace, _window, cx| { + struct OpenInWsl; + workspace.show_notification(NotificationId::unique::(), cx, move |cx| { + let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy()); + let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote"); + cx.new(move |cx| { + MessageNotification::new(msg, cx) + .primary_message("Open in WSL") + .primary_icon(IconName::FolderOpen) + .primary_on_click(move |window, cx| { + window.dispatch_action(Box::new(remote::OpenWslPath { + distro: remote::WslConnectionOptions { + distro_name: distro.clone(), + user: None, + }, + paths: vec![path.clone().into()], + }), cx) + }) + }) + }); + }) + .unwrap(); + }; + result }) } diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 3fc9903cdc99a8bf5fdb7c14619e3ce963b8fc46..3506e492b77d1eca6a1dde84bf5ea0a2be107540 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -519,6 +519,12 @@ actions!( ] ); +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WslConnectionOptions { + pub distro_name: String, + pub user: Option, +} + #[cfg(target_os = "windows")] pub mod wsl_actions { use gpui::Action;