From 95d831769afc1d4e33fed3c48220c6dd57240f58 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Wed, 29 Apr 2026 13:35:09 +0200 Subject: [PATCH 1/3] Add percent-encoding to file URI conversion Replace inline URI formatting logic with a proper utility function that handles percent-encoding of special characters in file paths. This ensures file:// URIs are correctly formatted per RFC 3986, particularly for paths with spaces and other special characters. --- Cargo.lock | 1 + Cargo.toml | 1 + src/java.rs | 7 +----- src/util.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 887e54f..d56549e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,6 +961,7 @@ name = "zed_java" version = "6.8.15" dependencies = [ "hex", + "percent-encoding", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c610014..67770bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["cdylib"] [dependencies] hex = "0.4.3" +percent-encoding = "2.3.2" regex = "1.12.2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" diff --git a/src/java.rs b/src/java.rs index 89385dd..98394f7 100644 --- a/src/java.rs +++ b/src/java.rs @@ -388,12 +388,7 @@ impl Extension for Java { // Inject workspaceFolders default if not already set by the user let options_obj = options.as_object_mut().unwrap(); if !options_obj.contains_key("workspaceFolders") { - let root = worktree.root_path(); - let uri = if root.starts_with('/') { - format!("file://{root}") - } else { - format!("file:///{}", root.replace('\\', "/")) - }; + let uri = util::path_to_file_uri(&worktree.root_path()); options_obj.insert("workspaceFolders".to_string(), json!([uri])); } diff --git a/src/util.rs b/src/util.rs index f78ebb1..2323c2e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,4 @@ +use percent_encoding::utf8_percent_encode; use regex::Regex; use serde::{Deserialize, Serialize, Serializer}; use std::{ @@ -316,6 +317,38 @@ pub fn path_to_string>(path: P) -> zed::Result { .map_err(|_| PATH_TO_STR_ERROR.to_string()) } +/// Characters to percent-encode in the path component of a file:// URI. +/// Encodes everything except characters that are valid unencoded in URI paths per RFC 3986. +const PATH_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC + .remove(b'/') + .remove(b':') + .remove(b'-') + .remove(b'.') + .remove(b'_') + .remove(b'~') + .remove(b'@'); + +/// Converts a filesystem path to a `file://` URI with proper percent-encoding. +/// +/// Handles both Unix (`/home/user/project`) and Windows (`C:\Users\user\project`) paths. +/// +/// # Arguments +/// +/// * `path` - The filesystem path to convert. +/// +/// # Returns +/// +/// A properly encoded `file://` URI string. +pub fn path_to_file_uri(path: &str) -> String { + let normalized = if path.starts_with('/') { + path.to_string() + } else { + format!("/{}", path.replace('\\', "/")) + }; + let encoded = utf8_percent_encode(&normalized, &PATH_ENCODE_SET).to_string(); + format!("file://{encoded}") +} + /// Remove all files or directories that aren't equal to [`filename`]. /// /// This function scans the directory given by [`prefix`] and removes any @@ -498,4 +531,36 @@ mod tests { let serialized = serde_json::to_value(&wrapper).unwrap(); assert_eq!(serialized["args"], ""); } + + #[test] + fn test_file_uri_unix_path() { + assert_eq!( + path_to_file_uri("/home/user/project"), + "file:///home/user/project" + ); + } + + #[test] + fn test_file_uri_unix_path_with_spaces() { + assert_eq!( + path_to_file_uri("/my/path with/spaces"), + "file:///my/path%20with/spaces" + ); + } + + #[test] + fn test_file_uri_windows_path() { + assert_eq!( + path_to_file_uri(r"C:\Users\user\project"), + "file:///C:/Users/user/project" + ); + } + + #[test] + fn test_file_uri_windows_path_with_spaces() { + assert_eq!( + path_to_file_uri(r"C:\Users\My User\project"), + "file:///C:/Users/My%20User/project" + ); + } } From 799ae977e5c9e8da9837e0a31d4a7537c4bee2f3 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Wed, 29 Apr 2026 13:44:51 +0200 Subject: [PATCH 2/3] Update README Clarify project root setup and workspaceFolders option --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff01859..d66b22b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This extension adds support for the Java language to [Zed](https://zed.dev). It Install the extension via Zeds extension manager. It should work out of the box for most people. However, there are some things to know: -- It is generally recommended to open projects with the Zed-project root at the Java project root folder (where you would commonly have your `pom.xml` or `build.gradle` file). +- It is generally recommended to open projects with the Zed-project root at the Java project root folder (where you would commonly have your `pom.xml` or `build.gradle` file). The extension will automatically detect Maven and Gradle projects in subdirectories, but opening at the project root provides the best experience. - By default the extension will download and run the latest official version of JDTLS for you, but this requires Java version 21 to be available on your system via either the `$JAVA_HOME` environment variable or as a `java(.exe)` executable on your `$PATH`. If your project requires a lower Java version in the environment, you can specify a different JDK to use for running JDTLS via the `java_home` configuration option. @@ -115,9 +115,9 @@ JDTLS provides many configuration options that can be passed via the `initialize "jdtls": { "initialization_options": { "bundles": [], - "workspaceFolders": [ - "file:///home/snjeza/Project" - ], + // The extension automatically sets this to the worktree root. + // Override only if your Java project root differs from the opened folder: + // "workspaceFolders": ["file:///path/to/your/java/project"], "settings": { "java": { "configuration": { From 0446d516f60135986667bd2d5b2e2f9003ccf461 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Wed, 29 Apr 2026 23:24:51 +0200 Subject: [PATCH 3/3] Optimize path_to_file_uri to reduce allocations Use pre-allocated String and extend directly instead of creating intermediate formatted strings. --- src/util.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/util.rs b/src/util.rs index 2323c2e..88926b9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -340,13 +340,17 @@ const PATH_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHAN /// /// A properly encoded `file://` URI string. pub fn path_to_file_uri(path: &str) -> String { - let normalized = if path.starts_with('/') { - path.to_string() + let mut uri = String::with_capacity(path.len() + 8); + uri.push_str("file://"); + if path.starts_with('/') { + uri.extend(utf8_percent_encode(path, &PATH_ENCODE_SET)); } else { - format!("/{}", path.replace('\\', "/")) - }; - let encoded = utf8_percent_encode(&normalized, &PATH_ENCODE_SET).to_string(); - format!("file://{encoded}") + for chunk in path.split('\\') { + uri.push('/'); + uri.extend(utf8_percent_encode(chunk, &PATH_ENCODE_SET)); + } + } + uri } /// Remove all files or directories that aren't equal to [`filename`].