From 5ba58ec24558867d6f1f9ca76900bff44be971ef Mon Sep 17 00:00:00 2001 From: pigletfly Date: Thu, 22 Jan 2026 10:01:01 +0800 Subject: [PATCH] windows: Fix open a file at a specific line and column Signed-off-by: pigletfly --- crates/zed/src/main.rs | 44 ++++++++++++++------ crates/zed/src/zed/open_listener.rs | 64 +++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 62f585ec1a7216..3786f2aa4d649f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1554,21 +1554,41 @@ impl ToString for IdType { } fn parse_url_arg(arg: &str, cx: &App) -> String { - match std::fs::canonicalize(Path::new(&arg)) { - Ok(path) => format!("file://{}", path.display()), - Err(_) => { - if arg.starts_with("file://") - || arg.starts_with("zed://") - || arg.starts_with("zed-cli://") - || arg.starts_with("ssh://") - || parse_zed_link(arg, cx).is_some() - { - arg.into() - } else { - format!("file://{arg}") + if arg.starts_with("file://") + || arg.starts_with("zed://") + || arg.starts_with("zed-cli://") + || arg.starts_with("ssh://") + || parse_zed_link(arg, cx).is_some() + { + return arg.into(); + } + + #[cfg(target_os = "windows")] + { + use util::paths::PathWithPosition; + + // On Windows, `path:line[:column]` is never a valid filesystem path (':' isn't allowed + // in filenames), but we still want to canonicalize the base path so opening works even + // when forwarding the request to an already-running Zed instance with a different CWD. + let path_with_position = PathWithPosition::parse_str(arg); + if let Some(row) = path_with_position.row { + if let Ok(canonicalized_path) = std::fs::canonicalize(&path_with_position.path) { + let mut canonicalized = canonicalized_path.display().to_string(); + canonicalized.push(':'); + canonicalized.push_str(&row.to_string()); + if let Some(column) = path_with_position.column { + canonicalized.push(':'); + canonicalized.push_str(&column.to_string()); + } + return format!("file://{canonicalized}"); } } } + + match std::fs::canonicalize(Path::new(arg)) { + Ok(path) => format!("file://{}", path.display()), + Err(_) => format!("file://{arg}"), + } } fn load_embedded_fonts(cx: &App) { diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 6e92e504232442..b928d0555e6fe5 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -687,16 +687,35 @@ pub async fn derive_paths_with_position( path_strings: impl IntoIterator>, ) -> Vec { join_all(path_strings.into_iter().map(|path_str| async move { - let canonicalized = fs.canonicalize(Path::new(path_str.as_ref())).await; - (path_str, canonicalized) + let original = path_str.as_ref(); + + // On Windows, `path:line[:column]` is never a valid filename (':' isn't allowed), but the + // filesystem may still accept it (e.g. NTFS alternate data streams). Canonicalizing the + // whole string can therefore succeed and cause us to drop the position suffix. + // + // To ensure `zed file:line[:column]` jumps to the right location, canonicalize only the + // base path and keep the parsed row/column. + #[cfg(target_os = "windows")] + { + let parsed = PathWithPosition::parse_str(original); + if parsed.row.is_some() { + return match fs.canonicalize(&parsed.path).await { + Ok(canonicalized) => PathWithPosition { + path: canonicalized, + row: parsed.row, + column: parsed.column, + }, + Err(_) => parsed, + }; + } + } + + match fs.canonicalize(Path::new(original)).await { + Ok(canonicalized) => PathWithPosition::from_path(canonicalized), + Err(_) => PathWithPosition::parse_str(original), + } })) .await - .into_iter() - .map(|(original, canonicalized)| match canonicalized { - Ok(canonicalized) => PathWithPosition::from_path(canonicalized), - Err(_) => PathWithPosition::parse_str(original.as_ref()), - }) - .collect() } #[cfg(test)] @@ -718,6 +737,35 @@ mod tests { use util::path; use workspace::{AppState, Workspace}; + #[cfg(target_os = "windows")] + #[gpui::test] + async fn test_derive_paths_with_position_preserves_row_on_windows(cx: &mut TestAppContext) { + init_test(cx); + + let fs = fs::FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "README.md": "line 1\nline 2\nline 3\n", + // Create an entry for the full `path:line` string so `canonicalize` would succeed + // even if it were attempted on the full string. + "README.md:2": "not used\n", + }), + ) + .await; + + let original = format!("{}:2", path!("/dir/README.md")); + let parsed = derive_paths_with_position(fs.as_ref(), [&original]).await; + assert_eq!( + parsed, + vec![PathWithPosition { + path: path!("/dir/README.md").into(), + row: Some(2), + column: None, + }] + ); + } + #[gpui::test] fn test_parse_ssh_url(cx: &mut TestAppContext) { let _app_state = init_test(cx);