terminal: Fix hyperlinks for `file://` schemas windows drive URIs (#44847)

Lukas Wirth created

Closes https://github.com/zed-industries/zed/issues/39189

Release Notes:

- Fixed terminal hyperlinking not working for `file://` schemes with
windows drive letters

Change summary

crates/terminal/Cargo.toml                 |  2 
crates/terminal/src/terminal_hyperlinks.rs | 28 ++++++++++++++---------
2 files changed, 18 insertions(+), 12 deletions(-)

Detailed changes

crates/terminal/Cargo.toml 🔗

@@ -38,6 +38,7 @@ smol.workspace = true
 task.workspace = true
 theme.workspace = true
 thiserror.workspace = true
+url.workspace = true
 util.workspace = true
 urlencoding.workspace = true
 
@@ -49,5 +50,4 @@ gpui = { workspace = true, features = ["test-support"] }
 rand.workspace = true
 serde_json.workspace = true
 settings = { workspace = true, features = ["test-support"] }
-url.workspace = true
 util_macros.workspace = true

crates/terminal/src/terminal_hyperlinks.rs 🔗

@@ -14,6 +14,7 @@ use std::{
     ops::{Index, Range},
     time::{Duration, Instant},
 };
+use url::Url;
 
 const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`']+"#;
 const WIDE_CHAR_SPACERS: Flags =
@@ -128,8 +129,19 @@ pub(super) fn find_from_grid_point<T: EventListener>(
         if is_url {
             // Treat "file://" IRIs like file paths to ensure
             // that line numbers at the end of the path are
-            // handled correctly
-            if let Some(path) = maybe_url_or_path.strip_prefix("file://") {
+            // handled correctly.
+            // Use Url::to_file_path() to properly handle Windows drive letters
+            // (e.g., file:///C:/path -> C:\path)
+            if maybe_url_or_path.starts_with("file://") {
+                if let Ok(url) = Url::parse(&maybe_url_or_path) {
+                    if let Ok(path) = url.to_file_path() {
+                        return (path.to_string_lossy().into_owned(), false, word_match);
+                    }
+                }
+                // Fallback: strip file:// prefix if URL parsing fails
+                let path = maybe_url_or_path
+                    .strip_prefix("file://")
+                    .unwrap_or(&maybe_url_or_path);
                 (path.to_string(), false, word_match)
             } else {
                 (maybe_url_or_path, true, word_match)
@@ -1042,8 +1054,9 @@ mod tests {
     }
 
     mod file_iri {
-        // File IRIs have a ton of use cases, most of which we currently do not support. A few of
-        // those cases are documented here as tests which are expected to fail.
+        // File IRIs have a ton of use cases. Absolute file URIs are supported on all platforms,
+        // including Windows drive letters (e.g., file:///C:/path) and percent-encoded characters.
+        // Some cases like relative file IRIs are not supported.
         // See https://en.wikipedia.org/wiki/File_URI_scheme
 
         /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns**
@@ -1063,7 +1076,6 @@ mod tests {
         mod issues {
             #[cfg(not(target_os = "windows"))]
             #[test]
-            #[should_panic(expected = "Path = «/test/Ῥόδος/», at grid cells (0, 0)..=(15, 1)")]
             fn issue_file_iri_with_percent_encoded_characters() {
                 // Non-space characters
                 // file:///test/Ῥόδος/
@@ -1092,18 +1104,12 @@ mod tests {
                 // See https://en.wikipedia.org/wiki/File_URI_scheme
                 // https://github.com/zed-industries/zed/issues/39189
                 #[test]
-                #[should_panic(
-                    expected = r#"Path = «C:\\test\\cool\\index.rs», at grid cells (0, 0)..=(9, 1)"#
-                )]
                 fn issue_39189() {
                     test_file_iri!("file:///C:/test/cool/index.rs");
                     test_file_iri!("file:///C:/test/cool/");
                 }
 
                 #[test]
-                #[should_panic(
-                    expected = r#"Path = «C:\\test\\Ῥόδος\\», at grid cells (0, 0)..=(16, 1)"#
-                )]
                 fn issue_file_iri_with_percent_encoded_characters() {
                     // Non-space characters
                     // file:///test/Ῥόδος/