From ce1369d1e4c300801171670039fe26a5d7add476 Mon Sep 17 00:00:00 2001 From: KuaaMU Date: Sat, 2 May 2026 04:33:19 +0800 Subject: [PATCH] fix(windows): context menu "Open Warp in new tab" navigates to home instead of selected directory (#9844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows Explorer context menu passes raw filesystem paths via `%1`/`%V` without URL-encoding them into the `warp://` URI query string. While the `url` crate handles bare backslashes and drive-letter colons correctly in most cases, the resulting `PathBuf` can still fail the `is_dir()` gate in `path_if_directory()` — for example when the OS needs a canonicalized form (symlinks, trailing separators, `\?\` prefix, etc.). When that happens the initial directory is silently dropped and the new tab falls back to `~`. Three changes: 1. `parse_tab_path` now trims trailing whitespace from the decoded path, catching an edge-case where `%1`/`%V` expansion appends invisible chars. 2. `path_if_directory` now falls back to `std::fs::canonicalize()` when `is_dir()` returns false, resolving symlinks, `.` / `..` segments, and other minor path normalization issues before giving up. 3. `on_open_urls` now has a Windows-only fallback: if `Url::parse()` fails on a bare `C:\...` path, it converts it to a `file:///` URL before retrying. This is a defense-in-depth measure for any code path that receives a raw Windows path instead of a properly-formatted URI. Closes #9844 Co-Authored-By: Claude Opus 4.7 --- app/src/lib.rs | 17 ++++++++++++++++- app/src/root_view.rs | 18 ++++++++++++++++-- app/src/uri/mod.rs | 9 ++++++++- app/src/uri/uri_test.rs | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/src/lib.rs b/app/src/lib.rs index cdad0f2f7..92c5bda64 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -2131,7 +2131,22 @@ fn app_callbacks(is_integration_test: bool) -> warpui::platform::AppCallbacks { })), on_open_urls: Some(Box::new(move |urls, ctx| { for url in &urls { - let parsed_url = Url::parse(url); + let parsed_url = Url::parse(url).or_else(|_| { + // On Windows, a bare path like "C:\folder" fails URL parsing + // because "C:" looks like a URI scheme. Convert to a file:// URL. + #[cfg(windows)] + { + let path = url.trim(); + if path.len() >= 2 + && path.as_bytes()[1] == b':' + && (path.as_bytes()[0].is_ascii_alphabetic()) + { + let forward = path.replace('\\', "/"); + return Url::parse(&format!("file:///{forward}")); + } + } + Err(url::ParseError::EmptyHost) + }); match parsed_url { Ok(url) => uri::handle_incoming_uri(&url, ctx), Err(e) => log::warn!("Unable to parse received url: {e}"), diff --git a/app/src/root_view.rs b/app/src/root_view.rs index 7cc47ce52..7fb637473 100644 --- a/app/src/root_view.rs +++ b/app/src/root_view.rs @@ -914,8 +914,22 @@ fn open_from_restored(arg: &OpenFromRestoredArg, ctx: &mut AppContext) { } } -fn path_if_directory(path: &Path) -> Option<&Path> { - path.is_dir().then_some(path) +fn path_if_directory(path: &Path) -> Option { + if path.is_dir() { + return Some(path.to_path_buf()); + } + // On Windows, paths from Explorer context menu may arrive without URL-encoding + // (e.g. `warp://action/new_tab?path=C:\Projects\my-app`). The url crate handles + // this correctly, but the resulting path can still fail `is_dir()` when the OS + // needs a normalized form (canonicalized, trailing-separator-stripped, etc.). + // Try canonicalizing as a fallback — this resolves symlinks, relative segments, + // and other minor path differences that `is_dir()` alone would reject. + if let Ok(canonical) = std::fs::canonicalize(path) { + if canonical.is_dir() { + return Some(canonical); + } + } + None } /// Opens a new window with the workspace configured according to `source`. Returns the diff --git a/app/src/uri/mod.rs b/app/src/uri/mod.rs index 393b28471..2a2086c68 100644 --- a/app/src/uri/mod.rs +++ b/app/src/uri/mod.rs @@ -663,7 +663,14 @@ fn find_matching_config_name<'a>( /// user's home directory. fn parse_tab_path(url: &Url) -> Option { let raw = url.query_pairs().find(|(k, _)| k == "path")?.1; - Some(PathBuf::from(shellexpand::tilde(&raw).into_owned())) + let expanded = shellexpand::tilde(&raw).into_owned(); + // Trim whitespace that may be appended by Windows shell variable expansion + // (e.g. `%1` or `%V` can carry trailing spaces/newlines in some configurations). + let trimmed = expanded.trim(); + if trimmed.is_empty() { + return None; + } + Some(PathBuf::from(trimmed)) } #[derive(Debug)] diff --git a/app/src/uri/uri_test.rs b/app/src/uri/uri_test.rs index 44ba39e43..7e867fc56 100644 --- a/app/src/uri/uri_test.rs +++ b/app/src/uri/uri_test.rs @@ -659,3 +659,39 @@ fn test_open_file_non_runnable_shebang_routes_to_editor() { std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o644)).unwrap(); assert_eq!(classify_open_file_action(&p), OpenFileAction::Editor); } + +// -- parse_tab_path edge cases (GH #9844) ----------------------------------- +// +// These tests cover the fix for Windows "Open Warp in new tab" context menu +// not navigating to the selected directory. The Windows shell passes raw +// paths via `%1` / `%V` without URL-encoding, so the query parameter may +// contain backslashes, colons, and trailing whitespace. + +#[test] +fn test_parse_tab_path_windows_drive_letter() { + let url = Url::parse(r"warp://action/new_tab?path=C:\Projects\my-app").unwrap(); + assert_eq!( + parse_tab_path(&url), + Some(PathBuf::from(r"C:\Projects\my-app")) + ); +} + +#[test] +fn test_parse_tab_path_windows_with_trailing_whitespace() { + let url = Url::parse("warp://action/new_tab?path=C%3A%5CProjects%20").unwrap(); + // Trailing space should be trimmed + let result = parse_tab_path(&url); + assert!(result.is_some()); +} + +#[test] +fn test_parse_tab_path_empty_returns_none() { + let url = Url::parse("warp://action/new_tab?path=").unwrap(); + assert_eq!(parse_tab_path(&url), None); +} + +#[test] +fn test_parse_tab_path_whitespace_only_returns_none() { + let url = Url::parse("warp://action/new_tab?path=%20%20%20").unwrap(); + assert_eq!(parse_tab_path(&url), None); +}