From fe6e5ce24658e722b85ec11e6bd9b5b50f091213 Mon Sep 17 00:00:00 2001 From: smulet <122980527+simuleite@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:16:03 +0800 Subject: [PATCH 01/93] Use standard library path structure instead of plain string manipulation (#47) --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 26965b3..99006c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use std::{ collections::BTreeSet, fs::{self, create_dir}, + path::Path, }; use zed_extension_api::{ @@ -222,6 +223,7 @@ impl Java { let prefix = "lombok"; let jar_name = format!("lombok-{latest_version}.jar"); let jar_path = format!("{prefix}/{jar_name}"); + let jar_path = Path::new(prefix).join(&jar_name).to_string_lossy().into_owned(); // If latest version isn't installed, if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { From a2d1d0bdf7cebebfb312191c2b87932b38d5d158 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Fri, 4 Apr 2025 08:55:52 -0700 Subject: [PATCH 02/93] Add `environment` element to issue forms --- .github/ISSUE_TEMPLATE/1-grammar-bug.yml | 9 +++++++++ .github/ISSUE_TEMPLATE/2-language-server-bug.yml | 10 ++++++++++ .github/ISSUE_TEMPLATE/3-other-bug.yml | 9 +++++++++ 3 files changed, 28 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/1-grammar-bug.yml b/.github/ISSUE_TEMPLATE/1-grammar-bug.yml index df524c2..d7435e0 100644 --- a/.github/ISSUE_TEMPLATE/1-grammar-bug.yml +++ b/.github/ISSUE_TEMPLATE/1-grammar-bug.yml @@ -25,3 +25,12 @@ body: placeholder: I expected to see 1, 2, and 3. validations: required: true + - type: textarea + id: environment + attributes: + label: Environment + value: | + Zed: + Platform: + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/2-language-server-bug.yml b/.github/ISSUE_TEMPLATE/2-language-server-bug.yml index 9a9e03e..7fdc871 100644 --- a/.github/ISSUE_TEMPLATE/2-language-server-bug.yml +++ b/.github/ISSUE_TEMPLATE/2-language-server-bug.yml @@ -25,3 +25,13 @@ body: placeholder: I expected to see 1, 2, and 3. validations: required: true + - type: textarea + id: environment + attributes: + label: Environment + value: | + Zed: + Platform: + Java: + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/3-other-bug.yml b/.github/ISSUE_TEMPLATE/3-other-bug.yml index 48ca83e..aec1292 100644 --- a/.github/ISSUE_TEMPLATE/3-other-bug.yml +++ b/.github/ISSUE_TEMPLATE/3-other-bug.yml @@ -17,3 +17,12 @@ body: placeholder: I expected to see 1, 2, and 3. validations: required: true + - type: textarea + id: environment + attributes: + label: Environment + value: | + Zed: + Platform: + validations: + required: true From 545f553c9fdab2fbae86278ca2a010679be6bac7 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Fri, 11 Apr 2025 15:09:58 -0700 Subject: [PATCH 03/93] Use `Java::language_server_initialization_options` in `Java::language_server_command` --- src/lib.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 99006c2..f12c1f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,8 +222,10 @@ impl Java { .ok_or("malformed GitHub tags response")?[1..]; let prefix = "lombok"; let jar_name = format!("lombok-{latest_version}.jar"); - let jar_path = format!("{prefix}/{jar_name}"); - let jar_path = Path::new(prefix).join(&jar_name).to_string_lossy().into_owned(); + let jar_path = Path::new(prefix) + .join(&jar_name) + .to_string_lossy() + .into_owned(); // If latest version isn't installed, if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { @@ -288,8 +290,10 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result { - let java_home = LspSettings::for_worktree(language_server_id.as_ref(), worktree)? - .initialization_options + let initialization_options = + self.language_server_initialization_options(language_server_id, worktree)?; + let java_home = initialization_options + .as_ref() .and_then(|initialization_options| { initialization_options .pointer("/settings/java/home") @@ -306,16 +310,15 @@ impl Extension for Java { } let mut args = Vec::new(); - // Add lombok as javaagent if initialization_options.settings.java.jdt.ls.lombokSupport.enabled is true - let lombok_enabled = LspSettings::for_worktree(language_server_id.as_ref(), worktree)? - .initialization_options + let lombok_enabled = initialization_options .and_then(|initialization_options| { initialization_options .pointer("/settings/java/jdt/ls/lombokSupport/enabled") .and_then(|enabled| enabled.as_bool()) }) .unwrap_or(false); + if lombok_enabled { let lombok_jar_path = self.lombok_jar_path(language_server_id)?; let lombok_jar_full_path = std::env::current_dir() @@ -323,6 +326,7 @@ impl Extension for Java { .join(&lombok_jar_path) .to_string_lossy() .to_string(); + args.push(format!("--jvm-arg=-javaagent:{lombok_jar_full_path}")); } From b058a9903623039328ebd76b9d9942bdb84aa692 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Fri, 11 Apr 2025 15:11:35 -0700 Subject: [PATCH 04/93] Add `type` key in issue templates --- .github/ISSUE_TEMPLATE/1-grammar-bug.yml | 1 + .github/ISSUE_TEMPLATE/2-language-server-bug.yml | 1 + .github/ISSUE_TEMPLATE/3-other-bug.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/1-grammar-bug.yml b/.github/ISSUE_TEMPLATE/1-grammar-bug.yml index d7435e0..cf7d7eb 100644 --- a/.github/ISSUE_TEMPLATE/1-grammar-bug.yml +++ b/.github/ISSUE_TEMPLATE/1-grammar-bug.yml @@ -1,6 +1,7 @@ name: Grammar Bug description: A bug related to the Tree-Sitter grammar (e.g. syntax highlighting, auto-indents, outline, bracket-closing). labels: ["bug", "grammar"] +type: "Bug" body: - type: checkboxes id: relevance-confirmation diff --git a/.github/ISSUE_TEMPLATE/2-language-server-bug.yml b/.github/ISSUE_TEMPLATE/2-language-server-bug.yml index 7fdc871..1e14071 100644 --- a/.github/ISSUE_TEMPLATE/2-language-server-bug.yml +++ b/.github/ISSUE_TEMPLATE/2-language-server-bug.yml @@ -1,6 +1,7 @@ name: Language Server Bug description: A bug related to the language server (e.g. autocomplete, diagnostics, hover-docs, go to symbol, initialization options). labels: ["bug", "language-server"] +type: "Bug" body: - type: checkboxes id: relevance-confirmation diff --git a/.github/ISSUE_TEMPLATE/3-other-bug.yml b/.github/ISSUE_TEMPLATE/3-other-bug.yml index aec1292..86b8a6e 100644 --- a/.github/ISSUE_TEMPLATE/3-other-bug.yml +++ b/.github/ISSUE_TEMPLATE/3-other-bug.yml @@ -1,6 +1,7 @@ name: Other Bug description: A bug related to something else! labels: ["bug"] +type: "Bug" body: - type: textarea id: what-happened From 778e6e25198913295685488ecfcb901f2c728b81 Mon Sep 17 00:00:00 2001 From: Igor Tolmachev Date: Mon, 14 Apr 2025 20:39:17 +0300 Subject: [PATCH 05/93] Fix configuration settings (#52) - Closes: #42 - Closes: #43 I just read nvim-jdtls [configuration example] and make hypotheses about jdtls. And it turned out, the settings field in [InitializationOptions] is a field in [DidChangeConfigurationParams]. It is so..... stupid. [configuration example]: https://github.com/mfussenegger/nvim-jdtls/blob/7223b812dde98f4260084fe9303c8301b9831a58/README.md?plain=1#L149-L152 [InitializationOptions]: https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request [DidChangeConfigurationParams]: https://github.com/Microsoft/language-server-protocol/blob/main/versions/protocol-2-x.md#didchangeconfiguration-notification Co-authored-by: Valentine Briese --- README.md | 155 ++++++++++++++++++++++++++--------------------------- src/lib.rs | 50 ++++++++++------- 2 files changed, 109 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index ebf3830..8558bbc 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ This extension adds support for the Java language. -## Initialization Options +## Configuration Options -If [Lombok] support is enabled via [JDTLS] initialization option -(`initialization_options.settings.java.jdt.ls.lombokSupport.enabled`), this +If [Lombok] support is enabled via [JDTLS] configuration option +(`settings.java.jdt.ls.lombokSupport.enabled`), this extension will download and add [Lombok] as a javaagent to the JVM arguments for [JDTLS]. @@ -19,90 +19,89 @@ for example: "initialization_options": { "bundles": [], "workspaceFolders": ["file:///home/snjeza/Project"], - "settings": { - "java": { - "home": "/usr/local/jdk-9.0.1", - "errors": { - "incompleteClasspath": { - "severity": "warning" - } + }, + "settings": { + "java": { + "home": "/usr/local/jdk-9.0.1", + "errors": { + "incompleteClasspath": { + "severity": "warning", }, - "configuration": { - "updateBuildConfiguration": "interactive", - "maven": { - "userSettings": null - } + }, + "configuration": { + "updateBuildConfiguration": "interactive", + "maven": { + "userSettings": null, }, - "trace": { - "server": "verbose" + }, + "trace": { + "server": "verbose", + }, + "import": { + "gradle": { + "enabled": true, }, - "import": { - "gradle": { - "enabled": true - }, - "maven": { - "enabled": true - }, - "exclusions": [ - "**/node_modules/**", - "**/.metadata/**", - "**/archetype-resources/**", - "**/META-INF/maven/**", - "/**/test/**" - ] - }, - "jdt": { - "ls": { - "lombokSupport": { - "enabled": false // Set this to true to enable lombok support - } - } - }, - "referencesCodeLens": { - "enabled": false - }, - "signatureHelp": { - "enabled": false + "maven": { + "enabled": true, }, - "implementationsCodeLens": { - "enabled": false - }, - "format": { - "enabled": true - }, - "saveActions": { - "organizeImports": false - }, - "contentProvider": { - "preferred": null - }, - "autobuild": { - "enabled": false + "exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**", + "/**/test/**", + ], + }, + "jdt": { + "ls": { + "lombokSupport": { + "enabled": false, // Set this to true to enable lombok support + }, }, - "completion": { - "favoriteStaticMembers": [ - "org.junit.Assert.*", - "org.junit.Assume.*", - "org.junit.jupiter.api.Assertions.*", - "org.junit.jupiter.api.Assumptions.*", - "org.junit.jupiter.api.DynamicContainer.*", - "org.junit.jupiter.api.DynamicTest.*" - ], - "importOrder": ["java", "javax", "com", "org"] - } - } - } - } - } - } + }, + "referencesCodeLens": { + "enabled": false, + }, + "signatureHelp": { + "enabled": false, + }, + "implementationsCodeLens": { + "enabled": false, + }, + "format": { + "enabled": true, + }, + "saveActions": { + "organizeImports": false, + }, + "contentProvider": { + "preferred": null, + }, + "autobuild": { + "enabled": false, + }, + "completion": { + "favoriteStaticMembers": [ + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*", + ], + "importOrder": ["java", "javax", "com", "org"], + }, + }, + }, + }, + }, } ``` -*Example taken from JDTLS's [initialization options wiki page].* +*Example taken from JDTLS's [configuration options wiki page].* -You can see all the options JDTLS accepts [here][initialization options wiki -page]. +You can see all the options JDTLS accepts [here][configuration options wiki page]. [JDTLS]: https://github.com/eclipse-jdtls/eclipse.jdt.ls -[initialization options wiki page]: https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request +[configuration options wiki page]: https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request [Lombok]: https://projectlombok.org diff --git a/src/lib.rs b/src/lib.rs index f12c1f0..0c6aa21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -290,19 +290,17 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result { - let initialization_options = - self.language_server_initialization_options(language_server_id, worktree)?; - let java_home = initialization_options - .as_ref() - .and_then(|initialization_options| { - initialization_options - .pointer("/settings/java/home") - .and_then(|java_home_value| { - java_home_value - .as_str() - .map(|java_home_str| java_home_str.to_string()) - }) - }); + let configuration = + self.language_server_workspace_configuration(language_server_id, worktree)?; + let java_home = configuration.as_ref().and_then(|configuration| { + configuration + .pointer("/java/home") + .and_then(|java_home_value| { + java_home_value + .as_str() + .map(|java_home_str| java_home_str.to_string()) + }) + }); let mut env = Vec::new(); if let Some(java_home) = java_home { @@ -310,11 +308,11 @@ impl Extension for Java { } let mut args = Vec::new(); - // Add lombok as javaagent if initialization_options.settings.java.jdt.ls.lombokSupport.enabled is true - let lombok_enabled = initialization_options - .and_then(|initialization_options| { - initialization_options - .pointer("/settings/java/jdt/ls/lombokSupport/enabled") + // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true + let lombok_enabled = configuration + .and_then(|configuration| { + configuration + .pointer("/java/jdt/ls/lombokSupport/enabled") .and_then(|enabled| enabled.as_bool()) }) .unwrap_or(false); @@ -346,6 +344,22 @@ impl Extension for Java { .map(|lsp_settings| lsp_settings.initialization_options) } + fn language_server_workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) + .or(self + .language_server_initialization_options(language_server_id, worktree) + .map(|initialization_options| { + initialization_options.and_then(|initialization_options| { + initialization_options.get("settings").cloned() + }) + })) + } + fn label_for_completion( &self, _language_server_id: &LanguageServerId, From 778b8f6f1db0eb5d02b18ca6a0fd01eaa5b2a1db Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Mon, 14 Apr 2025 10:42:55 -0700 Subject: [PATCH 06/93] Bump to v6.0.2 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 219ede3..673fc88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.0.1" +version = "6.0.2" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index 74a7ff7..fd8a721 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.0.1" +version = "6.0.2" schema_version = 1 authors = [ "Valentine Briese ", From 166540e8c5111eaf9fb5fce74d17bfa09be53f5d Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Tue, 22 Apr 2025 09:50:11 -0700 Subject: [PATCH 07/93] Fix `jdtls.settings` not falling back to `jdtls.initialization_options.settings` like intended --- src/lib.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0c6aa21..4813d66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -349,15 +349,26 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result> { - LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.settings) - .or(self + // FIXME(Valentine Briese): I don't really like that we have a variable + // here, there're probably some `Result` and/or + // `Option` methods that would eliminate the + // need for this, but at least this is easy to + // read. + + let mut settings = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings); + + if !matches!(settings, Ok(Some(_))) { + settings = self .language_server_initialization_options(language_server_id, worktree) .map(|initialization_options| { initialization_options.and_then(|initialization_options| { initialization_options.get("settings").cloned() }) - })) + }) + } + + settings } fn label_for_completion( From 5f37a48cbae6ee8ac8264879032f853bf4d29006 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Tue, 22 Apr 2025 09:51:28 -0700 Subject: [PATCH 08/93] Bump to v6.0.3 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 673fc88..193d9ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.0.2" +version = "6.0.3" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index fd8a721..a400513 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.0.2" +version = "6.0.3" schema_version = 1 authors = [ "Valentine Briese ", From b874a408b9ddcd90a3c343db180643952cf976a0 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Wed, 21 May 2025 22:44:09 -0700 Subject: [PATCH 09/93] Fix Lombok path on Windows (#58) Resolves #57. --- src/lib.rs | 58 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4813d66..f828e96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ use std::{ collections::BTreeSet, + env::current_dir, fs::{self, create_dir}, - path::Path, + path::{Path, PathBuf}, }; use zed_extension_api::{ @@ -15,9 +16,11 @@ use zed_extension_api::{ settings::LspSettings, }; +const PATH_TO_STR_ERROR: &str = "failed to convert path to string"; + struct Java { - cached_binary_path: Option, - cached_lombok_path: Option, + cached_binary_path: Option, + cached_lombok_path: Option, } impl Java { @@ -25,7 +28,7 @@ impl Java { &mut self, language_server_id: &LanguageServerId, worktree: &Worktree, - ) -> zed::Result { + ) -> zed::Result { // Use cached path if exists if let Some(path) = &self.cached_binary_path { @@ -43,7 +46,7 @@ impl Java { }; if let Some(path_binary) = worktree.which(binary_name) { - return Ok(path_binary); + return Ok(PathBuf::from(path_binary)); } // Check for latest version @@ -128,11 +131,11 @@ impl Java { format!("attempt to get latest version's build resulted in a malformed response: {err}") })?; let latest_version_build = latest_version_build.trim_end(); - let prefix = "jdtls"; + let prefix = PathBuf::from("jdtls"); // Exclude ".tar.gz" let build_directory = &latest_version_build[..latest_version_build.len() - 7]; - let build_path = format!("{prefix}/{build_directory}"); - let binary_path = format!("{build_path}/bin/{binary_name}"); + let build_path = prefix.join(build_directory); + let binary_path = build_path.join("bin").join(binary_name); // If latest version isn't installed, if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { @@ -146,10 +149,10 @@ impl Java { &format!( "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", ), - &build_path, + build_path.to_str().ok_or(PATH_TO_STR_ERROR)?, DownloadedFileType::GzipTar, )?; - make_file_executable(&binary_path)?; + make_file_executable(binary_path.to_str().ok_or(PATH_TO_STR_ERROR)?)?; // ...and delete other versions @@ -182,7 +185,7 @@ impl Java { Ok(binary_path) } - fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { + fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { // Use cached path if exists if let Some(path) = &self.cached_lombok_path { @@ -222,10 +225,7 @@ impl Java { .ok_or("malformed GitHub tags response")?[1..]; let prefix = "lombok"; let jar_name = format!("lombok-{latest_version}.jar"); - let jar_path = Path::new(prefix) - .join(&jar_name) - .to_string_lossy() - .into_owned(); + let jar_path = Path::new(prefix).join(&jar_name); // If latest version isn't installed, if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { @@ -238,7 +238,7 @@ impl Java { create_dir(prefix).map_err(|err| err.to_string())?; download_file( &format!("https://projectlombok.org/downloads/{jar_name}"), - &jar_path, + jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, DownloadedFileType::Uncompressed, )?; @@ -318,18 +318,32 @@ impl Extension for Java { .unwrap_or(false); if lombok_enabled { + let mut current_dir = + current_dir().map_err(|err| format!("could not get current dir: {err}"))?; + + if current_platform().0 == Os::Windows { + current_dir = current_dir + .strip_prefix("/") + .map_err(|err| err.to_string())? + .to_path_buf(); + } + let lombok_jar_path = self.lombok_jar_path(language_server_id)?; - let lombok_jar_full_path = std::env::current_dir() - .map_err(|e| format!("could not get current dir: {e}"))? - .join(&lombok_jar_path) - .to_string_lossy() + let canonical_lombok_jar_path = current_dir + .join(lombok_jar_path) + .to_str() + .ok_or(PATH_TO_STR_ERROR)? .to_string(); - args.push(format!("--jvm-arg=-javaagent:{lombok_jar_full_path}")); + args.push(format!("--jvm-arg=-javaagent:{canonical_lombok_jar_path}")); } Ok(zed::Command { - command: self.language_server_binary_path(language_server_id, worktree)?, + command: self + .language_server_binary_path(language_server_id, worktree)? + .to_str() + .ok_or(PATH_TO_STR_ERROR)? + .to_string(), args, env, }) From d9fc21aa7888b46f1cac949ee2f0b5d393cf586f Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Wed, 21 May 2025 22:57:42 -0700 Subject: [PATCH 10/93] Bump to v6.0.4 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 193d9ac..0a7c8e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.0.3" +version = "6.0.4" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index a400513..6635c8a 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.0.3" +version = "6.0.4" schema_version = 1 authors = [ "Valentine Briese ", From c4ac94b16b86815a1f2d6a6cd72bf851ca2bf07a Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 6 Jun 2025 12:55:56 -0400 Subject: [PATCH 11/93] Add support for Java `.properties` files (#60) Tree-sitter: https://github.com/tree-sitter-grammars/tree-sitter-properties I don't use java, so don't know how useful this is, but it was easy enough to add support for. Thoughts @valentinegb? - Closes: https://github.com/zed-industries/extensions/issues/2703 CC: @blacksoulgem95 Screenshot 2025-06-05 at 13 55 47 --- extension.toml | 4 ++++ languages/properties/config.toml | 5 +++++ languages/properties/highlights.scm | 28 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 languages/properties/config.toml create mode 100644 languages/properties/highlights.scm diff --git a/extension.toml b/extension.toml index 6635c8a..bbbf286 100644 --- a/extension.toml +++ b/extension.toml @@ -14,6 +14,10 @@ repository = "https://github.com/zed-extensions/java" repository = "https://github.com/tree-sitter/tree-sitter-java" commit = "94703d5a6bed02b98e438d7cad1136c01a60ba2c" +[grammars.properties] +repository = "https://github.com/tree-sitter-grammars/tree-sitter-properties" +commit = "579b62f5ad8d96c2bb331f07d1408c92767531d9" + [language_servers.jdtls] name = "Eclipse JDT Language Server" language = "Java" diff --git a/languages/properties/config.toml b/languages/properties/config.toml new file mode 100644 index 0000000..9537eef --- /dev/null +++ b/languages/properties/config.toml @@ -0,0 +1,5 @@ +name = "Properties" +grammar = "properties" +path_suffixes = ["properties"] +line_comments = ["# "] +brackets = [{ start = "[", end = "]", close = true, newline = true }] diff --git a/languages/properties/highlights.scm b/languages/properties/highlights.scm new file mode 100644 index 0000000..5b71487 --- /dev/null +++ b/languages/properties/highlights.scm @@ -0,0 +1,28 @@ +(comment) @comment + +(key) @property + +(value) @string + +(value (escape) @string.escape) + +((index) @number + (#match? @number "^[0-9]+$")) + +((substitution (key) @constant) + (#match? @constant "^[A-Z0-9_]+")) + +(substitution + (key) @function + "::" @punctuation.special + (secret) @embedded) + +(property [ "=" ":" ] @operator) + +[ "${" "}" ] @punctuation.special + +(substitution ":" @punctuation.special) + +[ "[" "]" ] @punctuation.bracket + +[ "." "\\" ] @punctuation.delimiter From e4dfbda502331065591becd98f85d009859180fc Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Tue, 17 Jun 2025 11:06:39 -0700 Subject: [PATCH 12/93] Autoclose single and double quotes Resolves #63 --- languages/java/brackets.scm | 5 +++-- languages/java/config.toml | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/languages/java/brackets.scm b/languages/java/brackets.scm index 191fd9c..444d70b 100644 --- a/languages/java/brackets.scm +++ b/languages/java/brackets.scm @@ -1,3 +1,4 @@ -("(" @open ")" @close) -("[" @open "]" @close) ("{" @open "}" @close) +("[" @open "]" @close) +("(" @open ")" @close) +("\"" @open "\"" @close) diff --git a/languages/java/config.toml b/languages/java/config.toml index 121f5a1..1ec083e 100644 --- a/languages/java/config.toml +++ b/languages/java/config.toml @@ -7,6 +7,8 @@ brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string_literal"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["character_literal"] }, ] prettier_parser_name = "java" prettier_plugins = ["prettier-plugin-java"] From 91a032a9a8a4294f9375fac5551b96ab94950d77 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Tue, 17 Jun 2025 11:24:42 -0700 Subject: [PATCH 13/93] Add some configuration for block comments --- languages/java/config.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/languages/java/config.toml b/languages/java/config.toml index 1ec083e..d5f8981 100644 --- a/languages/java/config.toml +++ b/languages/java/config.toml @@ -9,6 +9,10 @@ brackets = [ { start = "(", end = ")", close = true, newline = false }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["string_literal"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["character_literal"] }, + # TODO: Figure out how the Rust language support is able to handle block comments so well + { start = "/*", end = " */", close = true, newline = true, not_in = ["string_literal", "block_comment"] }, ] +collapsed_placeholder = " /* ... */ " +documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } prettier_parser_name = "java" prettier_plugins = ["prettier-plugin-java"] From 6eb51a4a37b9909fd3fa2a0f256de3e66510e7af Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Tue, 17 Jun 2025 11:26:59 -0700 Subject: [PATCH 14/93] Bump to v6.1.0 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a7c8e3..aca1f68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.0.4" +version = "6.1.0" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index bbbf286..ae25be8 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.0.4" +version = "6.1.0" schema_version = 1 authors = [ "Valentine Briese ", From b65fe9af77f9b0edad458b207b75191b183405a2 Mon Sep 17 00:00:00 2001 From: chbk Date: Fri, 20 Jun 2025 18:35:25 +0200 Subject: [PATCH 15/93] Improve Java highlighting (#61) This PR makes Java highlighting more consistent with other languages. Release Notes: - Improved Java highlighting | Java 6.0.3 | With this PR | | --- | --- | | ![](https://github.com/user-attachments/assets/64897886-5711-43ea-b3f7-179d779d8ea6) | ![](https://github.com/user-attachments/assets/525b7d2f-df66-4a3e-ad61-8d52e4ed7e97) | ```java @interface Annotation { String value() default "value"; } interface Interface { public String message(); } class Test implements Interface { @Annotation(value="test") public String message() { String string; String nothing = null; String formatted = STR."string: \{nothing}"; return formatted; } } class Main { public static void main(String[] args) { Test test = new Test(); System.out.println(test.message()); } } ``` - `Annotation`: `type` -> `attribute`, as in [Python](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/python/highlights.scm#L285), [Java](https://github.com/zed-extensions/java/blob/c4ac94b16b86815a1f2d6a6cd72bf851ca2bf07a/languages/java/highlights.scm#L143) itself - `@`: `punctuation` as in [Python](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/python/highlights.scm#L40), [JavaScript](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/javascript/highlights.scm#L222), etc. - `null`: `type` -> `constant.builtin`, as in [JavaScript](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/javascript/highlights.scm#L72), [Go](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/go/highlights.scm#L136), etc. - `new`: `operator` -> `keyword.operator` as in [JavaScript](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/javascript/highlights.scm#L196), [C++](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/cpp/highlights.scm#L115), etc. - `\{ }`: `string.special.symbol` -> `punctuation` `embedded`, as in [Python](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/python/highlights.scm#L149), [JavaScript](https://github.com/zed-industries/zed/blob/8332e60ca99187b1c08be50173357b2f1cc68dc4/crates/languages/src/javascript/highlights.scm#L214), etc. --- languages/java/highlights.scm | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/languages/java/highlights.scm b/languages/java/highlights.scm index 29770d1..a156246 100644 --- a/languages/java/highlights.scm +++ b/languages/java/highlights.scm @@ -73,7 +73,7 @@ name: (identifier) @type) (annotation_type_declaration - name: (identifier) @type) + name: (identifier) @attribute) (class_declaration name: (identifier) @type) @@ -139,11 +139,11 @@ ; Annotations (annotation - "@" @attribute + "@" @punctuation.special name: (identifier) @attribute) (marker_annotation - "@" @attribute + "@" @punctuation.special name: (identifier) @attribute) ; Literals @@ -167,7 +167,7 @@ (false) ] @boolean -(null_literal) @type +(null_literal) @constant.builtin ; Keywords [ @@ -184,6 +184,7 @@ "permits" "to" "with" + "new" ] @keyword [ @@ -212,8 +213,6 @@ "yield" ] @keyword -"new" @operator - ; Conditionals [ "if" @@ -289,7 +288,7 @@ [ "\\{" "}" - ] @string.special.symbol) + ] @punctuation.special) @embedded ; Exceptions [ From 7bb2573dceaa27bade3f4d8e1b09f603593d24ed Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Fri, 20 Jun 2025 09:39:38 -0700 Subject: [PATCH 16/93] Bump to v6.2.0 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aca1f68..315687c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.1.0" +version = "6.2.0" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index ae25be8..827e59a 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.1.0" +version = "6.2.0" schema_version = 1 authors = [ "Valentine Briese ", From 12a72509a4ecfe4a9c98db2d064693cec89b4b12 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Sat, 23 Aug 2025 00:56:23 +0300 Subject: [PATCH 17/93] Add support for debugger (#71) Implement #67 --------- Co-authored-by: Valentine Briese --- Cargo.toml | 4 +- debug_adapter_schemas/Java.json | 153 ++++++++++++++ extension.toml | 2 + languages/java/config.toml | 1 + src/debugger.rs | 344 ++++++++++++++++++++++++++++++++ src/lib.rs | 225 +++++++++++++++++---- src/lsp.rs | 146 ++++++++++++++ src/proxy.mjs | 260 ++++++++++++++++++++++++ 8 files changed, 1097 insertions(+), 38 deletions(-) create mode 100644 debug_adapter_schemas/Java.json create mode 100644 src/debugger.rs create mode 100644 src/lsp.rs create mode 100644 src/proxy.mjs diff --git a/Cargo.toml b/Cargo.toml index 315687c..f14fc05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,6 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.3.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +zed_extension_api = "0.6.0" diff --git a/debug_adapter_schemas/Java.json b/debug_adapter_schemas/Java.json new file mode 100644 index 0000000..896a3b9 --- /dev/null +++ b/debug_adapter_schemas/Java.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "oneOf": [ + { + "title": "Launch", + "properties": { + "request": { + "type": "string", + "enum": ["launch"], + "description": "The request type for the Java debug adapter, always \"launch\"." + }, + "projectName": { + "type": "string", + "description": "The fully qualified name of the project" + }, + "mainClass": { + "type": "string", + "description": "The fully qualified name of the class containing the main method. If not specified, the debugger automatically resolves the possible main class from the current project." + }, + "args": { + "type": "string", + "description": "The command line arguments passed to the program." + }, + "vmArgs": { + "type": "string", + "description": "The extra options and system properties for the JVM (e.g., -Xms -Xmx -D=)." + }, + "encoding": { + "type": "string", + "description": "The file.encoding setting for the JVM. Possible values can be found in the Supported Encodings documentation." + }, + "classPaths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The classpaths for launching the JVM. If not specified, the debugger will automatically resolve them from the current project. If multiple values are specified, the debugger will merge them together. Available values for special handling include: '$Auto' - Automatically resolve the classpaths of the current project. '$Runtime' - The classpaths within the 'runtime' scope of the current project. '$Test' - The classpaths within the 'test' scope of the current project." + }, + "modulePaths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The modulepaths for launching the JVM. If not specified, the debugger will automatically resolve them from the current project." + }, + "cwd": { + "type": "string", + "description": "The working directory of the program. Defaults to '${workspaceFolder}'." + }, + "env": { + "type": "object", + "description": "The extra environment variables for the program.", + "additionalProperties": { + "type": "string" + } + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically pause the program after launching." + }, + "noDebug": { + "type": "boolean", + "description": "If set to 'true', disables debugging. The program will be launched without attaching the debugger. Useful for launching a program without debugging, for instance for profiling." + }, + "console": { + "type": "string", + "enum": ["internalConsole", "integratedTerminal", "externalTerminal"], + "description": "The specified console to launch the program." + }, + "shortenCommandLine": { + "type": "string", + "enum": ["none", "jarmanifest", "argfile"], + "description": "Provides multiple approaches to shorten the command line when it exceeds the maximum command line string limitation allowed by the OS." + }, + "launcherScript": { + "type": "string", + "description": "The path to an external launcher script to use instead of the debugger's built-in launcher. This is an advanced option for customizing how the JVM is launched." + }, + "javaExec": { + "type": "string", + "description": "The path to the Java executable to use. By default, the project JDK's Java executable is used." + } + }, + "required": ["request"] + }, + { + "title": "Attach", + "properties": { + "request": { + "type": "string", + "enum": ["attach"], + "description": "The request type for the Java debug adapter, always \"attach\"." + }, + "hostName": { + "type": "string", + "description": "The host name or IP address of the remote debuggee." + }, + "port": { + "type": "integer", + "description": "The debug port of the remote debuggee." + }, + "timeout": { + "type": "integer", + "description": "Timeout value before reconnecting, in milliseconds (default to 30000ms)." + }, + "projectName": { + "type": "string", + "description": "The fully qualified name of the project" + }, + "sourcePaths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The source paths for the debugger. If not specified, the debugger will automatically resolve the source paths from the current project." + }, + "stepFilters": { + "type": "object", + "properties": { + "allowClasses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Restricts the events generated by the request to those whose location is in a class whose name matches this restricted regular expression. Regular expressions are limited to exact matches and patterns that begin with '*' or end with '*'; for example, \"*.Foo\" or \"java.*\"." + }, + "skipClasses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Restricts the events generated by the request to those whose location is in a class whose name does not match this restricted regular expression, e.g. \"java.*\" or \"*.Foo\"." + }, + "skipSynthetics": { + "type": "boolean", + "description": "If true, skips synthetic methods." + }, + "skipStaticInitializers": { + "type": "boolean", + "description": "If true, skips static initializers." + }, + "skipConstructors": { + "type": "boolean", + "description": "If true, skips constructors." + } + } + } + }, + "required": ["request", "hostName", "port"] + } + ] +} diff --git a/extension.toml b/extension.toml index 827e59a..61977d6 100644 --- a/extension.toml +++ b/extension.toml @@ -21,3 +21,5 @@ commit = "579b62f5ad8d96c2bb331f07d1408c92767531d9" [language_servers.jdtls] name = "Eclipse JDT Language Server" language = "Java" + +[debug_adapters.Java] diff --git a/languages/java/config.toml b/languages/java/config.toml index d5f8981..a42025a 100644 --- a/languages/java/config.toml +++ b/languages/java/config.toml @@ -16,3 +16,4 @@ collapsed_placeholder = " /* ... */ " documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } prettier_parser_name = "java" prettier_plugins = ["prettier-plugin-java"] +debuggers = ["Java"] \ No newline at end of file diff --git a/src/debugger.rs b/src/debugger.rs new file mode 100644 index 0000000..8f778ed --- /dev/null +++ b/src/debugger.rs @@ -0,0 +1,344 @@ +use std::{collections::HashMap, env::current_dir, fs, path::PathBuf}; + +use serde::{Deserialize, Serialize}; +use zed_extension_api::{ + self as zed, DownloadedFileType, LanguageServerId, LanguageServerInstallationStatus, Os, + TcpArgumentsTemplate, Worktree, current_platform, download_file, + http_client::{HttpMethod, HttpRequest, fetch}, + serde_json::{self, Value, json}, + set_language_server_installation_status, +}; + +use crate::lsp::LspWrapper; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct JavaDebugLaunchConfig { + request: String, + #[serde(skip_serializing_if = "Option::is_none")] + project_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + main_class: Option, + #[serde(skip_serializing_if = "Option::is_none")] + args: Option, + #[serde(skip_serializing_if = "Option::is_none")] + vm_args: Option, + #[serde(skip_serializing_if = "Option::is_none")] + encoding: Option, + #[serde(skip_serializing_if = "Option::is_none")] + class_paths: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + module_paths: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + cwd: Option, + #[serde(skip_serializing_if = "Option::is_none")] + env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + stop_on_entry: Option, + #[serde(skip_serializing_if = "Option::is_none")] + no_debug: Option, + #[serde(skip_serializing_if = "Option::is_none")] + console: Option, + #[serde(skip_serializing_if = "Option::is_none")] + shorten_command_line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + launcher_script: Option, + #[serde(skip_serializing_if = "Option::is_none")] + java_exec: Option, +} + +const TEST_SCOPE: &str = "$Test"; +const AUTO_SCOPE: &str = "$Auto"; +const RUNTIME_SCOPE: &str = "$Runtime"; + +const SCOPES: [&str; 3] = [TEST_SCOPE, AUTO_SCOPE, RUNTIME_SCOPE]; + +const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; + +const MAVEN_SEARCH_URL: &str = + "https://search.maven.org/solrsearch/select?q=a:com.microsoft.java.debug.plugin"; + +pub struct Debugger { + lsp: LspWrapper, + plugin_path: Option, +} + +impl Debugger { + pub fn new(lsp: LspWrapper) -> Debugger { + Debugger { + plugin_path: None, + lsp, + } + } + + pub fn get_or_download( + &mut self, + language_server_id: &LanguageServerId, + ) -> zed::Result { + let prefix = "debugger"; + + if let Some(path) = &self.plugin_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); + } + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + let res = fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url(MAVEN_SEARCH_URL) + .build()?, + ); + + // Maven loves to be down, trying to resolve it gracefully + if let Err(err) = &res { + if !fs::metadata(prefix).is_ok_and(|stat| stat.is_dir()) { + return Err(err.to_owned()); + } + + // If it's not a 5xx code, then return an error. + if !err.contains("status code 5") { + return Err(err.to_owned()); + } + + let exists = fs::read_dir(prefix) + .ok() + .and_then(|dir| dir.last().map(|v| v.ok())) + .flatten(); + + if let Some(file) = exists { + if !file.metadata().is_ok_and(|stat| stat.is_file()) { + return Err(err.to_owned()); + } + + if !file + .file_name() + .to_str() + .is_some_and(|name| name.ends_with(".jar")) + { + return Err(err.to_owned()); + } + + let jar_path = PathBuf::from(prefix).join(file.file_name()); + self.plugin_path = Some(jar_path.clone()); + + return Ok(jar_path); + } + } + + let maven_response_body = serde_json::from_slice::(&res?.body) + .map_err(|err| format!("failed to deserialize Maven response: {err}"))?; + + let latest_version = maven_response_body + .pointer("/response/docs/0/latestVersion") + .and_then(|v| v.as_str()) + .ok_or("Malformed maven response")?; + + let artifact = maven_response_body + .pointer("/response/docs/0/a") + .and_then(|v| v.as_str()) + .ok_or("Malformed maven response")?; + + let jar_name = format!("{artifact}-{latest_version}.jar"); + let jar_path = PathBuf::from(prefix).join(&jar_name); + + if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { + if let Err(err) = fs::remove_dir_all(prefix) { + println!("failed to remove directory entry: {err}"); + } + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + fs::create_dir(prefix).map_err(|err| err.to_string())?; + + let url = format!( + "https://repo1.maven.org/maven2/com/microsoft/java/{artifact}/{latest_version}/{jar_name}" + ); + + download_file( + url.as_str(), + jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, + DownloadedFileType::Uncompressed, + ) + .map_err(|err| format!("Failed to download {url} {err}"))?; + } + + self.plugin_path = Some(jar_path.clone()); + Ok(jar_path) + } + + pub fn start_session(&self) -> zed::Result { + let port = self.lsp.get()?.request::( + "workspace/executeCommand", + json!({ "command": "vscode.java.startDebugSession" }), + )?; + + Ok(TcpArgumentsTemplate { + host: None, + port: Some(port), + timeout: None, + }) + } + + pub fn inject_config(&self, worktree: &Worktree, config_string: String) -> zed::Result { + let config: Value = serde_json::from_str(&config_string) + .map_err(|err| format!("Failed to parse debug config {err}"))?; + + if config + .get("request") + .and_then(Value::as_str) + .is_some_and(|req| req != "launch") + { + return Ok(config_string); + } + + let mut config = serde_json::from_value::(config) + .map_err(|err| format!("Failed to parse java debug config {err}"))?; + + let workspace_folder = worktree.root_path(); + + let (main_class, project_name) = { + let arguments = [config.main_class.clone(), config.project_name.clone()] + .iter() + .flatten() + .cloned() + .collect::>(); + + let entries = self + .lsp + .get()? + .resolve_main_class(arguments)? + .into_iter() + .filter(|entry| { + config + .main_class + .as_ref() + .map(|class| &entry.main_class == class) + .unwrap_or(true) + }) + .filter(|entry| { + config + .project_name + .as_ref() + .map(|class| &entry.project_name == class) + .unwrap_or(true) + }) + .collect::>(); + + if entries.len() > 1 { + return Err("Project have multiple entry points, you must explicitly specify \"mainClass\" or \"projectName\"".to_owned()); + } + + match entries.first() { + None => (config.main_class, config.project_name), + Some(entry) => ( + Some(entry.main_class.to_owned()), + Some(entry.project_name.to_owned()), + ), + } + }; + + let mut classpaths = config.class_paths.unwrap_or(vec![AUTO_SCOPE.to_string()]); + + if classpaths + .iter() + .any(|class| SCOPES.contains(&class.as_str())) + { + // https://github.com/microsoft/vscode-java-debug/blob/main/src/configurationProvider.ts#L518 + let scope = { + if classpaths.iter().any(|class| class == TEST_SCOPE) { + Some("test".to_string()) + } else if classpaths.iter().any(|class| class == AUTO_SCOPE) { + None + } else if classpaths.iter().any(|class| class == RUNTIME_SCOPE) { + Some("runtime".to_string()) + } else { + None + } + }; + + let arguments = vec![main_class.clone(), project_name.clone(), scope.clone()]; + + let result = self.lsp.get()?.resolve_class_path(arguments)?; + + for resolved in result { + classpaths.extend(resolved); + } + } + + classpaths.retain(|class| !SCOPES.contains(&class.as_str())); + classpaths.dedup(); + + config.class_paths = Some(classpaths); + + config.main_class = main_class; + config.project_name = project_name; + + config.cwd = config.cwd.or(Some(workspace_folder.to_string())); + + let config = serde_json::to_string(&config) + .map_err(|err| format!("Failed to stringify debug config {err}"))? + .replace("${workspaceFolder}", &workspace_folder); + + Ok(config) + } + + pub fn inject_plugin_into_options( + &self, + initialization_options: Option, + ) -> zed::Result { + let mut current_dir = + current_dir().map_err(|err| format!("could not get current dir: {err}"))?; + + if current_platform().0 == Os::Windows { + current_dir = current_dir + .strip_prefix("/") + .map_err(|err| err.to_string())? + .to_path_buf(); + } + + let canonical_path = Value::String( + current_dir + .join( + self.plugin_path + .as_ref() + .ok_or("Debugger is not loaded yet")?, + ) + .to_string_lossy() + .to_string(), + ); + + match initialization_options { + None => Ok(json!({ + "bundles": [canonical_path] + })), + Some(options) => { + let mut options = options.clone(); + + let mut bundles = options + .get_mut("bundles") + .unwrap_or(&mut Value::Array(vec![])) + .take(); + + let bundles_vec = bundles + .as_array_mut() + .ok_or("Invalid initialization_options format")?; + + if !bundles_vec.contains(&canonical_path) { + bundles_vec.push(canonical_path); + } + + options["bundles"] = bundles; + + Ok(options) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f828e96..0e51ce3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,40 +1,74 @@ +mod debugger; +mod lsp; use std::{ collections::BTreeSet, env::current_dir, fs::{self, create_dir}, path::{Path, PathBuf}, + str::FromStr, }; use zed_extension_api::{ - self as zed, CodeLabel, CodeLabelSpan, DownloadedFileType, Extension, LanguageServerId, - LanguageServerInstallationStatus, Os, Worktree, current_platform, download_file, + self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, + DownloadedFileType, Extension, LanguageServerId, LanguageServerInstallationStatus, Os, + StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, Worktree, + current_platform, download_file, http_client::{HttpMethod, HttpRequest, fetch}, lsp::{Completion, CompletionKind}, make_file_executable, register_extension, - serde_json::{self, Value}, + serde_json::{self, Value, json}, set_language_server_installation_status, settings::LspSettings, }; +use crate::{debugger::Debugger, lsp::LspWrapper}; + +const PROXY_FILE: &str = include_str!("proxy.mjs"); +const DEBUG_ADAPTER_NAME: &str = "Java"; const PATH_TO_STR_ERROR: &str = "failed to convert path to string"; struct Java { cached_binary_path: Option, cached_lombok_path: Option, + integrations: Option<(LspWrapper, Debugger)>, } impl Java { + #[allow(dead_code)] + fn lsp(&mut self) -> zed::Result<&LspWrapper> { + self.integrations + .as_ref() + .ok_or("Lsp client is not initialized yet".to_owned()) + .map(|v| &v.0) + } + + fn debugger(&mut self) -> zed::Result<&mut Debugger> { + self.integrations + .as_mut() + .ok_or("Lsp client is not initialized yet".to_owned()) + .map(|v| &mut v.1) + } + fn language_server_binary_path( &mut self, language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result { + // Initialize lsp client and debugger + + if self.integrations.is_none() { + let lsp = LspWrapper::new(worktree.root_path()); + let debugger = Debugger::new(lsp.clone()); + + self.integrations = Some((lsp, debugger)); + } + // Use cached path if exists - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).is_ok_and(|stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); } // Use $PATH if binary is in it @@ -164,10 +198,10 @@ impl Java { for entry in entries { match entry { Ok(entry) => { - if entry.file_name().to_str() != Some(build_directory) { - if let Err(err) = fs::remove_dir_all(entry.path()) { - println!("failed to remove directory entry: {err}"); - } + if entry.file_name().to_str() != Some(build_directory) + && let Err(err) = fs::remove_dir_all(entry.path()) + { + println!("failed to remove directory entry: {err}"); } } Err(err) => println!("failed to load directory entry: {err}"), @@ -188,10 +222,10 @@ impl Java { fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { // Use cached path if exists - if let Some(path) = &self.cached_lombok_path { - if fs::metadata(path).is_ok_and(|stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_lombok_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); } // Check for latest version @@ -252,10 +286,10 @@ impl Java { for entry in entries { match entry { Ok(entry) => { - if entry.file_name().to_str() != Some(&jar_name) { - if let Err(err) = fs::remove_dir_all(entry.path()) { - println!("failed to remove directory entry: {err}"); - } + if entry.file_name().to_str() != Some(&jar_name) + && let Err(err) = fs::remove_dir_all(entry.path()) + { + println!("failed to remove directory entry: {err}"); } } Err(err) => println!("failed to load directory entry: {err}"), @@ -282,6 +316,101 @@ impl Extension for Java { Self { cached_binary_path: None, cached_lombok_path: None, + integrations: None, + } + } + + fn get_dap_binary( + &mut self, + adapter_name: String, + config: DebugTaskDefinition, + _user_provided_debug_adapter_path: Option, + worktree: &Worktree, + ) -> zed_extension_api::Result { + if adapter_name != DEBUG_ADAPTER_NAME { + return Err(format!( + "Cannot create binary for adapter \"{adapter_name}\"" + )); + } + + if self.integrations.is_some() { + self.lsp()?.switch_workspace(worktree.root_path())?; + } + + Ok(DebugAdapterBinary { + command: None, + arguments: vec![], + cwd: Some(worktree.root_path()), + envs: vec![], + request_args: StartDebuggingRequestArguments { + request: self.dap_request_kind( + adapter_name, + Value::from_str(config.config.as_str()) + .map_err(|e| format!("Invalid JSON configuration: {e}"))?, + )?, + configuration: self.debugger()?.inject_config(worktree, config.config)?, + }, + connection: Some(zed::resolve_tcp_template( + self.debugger()?.start_session()?, + )?), + }) + } + + fn dap_request_kind( + &mut self, + adapter_name: String, + config: Value, + ) -> Result { + if adapter_name != DEBUG_ADAPTER_NAME { + return Err(format!( + "Cannot create binary for adapter \"{adapter_name}\"" + )); + } + + match config.get("request") { + Some(launch) if launch == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch), + Some(attach) if attach == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach), + Some(value) => Err(format!( + "Unexpected value for `request` key in Java debug adapter configuration: {value:?}" + )), + None => { + Err("Missing required `request` field in Java debug adapter configuration".into()) + } + } + } + + fn dap_config_to_scenario( + &mut self, + config: zed::DebugConfig, + ) -> zed::Result { + match config.request { + zed::DebugRequest::Attach(attach) => { + let debug_config = if let Some(process_id) = attach.process_id { + json!({ + "request": "attach", + "processId": process_id, + "stopOnEntry": config.stop_on_entry + }) + } else { + json!({ + "request": "attach", + "hostName": "localhost", + "port": 5005, + }) + }; + + Ok(zed::DebugScenario { + adapter: config.adapter, + build: None, + tcp_connection: Some(self.debugger()?.start_session()?), + label: "Attach to Java process".to_string(), + config: debug_config.to_string(), + }) + } + + zed::DebugRequest::Launch(_launch) => { + Err("Java Extension doesn't support launching".to_string()) + } } } @@ -290,6 +419,16 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result { + let mut current_dir = + current_dir().map_err(|err| format!("could not get current dir: {err}"))?; + + if current_platform().0 == Os::Windows { + current_dir = current_dir + .strip_prefix("/") + .map_err(|err| err.to_string())? + .to_path_buf(); + } + let configuration = self.language_server_workspace_configuration(language_server_id, worktree)?; let java_home = configuration.as_ref().and_then(|configuration| { @@ -301,13 +440,25 @@ impl Extension for Java { .map(|java_home_str| java_home_str.to_string()) }) }); + let mut env = Vec::new(); if let Some(java_home) = java_home { env.push(("JAVA_HOME".to_string(), java_home)); } - let mut args = Vec::new(); + let mut args = vec![ + "--input-type=module".to_string(), + "-e".to_string(), + PROXY_FILE.to_string(), + current_dir.to_str().ok_or(PATH_TO_STR_ERROR)?.to_string(), + current_dir + .join(self.language_server_binary_path(language_server_id, worktree)?) + .to_str() + .ok_or(PATH_TO_STR_ERROR)? + .to_string(), + ]; + // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true let lombok_enabled = configuration .and_then(|configuration| { @@ -318,16 +469,6 @@ impl Extension for Java { .unwrap_or(false); if lombok_enabled { - let mut current_dir = - current_dir().map_err(|err| format!("could not get current dir: {err}"))?; - - if current_platform().0 == Os::Windows { - current_dir = current_dir - .strip_prefix("/") - .map_err(|err| err.to_string())? - .to_path_buf(); - } - let lombok_jar_path = self.lombok_jar_path(language_server_id)?; let canonical_lombok_jar_path = current_dir .join(lombok_jar_path) @@ -338,12 +479,12 @@ impl Extension for Java { args.push(format!("--jvm-arg=-javaagent:{canonical_lombok_jar_path}")); } + // download debugger if not exists + self.debugger()?.get_or_download(language_server_id)?; + self.lsp()?.switch_workspace(worktree.root_path())?; + Ok(zed::Command { - command: self - .language_server_binary_path(language_server_id, worktree)? - .to_str() - .ok_or(PATH_TO_STR_ERROR)? - .to_string(), + command: zed::node_binary_path()?, args, env, }) @@ -354,8 +495,18 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result> { - LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.initialization_options) + if self.integrations.is_some() { + self.lsp()?.switch_workspace(worktree.root_path())?; + } + + let options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.initialization_options)?; + + if self.integrations.is_some() { + return Ok(Some(self.debugger()?.inject_plugin_into_options(options)?)); + } + + Ok(options) } fn language_server_workspace_configuration( diff --git a/src/lsp.rs b/src/lsp.rs new file mode 100644 index 0000000..c567483 --- /dev/null +++ b/src/lsp.rs @@ -0,0 +1,146 @@ +use std::{ + fs::{self}, + path::Path, + sync::{Arc, RwLock}, +}; + +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::json; +use zed_extension_api::{ + self as zed, + http_client::{HttpMethod, HttpRequest, fetch}, + serde_json::{self, Map, Value}, +}; + +/** + * `proxy.mjs` starts an HTTP server and writes its port to + * `${workdir}/proxy/${hex(project_root)}`. + * + * This allows us to send LSP requests directly from the Java extension. + * It’s a temporary workaround until `zed_extension_api` + * provides the ability to send LSP requests directly. +*/ +pub struct LspClient { + workspace: String, +} + +#[derive(Clone)] +pub struct LspWrapper(Arc>); + +impl LspWrapper { + pub fn new(workspace: String) -> Self { + LspWrapper(Arc::new(RwLock::new(LspClient { workspace }))) + } + + pub fn get(&self) -> zed::Result> { + self.0 + .read() + .map_err(|err| format!("LspClient RwLock poisoned during read {err}")) + } + + pub fn switch_workspace(&self, workspace: String) -> zed::Result<()> { + let mut lock = self + .0 + .write() + .map_err(|err| format!("LspClient RwLock poisoned during read {err}"))?; + + lock.workspace = workspace; + + Ok(()) + } +} + +impl LspClient { + pub fn resolve_class_path(&self, args: Vec>) -> zed::Result>> { + self.request::>>( + "workspace/executeCommand", + json!({ + "command": "vscode.java.resolveClasspath", + "arguments": args + }), + ) + } + + pub fn resolve_main_class(&self, args: Vec) -> zed::Result> { + self.request::>( + "workspace/executeCommand", + json!({ + "command": "vscode.java.resolveMainClass", + "arguments": args + }), + ) + } + + pub fn request(&self, method: &str, params: Value) -> Result + where + T: DeserializeOwned, + { + // We cannot cache it because the user may restart the LSP + let port = { + let filename = string_to_hex(&self.workspace); + + let port_path = Path::new("proxy").join(filename); + + if !fs::metadata(&port_path).is_ok_and(|file| file.is_file()) { + return Err("Failed to find lsp port file".to_owned()); + } + + fs::read_to_string(port_path) + .map_err(|e| format!("Failed to read a lsp proxy port from file {e}"))? + .parse::() + .map_err(|e| format!("Failed to read a lsp proxy port, file corrupted {e}"))? + }; + + let mut body = Map::new(); + body.insert("method".to_owned(), Value::String(method.to_owned())); + body.insert("params".to_owned(), params); + + let res = fetch( + &HttpRequest::builder() + .method(HttpMethod::Post) + .url(format!("http://localhost:{port}")) + .body(Value::Object(body).to_string()) + .build()?, + ) + .map_err(|e| format!("Failed to send request to lsp proxy {e}"))?; + + let data: LspResponse = serde_json::from_slice(&res.body) + .map_err(|e| format!("Failed to parse response from lsp proxy {e}"))?; + + match data { + LspResponse::Success { result } => Ok(result), + LspResponse::Error { error } => { + Err(format!("{} {} {}", error.code, error.message, error.data)) + } + } + } +} +fn string_to_hex(s: &str) -> String { + let mut hex_string = String::new(); + for byte in s.as_bytes() { + hex_string.push_str(&format!("{:02x}", byte)); + } + hex_string +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum LspResponse { + Success { result: T }, + Error { error: LspError }, +} + +#[derive(Serialize, Deserialize)] +pub struct LspError { + code: i64, + message: String, + data: Value, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MainClassEntry { + pub main_class: String, + pub project_name: String, + pub file_path: String, +} diff --git a/src/proxy.mjs b/src/proxy.mjs new file mode 100644 index 0000000..686eab1 --- /dev/null +++ b/src/proxy.mjs @@ -0,0 +1,260 @@ +import { Buffer } from "node:buffer"; +import { spawn } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { + existsSync, + mkdirSync, + readdirSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { Transform } from "node:stream"; +import { text } from "node:stream/consumers"; + +const HTTP_PORT = 0; // 0 - random free one +const HEADER_SEPARATOR = Buffer.from("\r\n", "ascii"); +const CONTENT_SEPARATOR = Buffer.from("\r\n\r\n", "ascii"); +const NAME_VALUE_SEPARATOR = Buffer.from(": ", "ascii"); +const LENGTH_HEADER = "Content-Length"; +const TIMEOUT = 5_000; + +const workdir = process.argv[1]; +const bin = process.argv[2]; +const args = process.argv.slice(3); + +const PROXY_ID = Buffer.from(process.cwd().replace(/\/+$/, "")).toString("hex"); +const PROXY_HTTP_PORT_FILE = join(workdir, "proxy", PROXY_ID); + +const lsp = spawn(bin, args); +const proxy = createLspProxy({ server: lsp, proxy: process }); + +proxy.on("client", (data, passthrough) => { + passthrough(); +}); +proxy.on("server", (data, passthrough) => { + passthrough(); +}); + +const server = createServer(async (req, res) => { + if (req.method !== "POST") { + res.status = 405; + res.end("Method not allowed"); + return; + } + + const data = await text(req) + .then(safeJsonParse) + .catch(() => null); + + if (!data) { + res.status = 400; + res.end("Bad Request"); + return; + } + + const result = await proxy.request(data.method, data.params); + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.write(JSON.stringify(result)); + res.end(); +}).listen(HTTP_PORT, () => { + mkdirSync(dirname(PROXY_HTTP_PORT_FILE), { recursive: true }); + writeFileSync(PROXY_HTTP_PORT_FILE, server.address().port.toString()); +}); + +export function createLspProxy({ + server: { stdin: serverStdin, stdout: serverStdout, stderr: serverStderr }, + proxy: { stdin: proxyStdin, stdout: proxyStdout, stderr: proxyStderr }, +}) { + const events = new EventEmitter(); + const queue = new Map(); + const nextid = iterid(); + + proxyStdin.pipe(lspMessageSeparator()).on("data", (data) => { + events.emit("client", parse(data), () => serverStdin.write(data)); + }); + + serverStdout.pipe(lspMessageSeparator()).on("data", (data) => { + const message = parse(data); + + const pending = queue.get(message?.id); + if (pending) { + pending(message); + queue.delete(message.id); + return; + } + + events.emit("server", message, () => proxyStdout.write(data)); + }); + + serverStderr.pipe(proxyStderr); + + return Object.assign(events, { + /** + * + * @param {string} method + * @param {any} params + * @returns void + */ + notification(method, params) { + proxyStdout.write(stringify({ jsonrpc: "2.0", method, params })); + }, + + /** + * + * @param {string} method + * @param {any} params + * @returns Promise + */ + request(method, params) { + return new Promise((resolve, reject) => { + const id = nextid(); + queue.set(id, resolve); + + setTimeout(() => { + if (queue.has(id)) { + reject({ + jsonrpc: "2.0", + id, + error: { + code: -32803, + message: "Request to language server timed out after 5000ms.", + }, + }); + this.cancel(id); + } + }, TIMEOUT); + + serverStdin.write(stringify({ jsonrpc: "2.0", id, method, params })); + }); + }, + + cancel(id) { + queue.delete(id); + + serverStdin.write( + stringify({ + jsonrpc: "2.0", + method: "$/cancelRequest", + params: { id }, + }), + ); + }, + }); +} + +function iterid() { + let acc = 1; + return () => PROXY_ID + "-" + acc++; +} + +/** + * The base protocol consists of a header and a content part (comparable to HTTP). + * The header and content part are separated by a ‘\r\n’. + * + * The header part consists of header fields. + * Each header field is comprised of a name and a value, + * separated by ‘: ‘ (a colon and a space). + * The structure of header fields conforms to the HTTP semantic. + * Each header field is terminated by ‘\r\n’. + * Considering the last header field and the overall header + * itself are each terminated with ‘\r\n’, + * and that at least one header is mandatory, + * this means that two ‘\r\n’ sequences always immediately precede + * the content part of a message. + * + * @returns {Transform} + * @see [language-server-protocol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#headerPart) + */ +function lspMessageSeparator() { + let buffer = Buffer.alloc(0); + let contentLength = null; + let headersLength = null; + + return new Transform({ + transform(chunk, encoding, callback) { + buffer = Buffer.concat([buffer, chunk]); + + // A single chunk may contain multiple messages + while (true) { + // Wait until we get the whole headers block + if (buffer.indexOf(CONTENT_SEPARATOR) === -1) { + break; + } + + if (!headersLength) { + const headersEnd = buffer.indexOf(CONTENT_SEPARATOR); + const headers = Object.fromEntries( + buffer + .subarray(0, headersEnd) + .toString() + .split(HEADER_SEPARATOR) + .map((header) => header.split(NAME_VALUE_SEPARATOR)) + .map(([name, value]) => [name.toLowerCase(), value]), + ); + + // A "Content-Length" header must always be present + contentLength = parseInt(headers[LENGTH_HEADER.toLowerCase()], 10); + headersLength = headersEnd + CONTENT_SEPARATOR.length; + } + + const msgLength = headersLength + contentLength; + + // Wait until we get the whole content part + if (buffer.length < msgLength) { + break; + } + + this.push(buffer.subarray(0, msgLength)); + + buffer = buffer.subarray(msgLength); + contentLength = null; + headersLength = null; + } + + callback(); + }, + }); +} + +/** + * + * @param {any} content + * @returns {string} + */ +function stringify(content) { + const json = JSON.stringify(content); + return ( + LENGTH_HEADER + + NAME_VALUE_SEPARATOR + + json.length + + CONTENT_SEPARATOR + + json + ); +} + +/** + * + * @param {string} message + * @returns {any | null} + */ +function parse(message) { + const content = message.slice(message.indexOf(CONTENT_SEPARATOR)); + return safeJsonParse(content); +} + +/** + * + * @param {string} json + * @returns {any | null} + */ +function safeJsonParse(json) { + try { + return JSON.parse(json); + } catch (err) { + return null; + } +} From 301f4146f86002ddc5bcff65f5c05f5dd03b2523 Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Fri, 22 Aug 2025 14:59:39 -0700 Subject: [PATCH 18/93] Bump to v6.3.0 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f14fc05..4303faf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.2.0" +version = "6.3.0" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index 61977d6..cb551b2 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.2.0" +version = "6.3.0" schema_version = 1 authors = [ "Valentine Briese ", From 79cbdc13cb8a7f7573242d3f8af248ce93b3eda1 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 28 Aug 2025 01:07:53 +0200 Subject: [PATCH 19/93] Add overrides and update config accordingly (#76) Hey everyone! I stumbled across the `TODO` whilst looking through the repo and since I fixed this issue earlier for the Kotlin extension, I thought I'd quickly do that for Java as well! What was missing here for documentation comments to properly work was that we use the overrides from the `overrides.scm` for these wo work properly and you were missing that file. I believe we lack any documentation for that and I added that to my TODO-list. Hence, I added the `overrides.scm` here and updated the config accordingly. Appreciate everything you do, with this change, documentation comments should now properly work (as well as some other goodies of disabled edit predictions in comments if you have that set in your settings). Let me know what you think, happy to adjust as needed. Cheers! --- languages/java/config.toml | 12 ++++++------ languages/java/overrides.scm | 9 +++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 languages/java/overrides.scm diff --git a/languages/java/config.toml b/languages/java/config.toml index a42025a..5104ab7 100644 --- a/languages/java/config.toml +++ b/languages/java/config.toml @@ -7,13 +7,13 @@ brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = false }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["string_literal"] }, - { start = "'", end = "'", close = true, newline = false, not_in = ["character_literal"] }, - # TODO: Figure out how the Rust language support is able to handle block comments so well - { start = "/*", end = " */", close = true, newline = true, not_in = ["string_literal", "block_comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string"] }, + { start = "/*", end = " */", close = true, newline = true, not_in = ["string", "comment"] }, ] collapsed_placeholder = " /* ... */ " -documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } +block_comment = { start = "/*", prefix="", end = "*/", tab_size = 2 } +documentation_comment = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 } prettier_parser_name = "java" prettier_plugins = ["prettier-plugin-java"] -debuggers = ["Java"] \ No newline at end of file +debuggers = ["Java"] diff --git a/languages/java/overrides.scm b/languages/java/overrides.scm new file mode 100644 index 0000000..af73171 --- /dev/null +++ b/languages/java/overrides.scm @@ -0,0 +1,9 @@ +[ + (block_comment) + (line_comment) +] @comment.inclusive + +[ + (character_literal) + (string_literal) +] @string From d16220eda249053b064706e3bdb276cbefe687f2 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 28 Aug 2025 02:14:50 +0300 Subject: [PATCH 20/93] Allow extension work even if maven is down (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maven search has been down for 2 days straight :man_shrugging: I made the debug plugin optional in case the user hasn’t downloaded the plugin and Maven doesn’t respond. Also replaced the logic for getting the LTS version from the Maven search API with parsing the maven-metadata.xml file. --- src/debugger.rs | 31 ++++++++++++++++++------------- src/lib.rs | 15 +++++++++++++-- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 8f778ed..6db9c9a 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -55,8 +55,7 @@ const SCOPES: [&str; 3] = [TEST_SCOPE, AUTO_SCOPE, RUNTIME_SCOPE]; const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; -const MAVEN_SEARCH_URL: &str = - "https://search.maven.org/solrsearch/select?q=a:com.microsoft.java.debug.plugin"; +const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/java/com.microsoft.java.debug.plugin/maven-metadata.xml"; pub struct Debugger { lsp: LspWrapper, @@ -71,6 +70,10 @@ impl Debugger { } } + pub fn loaded(&self) -> bool { + self.plugin_path.is_some() + } + pub fn get_or_download( &mut self, language_server_id: &LanguageServerId, @@ -91,7 +94,7 @@ impl Debugger { let res = fetch( &HttpRequest::builder() .method(HttpMethod::Get) - .url(MAVEN_SEARCH_URL) + .url(MAVEN_METADATA_URL) .build()?, ); @@ -131,18 +134,20 @@ impl Debugger { } } - let maven_response_body = serde_json::from_slice::(&res?.body) - .map_err(|err| format!("failed to deserialize Maven response: {err}"))?; + let xml = String::from_utf8(res?.body).map_err(|err| { + format!("could not get string from maven metadata response body: {err}") + })?; + + let start_tag = ""; + let end_tag = ""; - let latest_version = maven_response_body - .pointer("/response/docs/0/latestVersion") - .and_then(|v| v.as_str()) - .ok_or("Malformed maven response")?; + let latest_version = xml + .split_once(start_tag) + .and_then(|(_, rest)| rest.split_once(end_tag)) + .map(|(content, _)| content.trim()) + .ok_or(format!("Failed to parse maven-metadata.xml response {xml}"))?; - let artifact = maven_response_body - .pointer("/response/docs/0/a") - .and_then(|v| v.as_str()) - .ok_or("Malformed maven response")?; + let artifact = "com.microsoft.java.debug.plugin"; let jar_name = format!("{artifact}-{latest_version}.jar"); let jar_path = PathBuf::from(prefix).join(&jar_name); diff --git a/src/lib.rs b/src/lib.rs index 0e51ce3..5cd98ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -327,6 +327,10 @@ impl Extension for Java { _user_provided_debug_adapter_path: Option, worktree: &Worktree, ) -> zed_extension_api::Result { + if !self.debugger().is_ok_and(|v| v.loaded()) { + return Err("Debugger plugin is not loaded".to_string()); + } + if adapter_name != DEBUG_ADAPTER_NAME { return Err(format!( "Cannot create binary for adapter \"{adapter_name}\"" @@ -383,6 +387,10 @@ impl Extension for Java { &mut self, config: zed::DebugConfig, ) -> zed::Result { + if !self.debugger().is_ok_and(|v| v.loaded()) { + return Err("Debugger plugin is not loaded".to_string()); + } + match config.request { zed::DebugRequest::Attach(attach) => { let debug_config = if let Some(process_id) = attach.process_id { @@ -480,7 +488,10 @@ impl Extension for Java { } // download debugger if not exists - self.debugger()?.get_or_download(language_server_id)?; + if let Err(err) = self.debugger()?.get_or_download(language_server_id) { + println!("Failed to download debugger: {err}"); + }; + self.lsp()?.switch_workspace(worktree.root_path())?; Ok(zed::Command { @@ -502,7 +513,7 @@ impl Extension for Java { let options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) .map(|lsp_settings| lsp_settings.initialization_options)?; - if self.integrations.is_some() { + if self.debugger().is_ok_and(|v| v.loaded()) { return Ok(Some(self.debugger()?.inject_plugin_into_options(options)?)); } From 9ca8918089f4ac0c18540e953c10b4fab11132ed Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Wed, 27 Aug 2025 16:16:21 -0700 Subject: [PATCH 21/93] Bump to v6.4.0 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4303faf..17f2763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.3.0" +version = "6.4.0" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index cb551b2..874005f 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.3.0" +version = "6.4.0" schema_version = 1 authors = [ "Valentine Briese ", From 447275cf2c4df1c65f58cdbcc3dfc8c5eaac31fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 26 Sep 2025 12:15:12 -0700 Subject: [PATCH 22/93] Bump extension api, remove windows path workaround (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ Don't merge until Zed 0.205.x is on stable ⚠️ See https://github.com/zed-industries/zed/pull/37811 This PR bumps the zed extension API to the latest version, which removes the need to work around a bug where `current_dir()` returned an invalid path on windows. --- Cargo.toml | 2 +- src/lib.rs | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17f2763..f2f304c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ crate-type = ["cdylib"] [dependencies] serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" -zed_extension_api = "0.6.0" +zed_extension_api = "0.7.0" diff --git a/src/lib.rs b/src/lib.rs index 5cd98ba..0f6880a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -430,13 +430,6 @@ impl Extension for Java { let mut current_dir = current_dir().map_err(|err| format!("could not get current dir: {err}"))?; - if current_platform().0 == Os::Windows { - current_dir = current_dir - .strip_prefix("/") - .map_err(|err| err.to_string())? - .to_path_buf(); - } - let configuration = self.language_server_workspace_configuration(language_server_id, worktree)?; let java_home = configuration.as_ref().and_then(|configuration| { From aa8b9b58a4a5db7210ef1f4263127c9a716157a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 26 Sep 2025 15:35:26 -0700 Subject: [PATCH 23/93] 6.4.1 --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2f304c..3ca7ae8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java" -version = "6.4.0" +version = "6.4.1" edition = "2024" [lib] diff --git a/extension.toml b/extension.toml index 874005f..26d5d74 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.4.0" +version = "6.4.1" schema_version = 1 authors = [ "Valentine Briese ", From d22ea4b0dbd217225d8827ee367c5fc7efb6c316 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 30 Sep 2025 11:49:19 -0400 Subject: [PATCH 24/93] Create LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From f51008cb5a927c97e7ee9af4ac4395be91e4fac2 Mon Sep 17 00:00:00 2001 From: Jakob Hjelm Date: Fri, 3 Oct 2025 21:10:07 +0200 Subject: [PATCH 25/93] Add bracket matching for angle brackets and text blocks, auto-surround for angle brackets (#85) - Add angle brackets (`<>`) and text blocks (`"""`) to brackets.scm to enable bracket matching. - Add angle brackets to config.toml to enable auto-surround (i.e. if you select text in the editor and then type `<`, the selected text will be wrapped in `<...>` instead of being replaced by a single `<`. --- languages/java/brackets.scm | 2 ++ languages/java/config.toml | 1 + 2 files changed, 3 insertions(+) diff --git a/languages/java/brackets.scm b/languages/java/brackets.scm index 444d70b..72ac07e 100644 --- a/languages/java/brackets.scm +++ b/languages/java/brackets.scm @@ -1,4 +1,6 @@ ("{" @open "}" @close) ("[" @open "]" @close) ("(" @open ")" @close) +("<" @open ">" @close) ("\"" @open "\"" @close) +("\"\"\"" @open "\"\"\"" @close) diff --git a/languages/java/config.toml b/languages/java/config.toml index 5104ab7..b777b70 100644 --- a/languages/java/config.toml +++ b/languages/java/config.toml @@ -7,6 +7,7 @@ brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = false }, + { start = "<", end = ">", close = true, newline = false }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["string"] }, { start = "/*", end = " */", close = true, newline = true, not_in = ["string", "comment"] }, From d6428c7d5fe3923d09e2b36dc8d4a3de1562aae5 Mon Sep 17 00:00:00 2001 From: Jakob Hjelm Date: Fri, 3 Oct 2025 21:13:49 +0200 Subject: [PATCH 26/93] Fix EINVAL error when spawning the JDT LS process in Windows (#84) Fixes #81 The Node `child_process.spawn` call to start JDTLS currently fails on Windows since .bat or .cmd files require a shell to run ([official docs](https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows)). This PR amends that by passing `shell: true` on Windows systems. --- src/proxy.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy.mjs b/src/proxy.mjs index 686eab1..d345fc6 100644 --- a/src/proxy.mjs +++ b/src/proxy.mjs @@ -28,7 +28,7 @@ const args = process.argv.slice(3); const PROXY_ID = Buffer.from(process.cwd().replace(/\/+$/, "")).toString("hex"); const PROXY_HTTP_PORT_FILE = join(workdir, "proxy", PROXY_ID); -const lsp = spawn(bin, args); +const lsp = spawn(bin, args, { shell: process.platform === 'win32' }); const proxy = createLspProxy({ server: lsp, proxy: process }); proxy.on("client", (data, passthrough) => { From 6b56755541f781be2023f376ca117285b0453817 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 3 Oct 2025 21:19:42 +0200 Subject: [PATCH 27/93] Remove another windows path workaround (#87) --- src/debugger.rs | 13 +++---------- src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 6db9c9a..a38979b 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env::current_dir, fs, path::PathBuf}; +use std::{collections::HashMap, env, fs, path::PathBuf}; use serde::{Deserialize, Serialize}; use zed_extension_api::{ @@ -299,15 +299,8 @@ impl Debugger { &self, initialization_options: Option, ) -> zed::Result { - let mut current_dir = - current_dir().map_err(|err| format!("could not get current dir: {err}"))?; - - if current_platform().0 == Os::Windows { - current_dir = current_dir - .strip_prefix("/") - .map_err(|err| err.to_string())? - .to_path_buf(); - } + let current_dir = + env::current_dir().map_err(|err| format!("could not get current dir: {err}"))?; let canonical_path = Value::String( current_dir diff --git a/src/lib.rs b/src/lib.rs index 0f6880a..5b071f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ mod debugger; mod lsp; use std::{ collections::BTreeSet, - env::current_dir, + env, fs::{self, create_dir}, path::{Path, PathBuf}, str::FromStr, @@ -427,8 +427,8 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result { - let mut current_dir = - current_dir().map_err(|err| format!("could not get current dir: {err}"))?; + let current_dir = + env::current_dir().map_err(|err| format!("could not get current dir: {err}"))?; let configuration = self.language_server_workspace_configuration(language_server_id, worktree)?; From db26915b3fd94e3684ed7998a5b6cd4ef23e4f11 Mon Sep 17 00:00:00 2001 From: lxgr-linux Date: Fri, 3 Oct 2025 21:29:23 +0200 Subject: [PATCH 28/93] Add runnables queries (#79) This PR adds run options to run main classes and tests via the IDE. ~One problem I wasnt able to overcome was me not being able to dynamically decide whether to use maven or gradle to run.~ ~An option would be to ship a script that, proxys maven and gradle and decides which one to use. But i don't know how elegant that would be. The Solution rn is just to show both options always.~ --- languages/java/runnables.scm | 86 ++++++++++++++++++++++++++++++++++++ languages/java/tasks.json | 29 ++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 languages/java/runnables.scm create mode 100644 languages/java/tasks.json diff --git a/languages/java/runnables.scm b/languages/java/runnables.scm new file mode 100644 index 0000000..d03b5dc --- /dev/null +++ b/languages/java/runnables.scm @@ -0,0 +1,86 @@ +; Run the main function +( + (package_declaration + (scoped_identifier) @java_package_name + ) + (class_declaration + (modifiers) @class-modifier + (#match? @class-modifier "public") + name: (identifier) @java_class_name + body: (class_body + (method_declaration + (modifiers) @modifier + name: (identifier) @run + (#eq? @run "main") + (#match? @modifier "public") + (#match? @modifier "static") + ) + ) + ) @_ + (#set! tag java-main) +) + +; Run the main class +( + (package_declaration + (scoped_identifier) @java_package_name + ) + (class_declaration + (modifiers) @class-modifier + (#match? @class-modifier "public") + name: (identifier) @java_class_name @run + body: (class_body + (method_declaration + (modifiers) @modifier + name: (identifier) @method_name + (#eq? @method_name "main") + (#match? @modifier "public") + (#match? @modifier "static") + ) + ) + ) @_ + (#set! tag java-main) +) + +; Run test function +( + (package_declaration + (scoped_identifier) @java_package_name + ) + (class_declaration + name: (identifier) @java_class_name + body: (class_body + (method_declaration + (modifiers + (marker_annotation + name: (identifier) @annotation_name + ) + ) + name: (identifier) @run @java_method_name + (#eq? @annotation_name "Test") + ) + ) + ) @_ + (#set! tag java-test-method) +) + +; Run test class +( + (package_declaration + (scoped_identifier) @java_package_name + ) + (class_declaration + name: (identifier) @java_class_name @run + body: (class_body + (method_declaration + (modifiers + (marker_annotation + name: (identifier) @annotation_name + ) + ) + (#eq? @annotation_name "Test") + ) + ) + ) @_ + (#set! tag java-test-class) +) diff --git a/languages/java/tasks.json b/languages/java/tasks.json new file mode 100644 index 0000000..c42cae2 --- /dev/null +++ b/languages/java/tasks.json @@ -0,0 +1,29 @@ +[ + { + "label": "Run $ZED_CUSTOM_java_class_name", + "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then mvn clean compile exec:java -Dexec.mainClass=$c; elif [ -f gradlew ]; then ./gradlew run -PmainClass=$c; else >&2 echo 'No build system found'; exit 1; fi;", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-main"] + }, + { + "label": "Test $ZED_CUSTOM_java_class_name.$ZED_CUSTOM_java_method_name", + "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; m=\"$ZED_CUSTOM_java_method_name\"; if [ -f pom.xml ]; then mvn clean test -Dtest=\"$c#$m\"; elif [ -f gradlew ]; then ./gradlew test --tests $c.$m; else >&2 echo 'No build system found'; exit 1; fi;", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-test-method"] + }, + { + "label": "Test class $ZED_CUSTOM_java_class_name", + "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then mvn clean test -Dtest=\"$c\"; elif [ -f gradlew ]; then ./gradlew test --tests $c; else >&2 echo 'No build system found'; exit 1; fi;", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-test-class"] + }, + { + "label": "Run tests", + "command": "if [ -f pom.xml ]; then mvn clean test; elif [ -f gradlew ]; then ./gradlew test; else >&2 echo 'No build system found'; exit 1; fi;", + "use_new_terminal": false, + "reveal": "always" + } +] From d51df67e34c5505ad09d53705d247cf50b335b92 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 3 Oct 2025 21:33:13 +0200 Subject: [PATCH 29/93] chore: Fix some build warnings ang problems (#88) This fixes some issues I noticed whilst building this. --- .cargo/config.toml | 2 -- src/debugger.rs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 6b509f5..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm32-wasip1" diff --git a/src/debugger.rs b/src/debugger.rs index a38979b..b6b3895 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -2,8 +2,8 @@ use std::{collections::HashMap, env, fs, path::PathBuf}; use serde::{Deserialize, Serialize}; use zed_extension_api::{ - self as zed, DownloadedFileType, LanguageServerId, LanguageServerInstallationStatus, Os, - TcpArgumentsTemplate, Worktree, current_platform, download_file, + self as zed, DownloadedFileType, LanguageServerId, LanguageServerInstallationStatus, + TcpArgumentsTemplate, Worktree, download_file, http_client::{HttpMethod, HttpRequest, fetch}, serde_json::{self, Value, json}, set_language_server_installation_status, From b9bd2bb3332f0ce216da6768748ff3671c90bcc1 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 3 Oct 2025 21:35:12 +0200 Subject: [PATCH 30/93] chore: Remove Cargo.lock from gitignore (#89) The lockfile should be tracked, hence re-adding it here. --- .gitignore | 1 - Cargo.lock | 805 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 805 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 4536eb6..fdf4353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ grammars/ target/ -Cargo.lock extension.wasm .DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1223dfe --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,805 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "java" +version = "6.4.1" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zed_extension_api" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] From 32b9aa6d3f9d3674b43dcb65440534ea10e607e3 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 3 Oct 2025 21:41:02 +0200 Subject: [PATCH 31/93] chore: Align structure more with other extensions (#90) This ensures the structure matches that of other crates within this organization. --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 5 ++++- src/{lib.rs => java.rs} | 0 3 files changed, 13 insertions(+), 10 deletions(-) rename src/{lib.rs => java.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 1223dfe..6bbd0ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,15 +323,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "java" -version = "6.4.1" -dependencies = [ - "serde", - "serde_json", - "zed_extension_api", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -750,6 +741,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "zed_java" +version = "6.4.1" +dependencies = [ + "serde", + "serde_json", + "zed_extension_api", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 3ca7ae8..62627da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,12 @@ [package] -name = "java" +name = "zed_java" version = "6.4.1" edition = "2024" +publish = false +license = "Apache-2.0" [lib] +path = "src/java.rs" crate-type = ["cdylib"] [dependencies] diff --git a/src/lib.rs b/src/java.rs similarity index 100% rename from src/lib.rs rename to src/java.rs From 04f9b6772b56775cd2b059d5ad9b44a0725e97ba Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 3 Oct 2025 21:44:50 +0200 Subject: [PATCH 32/93] Bump version to v6.5.0 (#91) This PR bump the version of the extension to 6.5.0. Includes: - https://github.com/zed-extensions/java/pull/79 - https://github.com/zed-extensions/java/pull/84 - https://github.com/zed-extensions/java/pull/85 - https://github.com/zed-extensions/java/pull/87 --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bbd0ba..f68f813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.4.1" +version = "6.5.0" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 62627da..d324cca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_java" -version = "6.4.1" +version = "6.5.0" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 26d5d74..3901cf3 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.4.1" +version = "6.5.0" schema_version = 1 authors = [ "Valentine Briese ", From 8c431b56fb47c7c517954eb9adce9f243fac2b62 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Sat, 11 Oct 2025 20:03:50 +0200 Subject: [PATCH 33/93] make runnable tasks shell agnostic (#93) force /bin/sh in case the users default shell is not a POSIX-shell (e.g. fish, nushell). This is a quick fix, it would be better to provide proper scripts somehow, and/or make them user-definable. Changed the scripts to use `./mvnw` instead of assuming a global `mvn` command in case of maven projects. Note the included tasks still do not work in windows --- languages/java/tasks.json | 40 +++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/languages/java/tasks.json b/languages/java/tasks.json index c42cae2..05e8cdd 100644 --- a/languages/java/tasks.json +++ b/languages/java/tasks.json @@ -1,29 +1,53 @@ [ { "label": "Run $ZED_CUSTOM_java_class_name", - "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then mvn clean compile exec:java -Dexec.mainClass=$c; elif [ -f gradlew ]; then ./gradlew run -PmainClass=$c; else >&2 echo 'No build system found'; exit 1; fi;", + "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then ./mvnw clean compile exec:java -Dexec.mainClass=$c; elif [ -f gradlew ]; then ./gradlew run -PmainClass=$c; else >&2 echo 'No build system found'; exit 1; fi;", "use_new_terminal": false, "reveal": "always", - "tags": ["java-main"] + "tags": ["java-main"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } }, { "label": "Test $ZED_CUSTOM_java_class_name.$ZED_CUSTOM_java_method_name", - "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; m=\"$ZED_CUSTOM_java_method_name\"; if [ -f pom.xml ]; then mvn clean test -Dtest=\"$c#$m\"; elif [ -f gradlew ]; then ./gradlew test --tests $c.$m; else >&2 echo 'No build system found'; exit 1; fi;", + "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; m=\"$ZED_CUSTOM_java_method_name\"; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$c#$m\"; elif [ -f gradlew ]; then ./gradlew test --tests $c.$m; else >&2 echo 'No build system found'; exit 1; fi;", "use_new_terminal": false, "reveal": "always", - "tags": ["java-test-method"] + "tags": ["java-test-method"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } }, { "label": "Test class $ZED_CUSTOM_java_class_name", - "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then mvn clean test -Dtest=\"$c\"; elif [ -f gradlew ]; then ./gradlew test --tests $c; else >&2 echo 'No build system found'; exit 1; fi;", + "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$c\"; elif [ -f gradlew ]; then ./gradlew test --tests $c; else >&2 echo 'No build system found'; exit 1; fi;", "use_new_terminal": false, "reveal": "always", - "tags": ["java-test-class"] + "tags": ["java-test-class"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } }, { "label": "Run tests", - "command": "if [ -f pom.xml ]; then mvn clean test; elif [ -f gradlew ]; then ./gradlew test; else >&2 echo 'No build system found'; exit 1; fi;", + "command": "if [ -f pom.xml ]; then ./mvnw clean test; elif [ -f gradlew ]; then ./gradlew test; else >&2 echo 'No build system found'; exit 1; fi;", "use_new_terminal": false, - "reveal": "always" + "reveal": "always", + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } } ] From dd1f7a5cf0bf523a073df87c30b041e13b62cbad Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Sat, 11 Oct 2025 20:09:08 +0200 Subject: [PATCH 34/93] add fallbacks for offline use (#95) Fixes #82 When we fail to download JDTLS, Lombok or Debugger but succeeded before, we now fall back to the version we have on disk. --- src/debugger.rs | 8 +- src/java.rs | 470 +++++++++++++++++++++++++++--------------------- 2 files changed, 272 insertions(+), 206 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index b6b3895..dcd4ace 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -104,10 +104,10 @@ impl Debugger { return Err(err.to_owned()); } - // If it's not a 5xx code, then return an error. - if !err.contains("status code 5") { - return Err(err.to_owned()); - } + println!( + "Could not fetch debugger: {}\nFalling back to local version.", + err + ); let exists = fs::read_dir(prefix) .ok() diff --git a/src/java.rs b/src/java.rs index 5b071f0..5d354db 100644 --- a/src/java.rs +++ b/src/java.rs @@ -26,6 +26,8 @@ use crate::{debugger::Debugger, lsp::LspWrapper}; const PROXY_FILE: &str = include_str!("proxy.mjs"); const DEBUG_ADAPTER_NAME: &str = "Java"; const PATH_TO_STR_ERROR: &str = "failed to convert path to string"; +const JDTLS_INSTALL_PATH: &str = "jdtls"; +const LOMBOK_INSTALL_PATH: &str = "lombok"; struct Java { cached_binary_path: Option, @@ -90,222 +92,296 @@ impl Java { &LanguageServerInstallationStatus::CheckingForUpdate, ); - // Yeah, this part's all pretty terrible... - // Note to self: make it good eventually - let downloads_html = String::from_utf8( - fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url("https://download.eclipse.org/jdtls/milestones/") - .build()?, - ) - .map_err(|err| format!("failed to get available versions: {err}"))? - .body, - ) - .map_err(|err| format!("could not get string from downloads page response body: {err}"))?; - let mut versions = BTreeSet::new(); - let mut number_buffer = String::new(); - let mut version_buffer: (Option, Option, Option) = (None, None, None); - - for char in downloads_html.chars() { - if char.is_numeric() { - number_buffer.push(char); - } else if char == '.' { - if version_buffer.0.is_none() && !number_buffer.is_empty() { - version_buffer.0 = Some( - number_buffer - .parse() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - ); - } else if version_buffer.1.is_none() && !number_buffer.is_empty() { - version_buffer.1 = Some( - number_buffer - .parse() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - ); + match try_to_fetch_and_install_latest_jdtls(binary_name, language_server_id) { + Ok(path) => { + self.cached_binary_path = Some(path.clone()); + Ok(path) + } + Err(e) => { + if let Some(local_version) = find_latest_local_jdtls(binary_name) { + self.cached_binary_path = Some(local_version.clone()); + Ok(local_version) } else { - version_buffer = (None, None, None); + Err(e) } + } + } + } - number_buffer.clear(); - } else { - if version_buffer.0.is_some() - && version_buffer.1.is_some() - && version_buffer.2.is_none() - { - versions.insert(( - version_buffer.0.ok_or("no major version number")?, - version_buffer.1.ok_or("no minor version number")?, - number_buffer - .parse::() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - )); + fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { + if let Some(path) = &self.cached_lombok_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); + } + + match try_to_fetch_and_install_latest_lombok(language_server_id) { + Ok(path) => { + self.cached_lombok_path = Some(path.clone()); + return Ok(path); + } + Err(e) => { + if let Some(local_version) = find_latest_local_lombok() { + self.cached_lombok_path = Some(local_version.clone()); + Ok(local_version) + } else { + Err(e) } + } + } + } +} - number_buffer.clear(); +fn try_to_fetch_and_install_latest_jdtls( + binary_name: &str, + language_server_id: &LanguageServerId, +) -> zed::Result { + // Yeah, this part's all pretty terrible... + // Note to self: make it good eventually + let downloads_html = String::from_utf8( + fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url("https://download.eclipse.org/jdtls/milestones/") + .build()?, + ) + .map_err(|err| format!("failed to get available versions: {err}"))? + .body, + ) + .map_err(|err| format!("could not get string from downloads page response body: {err}"))?; + let mut versions = BTreeSet::new(); + let mut number_buffer = String::new(); + let mut version_buffer: (Option, Option, Option) = (None, None, None); + + for char in downloads_html.chars() { + if char.is_numeric() { + number_buffer.push(char); + } else if char == '.' { + if version_buffer.0.is_none() && !number_buffer.is_empty() { + version_buffer.0 = Some( + number_buffer + .parse() + .map_err(|err| format!("could not parse number buffer: {err}"))?, + ); + } else if version_buffer.1.is_none() && !number_buffer.is_empty() { + version_buffer.1 = Some( + number_buffer + .parse() + .map_err(|err| format!("could not parse number buffer: {err}"))?, + ); + } else { version_buffer = (None, None, None); } + + number_buffer.clear(); + } else { + if version_buffer.0.is_some() + && version_buffer.1.is_some() + && version_buffer.2.is_none() + { + versions.insert(( + version_buffer.0.ok_or("no major version number")?, + version_buffer.1.ok_or("no minor version number")?, + number_buffer + .parse::() + .map_err(|err| format!("could not parse number buffer: {err}"))?, + )); + } + + number_buffer.clear(); + version_buffer = (None, None, None); } + } - let (major, minor, patch) = versions.last().ok_or("no available versions")?; - let latest_version = format!("{major}.{minor}.{patch}"); - let latest_version_build = String::from_utf8( - fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url(format!( - "https://download.eclipse.org/jdtls/milestones/{latest_version}/latest.txt" - )) - .build()?, - ) - .map_err(|err| format!("failed to get latest version's build: {err}"))? - .body, + let (major, minor, patch) = versions.last().ok_or("no available versions")?; + let latest_version = format!("{major}.{minor}.{patch}"); + let latest_version_build = String::from_utf8( + fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url(format!( + "https://download.eclipse.org/jdtls/milestones/{latest_version}/latest.txt" + )) + .build()?, ) - .map_err(|err| { - format!("attempt to get latest version's build resulted in a malformed response: {err}") - })?; - let latest_version_build = latest_version_build.trim_end(); - let prefix = PathBuf::from("jdtls"); - // Exclude ".tar.gz" - let build_directory = &latest_version_build[..latest_version_build.len() - 7]; - let build_path = prefix.join(build_directory); - let binary_path = build_path.join("bin").join(binary_name); - - // If latest version isn't installed, - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - download_file( - &format!( - "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", - ), - build_path.to_str().ok_or(PATH_TO_STR_ERROR)?, - DownloadedFileType::GzipTar, - )?; - make_file_executable(binary_path.to_str().ok_or(PATH_TO_STR_ERROR)?)?; - - // ...and delete other versions - - // This step is expected to fail sometimes, and since we don't know - // how to fix it yet, we just carry on so the user doesn't have to - // restart the language server. - match fs::read_dir(prefix) { - Ok(entries) => { - for entry in entries { - match entry { - Ok(entry) => { - if entry.file_name().to_str() != Some(build_directory) - && let Err(err) = fs::remove_dir_all(entry.path()) - { - println!("failed to remove directory entry: {err}"); - } + .map_err(|err| format!("failed to get latest version's build: {err}"))? + .body, + ) + .map_err(|err| { + format!("attempt to get latest version's build resulted in a malformed response: {err}") + })?; + let latest_version_build = latest_version_build.trim_end(); + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + // Exclude ".tar.gz" + let build_directory = &latest_version_build[..latest_version_build.len() - 7]; + let build_path = prefix.join(build_directory); + let binary_path = build_path.join("bin").join(binary_name); + + // If latest version isn't installed, + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { + // then download it... + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + download_file( + &format!( + "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", + ), + build_path.to_str().ok_or(PATH_TO_STR_ERROR)?, + DownloadedFileType::GzipTar, + )?; + make_file_executable(binary_path.to_str().ok_or(PATH_TO_STR_ERROR)?)?; + + // ...and delete other versions + + // This step is expected to fail sometimes, and since we don't know + // how to fix it yet, we just carry on so the user doesn't have to + // restart the language server. + match fs::read_dir(prefix) { + Ok(entries) => { + for entry in entries { + match entry { + Ok(entry) => { + if entry.file_name().to_str() != Some(build_directory) + && let Err(err) = fs::remove_dir_all(entry.path()) + { + println!("failed to remove directory entry: {err}"); } - Err(err) => println!("failed to load directory entry: {err}"), } + Err(err) => println!("failed to load directory entry: {err}"), } } - Err(err) => println!("failed to list prefix directory: {err}"), } + Err(err) => println!("failed to list prefix directory: {err}"), } - - // else use it - - self.cached_binary_path = Some(binary_path.clone()); - - Ok(binary_path) } - fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { - // Use cached path if exists + // else use it + Ok(binary_path) +} - if let Some(path) = &self.cached_lombok_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } +fn find_latest_local_jdtls(binary_name: &str) -> Option { + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + // walk the dir where we install jdtls + fs::read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + // get the most recently created subdirectory + .filter_map(|path| { + let created_time = fs::metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + // point at where the binary should be + .map(|(path, _)| path.join("bin").join(binary_name)) + }) + .ok() + .flatten() +} - // Check for latest version +fn try_to_fetch_and_install_latest_lombok( + language_server_id: &LanguageServerId, +) -> zed::Result { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + let tags_response_body = serde_json::from_slice::( + &fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url("https://api.github.com/repos/projectlombok/lombok/tags") + .build()?, + ) + .map_err(|err| format!("failed to fetch GitHub tags: {err}"))? + .body, + ) + .map_err(|err| format!("failed to deserialize GitHub tags response: {err}"))?; + let latest_version = &tags_response_body + .as_array() + .and_then(|tag| { + tag.first().and_then(|latest_tag| { + latest_tag + .get("name") + .and_then(|tag_name| tag_name.as_str()) + }) + }) + // Exclude 'v' at beginning + .ok_or("malformed GitHub tags response")?[1..]; + let prefix = LOMBOK_INSTALL_PATH; + let jar_name = format!("lombok-{latest_version}.jar"); + let jar_path = Path::new(prefix).join(&jar_name); + + // If latest version isn't installed, + if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { + // then download it... set_language_server_installation_status( language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, + &LanguageServerInstallationStatus::Downloading, ); - - let tags_response_body = serde_json::from_slice::( - &fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url("https://api.github.com/repos/projectlombok/lombok/tags") - .build()?, - ) - .map_err(|err| format!("failed to fetch GitHub tags: {err}"))? - .body, - ) - .map_err(|err| format!("failed to deserialize GitHub tags response: {err}"))?; - let latest_version = &tags_response_body - .as_array() - .and_then(|tag| { - tag.first().and_then(|latest_tag| { - latest_tag - .get("name") - .and_then(|tag_name| tag_name.as_str()) - }) - }) - // Exclude 'v' at beginning - .ok_or("malformed GitHub tags response")?[1..]; - let prefix = "lombok"; - let jar_name = format!("lombok-{latest_version}.jar"); - let jar_path = Path::new(prefix).join(&jar_name); - - // If latest version isn't installed, - if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - create_dir(prefix).map_err(|err| err.to_string())?; - download_file( - &format!("https://projectlombok.org/downloads/{jar_name}"), - jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, - DownloadedFileType::Uncompressed, - )?; - - // ...and delete other versions - - // This step is expected to fail sometimes, and since we don't know - // how to fix it yet, we just carry on so the user doesn't have to - // restart the language server. - match fs::read_dir(prefix) { - Ok(entries) => { - for entry in entries { - match entry { - Ok(entry) => { - if entry.file_name().to_str() != Some(&jar_name) - && let Err(err) = fs::remove_dir_all(entry.path()) - { - println!("failed to remove directory entry: {err}"); - } + create_dir(prefix).map_err(|err| err.to_string())?; + download_file( + &format!("https://projectlombok.org/downloads/{jar_name}"), + jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, + DownloadedFileType::Uncompressed, + )?; + + // ...and delete other versions + + // This step is expected to fail sometimes, and since we don't know + // how to fix it yet, we just carry on so the user doesn't have to + // restart the language server. + match fs::read_dir(prefix) { + Ok(entries) => { + for entry in entries { + match entry { + Ok(entry) => { + if entry.file_name().to_str() != Some(&jar_name) + && let Err(err) = fs::remove_dir_all(entry.path()) + { + println!("failed to remove directory entry: {err}"); } - Err(err) => println!("failed to load directory entry: {err}"), } + Err(err) => println!("failed to load directory entry: {err}"), } } - Err(err) => println!("failed to list prefix directory: {err}"), } + Err(err) => println!("failed to list prefix directory: {err}"), } + } - // else use it - - self.cached_lombok_path = Some(jar_path.clone()); + // else use it + Ok(jar_path) +} - Ok(jar_path) - } +fn find_latest_local_lombok() -> Option { + let prefix = PathBuf::from(LOMBOK_INSTALL_PATH); + // walk the dir where we install lombok + fs::read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + // get the most recently created jar file + .filter(|path| { + path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("jar") + }) + .filter_map(|path| { + let created_time = fs::metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + .map(|(path, _)| path) + }) + .ok() + .flatten() } impl Extension for Java { @@ -518,26 +594,16 @@ impl Extension for Java { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> zed::Result> { - // FIXME(Valentine Briese): I don't really like that we have a variable - // here, there're probably some `Result` and/or - // `Option` methods that would eliminate the - // need for this, but at least this is easy to - // read. - - let mut settings = LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.settings); - - if !matches!(settings, Ok(Some(_))) { - settings = self - .language_server_initialization_options(language_server_id, worktree) - .map(|initialization_options| { - initialization_options.and_then(|initialization_options| { - initialization_options.get("settings").cloned() - }) + if let Ok(Some(settings)) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) + { + Ok(Some(settings)) + } else { + self.language_server_initialization_options(language_server_id, worktree) + .map(|init_options| { + init_options.and_then(|init_options| init_options.get("settings").cloned()) }) } - - settings } fn label_for_completion( From 7202df4529aced25c0f5017f8dcd4e0236af031f Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Sat, 18 Oct 2025 12:07:41 +0200 Subject: [PATCH 35/93] Remove python launch script dependency (#97) - For the JDTLS that the extension downloads and manages, we now also include the launch script in the extension instead of using the one provided by jdtls (which needs python). - For users with `jdtls(.bat)` on their $PATH, it's still used as before. Apart from making it easy to use the extension for people who can't or don't want to have python, this also allows us to manage the cache in the future (clearing caches is sadly a common interaction with java language servers) --- Cargo.lock | 124 +++++++++++++++++++++ Cargo.toml | 4 + extension.toml | 5 + src/java.rs | 287 +++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 386 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f68f813..e300cfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.99" @@ -32,12 +41,30 @@ version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -47,6 +74,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -178,6 +225,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -193,6 +250,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "icu_collections" version = "2.0.0" @@ -329,6 +392,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + [[package]] name = "litemap" version = "0.8.0" @@ -417,6 +486,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298" + [[package]] name = "ryu" version = "1.0.20" @@ -464,6 +562,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "slab" version = "0.4.11" @@ -529,6 +638,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -559,6 +674,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasm-encoder" version = "0.227.1" @@ -745,8 +866,11 @@ dependencies = [ name = "zed_java" version = "6.5.0" dependencies = [ + "hex", + "regex", "serde", "serde_json", + "sha1", "zed_extension_api", ] diff --git a/Cargo.toml b/Cargo.toml index d324cca..c27e0e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ + [package] name = "zed_java" version = "6.5.0" @@ -10,6 +11,9 @@ path = "src/java.rs" crate-type = ["cdylib"] [dependencies] +hex = "0.4.3" +regex = "1.12.1" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" +sha1 = "0.10.6" zed_extension_api = "0.7.0" diff --git a/extension.toml b/extension.toml index 3901cf3..d9feca8 100644 --- a/extension.toml +++ b/extension.toml @@ -23,3 +23,8 @@ name = "Eclipse JDT Language Server" language = "Java" [debug_adapters.Java] + +[[capabilities]] +kind = "process:exec" +command = "*" +args = ["-version"] diff --git a/src/java.rs b/src/java.rs index 5d354db..b419ad5 100644 --- a/src/java.rs +++ b/src/java.rs @@ -8,6 +8,8 @@ use std::{ str::FromStr, }; +use regex::Regex; +use sha1::{Digest, Sha1}; use zed_extension_api::{ self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, DownloadedFileType, Extension, LanguageServerId, LanguageServerInstallationStatus, Os, @@ -15,7 +17,9 @@ use zed_extension_api::{ current_platform, download_file, http_client::{HttpMethod, HttpRequest, fetch}, lsp::{Completion, CompletionKind}, - make_file_executable, register_extension, + make_file_executable, + process::Command, + register_extension, serde_json::{self, Value, json}, set_language_server_installation_status, settings::LspSettings, @@ -36,7 +40,6 @@ struct Java { } impl Java { - #[allow(dead_code)] fn lsp(&mut self) -> zed::Result<&LspWrapper> { self.integrations .as_ref() @@ -73,18 +76,11 @@ impl Java { return Ok(path.clone()); } - // Use $PATH if binary is in it - - let (platform, _) = current_platform(); - let binary_name = match platform { + let binary_name = match current_platform().0 { Os::Windows => "jdtls.bat", _ => "jdtls", }; - if let Some(path_binary) = worktree.which(binary_name) { - return Ok(PathBuf::from(path_binary)); - } - // Check for latest version set_language_server_installation_status( @@ -98,7 +94,7 @@ impl Java { Ok(path) } Err(e) => { - if let Some(local_version) = find_latest_local_jdtls(binary_name) { + if let Some(local_version) = find_latest_local_jdtls() { self.cached_binary_path = Some(local_version.clone()); Ok(local_version) } else { @@ -258,11 +254,11 @@ fn try_to_fetch_and_install_latest_jdtls( } } - // else use it - Ok(binary_path) + // else return jdtls base path + Ok(build_path) } -fn find_latest_local_jdtls(binary_name: &str) -> Option { +fn find_latest_local_jdtls() -> Option { let prefix = PathBuf::from(JDTLS_INSTALL_PATH); // walk the dir where we install jdtls fs::read_dir(&prefix) @@ -277,8 +273,8 @@ fn find_latest_local_jdtls(binary_name: &str) -> Option { Some((path, created_time)) }) .max_by_key(|&(_, time)| time) - // point at where the binary should be - .map(|(path, _)| path.join("bin").join(binary_name)) + // and return it + .map(|(path, _)| path) }) .ok() .flatten() @@ -508,36 +504,24 @@ impl Extension for Java { let configuration = self.language_server_workspace_configuration(language_server_id, worktree)?; - let java_home = configuration.as_ref().and_then(|configuration| { - configuration - .pointer("/java/home") - .and_then(|java_home_value| { - java_home_value - .as_str() - .map(|java_home_str| java_home_str.to_string()) - }) - }); let mut env = Vec::new(); - if let Some(java_home) = java_home { + if let Some(java_home) = get_java_home(&configuration, worktree) { env.push(("JAVA_HOME".to_string(), java_home)); } + // our proxy takes workdir, bin, argv let mut args = vec![ "--input-type=module".to_string(), "-e".to_string(), PROXY_FILE.to_string(), - current_dir.to_str().ok_or(PATH_TO_STR_ERROR)?.to_string(), - current_dir - .join(self.language_server_binary_path(language_server_id, worktree)?) - .to_str() - .ok_or(PATH_TO_STR_ERROR)? - .to_string(), + path_to_string(current_dir.clone())?, ]; // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true let lombok_enabled = configuration + .as_ref() .and_then(|configuration| { configuration .pointer("/java/jdt/ls/lombokSupport/enabled") @@ -545,15 +529,32 @@ impl Extension for Java { }) .unwrap_or(false); - if lombok_enabled { + let lombok_jvm_arg = if lombok_enabled { let lombok_jar_path = self.lombok_jar_path(language_server_id)?; let canonical_lombok_jar_path = current_dir .join(lombok_jar_path) .to_str() .ok_or(PATH_TO_STR_ERROR)? .to_string(); + Some(format!("-javaagent:{canonical_lombok_jar_path}")) + } else { + None + }; - args.push(format!("--jvm-arg=-javaagent:{canonical_lombok_jar_path}")); + if let Some(launcher) = get_jdtls_launcher_from_path(worktree) { + // if the user has `jdtls(.bat)` on their PATH, we use that + args.push(launcher); + if let Some(lombok_jvm_arg) = lombok_jvm_arg { + args.push(format!("--jvm-arg={lombok_jvm_arg}")); + } + } else { + // otherwise we launch ourselves + args.extend(self.build_jdtls_launch_args( + &configuration, + language_server_id, + worktree, + lombok_jvm_arg.into_iter().collect(), + )?); } // download debugger if not exists @@ -730,4 +731,222 @@ impl Extension for Java { } } +impl Java { + fn build_jdtls_launch_args( + &mut self, + configuration: &Option, + language_server_id: &LanguageServerId, + worktree: &Worktree, + jvm_args: Vec, + ) -> zed::Result> { + if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) { + return Ok(vec![jdtls_launcher]); + } + + let java_executable = get_java_executable(configuration, worktree)?; + let java_major_version = get_java_major_version(&java_executable)?; + if java_major_version < 21 { + return Err("JDTLS requires at least Java 21. If you need to run a JVM < 21, you can specify a different one for JDTLS to use by specifying lsp.jdtls.settings.java.home in the settings".to_string()); + } + + let extension_workdir = env::current_dir().map_err(|_e| "Could not get current dir")?; + + let jdtls_base_path = + extension_workdir.join(self.language_server_binary_path(language_server_id, worktree)?); + + let shared_config_path = get_shared_config_path(&jdtls_base_path); + let jar_path = find_equinox_launcher(&jdtls_base_path)?; + let jdtls_data_path = get_jdtls_data_path(worktree)?; + + let mut args = vec![ + get_java_executable(configuration, worktree).and_then(path_to_string)?, + "-Declipse.application=org.eclipse.jdt.ls.core.id1".to_string(), + "-Dosgi.bundles.defaultStartLevel=4".to_string(), + "-Declipse.product=org.eclipse.jdt.ls.core.product".to_string(), + "-Dosgi.checkConfiguration=true".to_string(), + format!( + "-Dosgi.sharedConfiguration.area={}", + path_to_string(shared_config_path)? + ), + "-Dosgi.sharedConfiguration.area.readOnly=true".to_string(), + "-Dosgi.configuration.cascaded=true".to_string(), + "-Xms1G".to_string(), + "--add-modules=ALL-SYSTEM".to_string(), + "--add-opens".to_string(), + "java.base/java.util=ALL-UNNAMED".to_string(), + "--add-opens".to_string(), + "java.base/java.lang=ALL-UNNAMED".to_string(), + ]; + args.extend(jvm_args); + args.extend(vec![ + "-jar".to_string(), + path_to_string(jar_path)?, + "-data".to_string(), + path_to_string(jdtls_data_path)?, + ]); + if java_major_version >= 24 { + args.push("-Djdk.xml.maxGeneralEntitySizeLimit=0".to_string()); + args.push("-Djdk.xml.totalEntitySizeLimit=0".to_string()); + } + Ok(args) + } +} + +fn path_to_string(path: PathBuf) -> zed::Result { + path.into_os_string() + .into_string() + .map_err(|_| PATH_TO_STR_ERROR.to_string()) +} + +fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { + // Note: the JDTLS data path is where JDTLS stores its own caches. + // In the unlikely event we can't find the canonical OS-Level cache-path, + // we fall back to the the extension's workdir, which may never get cleaned up. + // In future we may want to deliberately manage caches to be able to force-clean them. + + let mut env_iter = worktree.shell_env().into_iter(); + let base_cachedir = match current_platform().0 { + Os::Mac => env_iter + .find(|(k, _)| k == "HOME") + .map(|(_, v)| PathBuf::from(v).join("Library").join("Caches")), + Os::Linux => env_iter + .find(|(k, _)| k == "HOME") + .map(|(_, v)| PathBuf::from(v).join(".cache")), + Os::Windows => env_iter + .find(|(k, _)| k == "APPDATA") + .map(|(_, v)| PathBuf::from(v)), + } + .unwrap_or_else(|| { + env::current_dir() + .expect("should be able to get extension workdir") + .join("caches") + }); + + // caches are unique per worktree-root-path + let cache_key = worktree.root_path(); + + let hex_digest = get_sha1_hex(&cache_key); + let unique_dir_name = format!("jdtls-{}", hex_digest); + Ok(base_cachedir.join(unique_dir_name)) +} + +fn get_sha1_hex(input: &str) -> String { + let mut hasher = Sha1::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) +} + +fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { + let jdtls_executable_filename = match current_platform().0 { + Os::Windows => "jdtls.bat", + _ => "jdtls", + }; + + worktree.which(jdtls_executable_filename) +} + +fn get_java_executable(configuration: &Option, worktree: &Worktree) -> zed::Result { + let java_executable_filename = match current_platform().0 { + Os::Windows => "java.exe", + _ => "java", + }; + + // Get executable from $JAVA_HOME + if let Some(java_home) = get_java_home(configuration, worktree) { + let java_executable = PathBuf::from(java_home) + .join("bin") + .join(java_executable_filename); + if fs::metadata(&java_executable).is_ok_and(|stat| stat.is_file()) { + return Ok(java_executable); + } + } + // If we can't, try to get it from $PATH + worktree + .which(java_executable_filename) + .map(PathBuf::from) + .ok_or_else(|| "Could not find Java executable in JAVA_HOME or on PATH".to_string()) +} + +fn get_java_home(configuration: &Option, worktree: &Worktree) -> Option { + // try to read the value from settings + if let Some(configuration) = configuration { + if let Some(java_home) = configuration + .pointer("/java/home") + .and_then(|java_home_value| java_home_value.as_str()) + { + return Some(java_home.to_string()); + } + } + + // try to read the value from env + match worktree + .shell_env() + .into_iter() + .find(|(k, _)| k == "JAVA_HOME") + { + Some((_, value)) if !value.is_empty() => Some(value), + _ => None, + } +} + +fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { + let program = java_executable + .to_str() + .ok_or_else(|| "Could not convert Java executable path to string".to_string())?; + let output_bytes = Command::new(program).arg("-version").output()?.stderr; + let output = String::from_utf8(output_bytes).map_err(|e| e.to_string())?; + + let major_version_regex = + Regex::new(r#"version\s"(?P\d+)(\.\d+\.\d+(_\d+)?)?"#).map_err(|e| e.to_string())?; + let major_version = major_version_regex + .captures_iter(&output) + .find_map(|c| c.name("major").and_then(|m| m.as_str().parse::().ok())); + + if let Some(major_version) = major_version { + Ok(major_version) + } else { + Err("Could not determine Java major version".to_string()) + } +} + +fn find_equinox_launcher(jdtls_base_directory: &PathBuf) -> Result { + let plugins_dir = jdtls_base_directory.join("plugins"); + + // if we have `org.eclipse.equinox.launcher.jar` use that + let specific_launcher = plugins_dir.join("org.eclipse.equinox.launcher.jar"); + if specific_launcher.is_file() { + return Ok(specific_launcher); + } + + // else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar' + let entries = fs::read_dir(&plugins_dir) + .map_err(|e| format!("Failed to read plugins directory: {}", e))?; + + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .find(|path| { + path.is_file() + && path + .file_name() + .and_then(|s| s.to_str()) + .map_or(false, |s| { + s.starts_with("org.eclipse.equinox.launcher_") && s.ends_with(".jar") + }) + }) + .ok_or_else(|| "Cannot find equinox launcher".to_string()) +} + +fn get_shared_config_path(jdtls_base_directory: &PathBuf) -> PathBuf { + // Note: JDTLS also provides config_linux_arm and config_mac_arm (and others), + // but does not use them in their own launch script. It may be worth investigating if we should use them when appropriate. + let config_to_use = match current_platform().0 { + Os::Linux => "config_linux", + Os::Mac => "config_mac", + Os::Windows => "config_win", + }; + jdtls_base_directory.join(config_to_use) +} + register_extension!(Java); From b65fa1033afe87048eae203484b25353d5c97978 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:00:32 +0200 Subject: [PATCH 36/93] do not check for java executable file in JAVA_HOME (#100) We can not see files outside of our workdir, so this check would always return file not found --- src/java.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/java.rs b/src/java.rs index b419ad5..7889da0 100644 --- a/src/java.rs +++ b/src/java.rs @@ -857,9 +857,7 @@ fn get_java_executable(configuration: &Option, worktree: &Worktree) -> ze let java_executable = PathBuf::from(java_home) .join("bin") .join(java_executable_filename); - if fs::metadata(&java_executable).is_ok_and(|stat| stat.is_file()) { - return Ok(java_executable); - } + return Ok(java_executable); } // If we can't, try to get it from $PATH worktree From 2f5cd531e611eaa2d4b89f4d6e89e2f6bbbc91db Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:25:05 +0200 Subject: [PATCH 37/93] Wrap binary in parentheses when launching through a shell (#106) Windows users with spaces in their `java_home` have problems launching jdtls, as the proxy launches through a shell. This should mitigate that by wrapping the binary path in quotes on windows. --- src/proxy.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/proxy.mjs b/src/proxy.mjs index d345fc6..f2c0250 100644 --- a/src/proxy.mjs +++ b/src/proxy.mjs @@ -27,8 +27,9 @@ const args = process.argv.slice(3); const PROXY_ID = Buffer.from(process.cwd().replace(/\/+$/, "")).toString("hex"); const PROXY_HTTP_PORT_FILE = join(workdir, "proxy", PROXY_ID); +const command = process.platform === "win32" ? `"${bin}"` : bin; -const lsp = spawn(bin, args, { shell: process.platform === 'win32' }); +const lsp = spawn(command, args, { shell: process.platform === "win32" }); const proxy = createLspProxy({ server: lsp, proxy: process }); proxy.on("client", (data, passthrough) => { From 874448976969678aee5653308dcd4e4dc4edb9ac Mon Sep 17 00:00:00 2001 From: 0x41*32 <31595285+x4132@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:39:05 -0700 Subject: [PATCH 38/93] Don't autoclose angle brackets (#105) Previously whenever a condition such as < was typed the corresponding closing bracket would also be autocompleted --- languages/java/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages/java/config.toml b/languages/java/config.toml index b777b70..589524e 100644 --- a/languages/java/config.toml +++ b/languages/java/config.toml @@ -7,7 +7,7 @@ brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, { start = "(", end = ")", close = true, newline = false }, - { start = "<", end = ">", close = true, newline = false }, + { start = "<", end = ">", close = false, newline = false }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["string"] }, { start = "/*", end = " */", close = true, newline = true, not_in = ["string", "comment"] }, From 83802f49dc103c6a0e2bf519a2c0f15c05f93c53 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:29:45 +0100 Subject: [PATCH 39/93] Use our own fork of java-debug (#98) Closes #67 Zed and java-debug appear to disagree in their interpretations of the DAP-spec. Since we have no control over the release cycle of java-debug and indeed whether they accept the change to begin with, and the Zed team declines to adopt a more lenient stance in their interpretation of the spec, we must run our own fork of java-debug. I've created a fork with the required fixes and a release for us to use here: https://github.com/playdohface/java-debug This PR will make the extension download the forked release once if not installed yet, and then use it indefinitely. When the underlying issues are resolved either from Zeds side (https://github.com/zed-industries/zed/issues/37080) or java-debug merges our changes and makes a new release (https://github.com/microsoft/java-debug/pull/609) we can switch back our code to use the latest official release for java-debug. --- src/debugger.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/debugger.rs b/src/debugger.rs index dcd4ace..ae62014 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -55,6 +55,8 @@ const SCOPES: [&str; 3] = [TEST_SCOPE, AUTO_SCOPE, RUNTIME_SCOPE]; const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; +const JAVA_DEBUG_PLUGIN_FORK_URL: &str = "https://github.com/zed-industries/java-debug/releases/download/0.53.2/com.microsoft.java.debug.plugin-0.53.2.jar"; + const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/java/com.microsoft.java.debug.plugin/maven-metadata.xml"; pub struct Debugger { @@ -77,6 +79,50 @@ impl Debugger { pub fn get_or_download( &mut self, language_server_id: &LanguageServerId, + ) -> zed::Result { + // when the fix to https://github.com/microsoft/java-debug/issues/605 becomes part of an official release + // switch back to this: + // return self.get_or_download_latest_official(language_server_id); + self.get_or_download_fork(language_server_id) + } + + fn get_or_download_fork( + &mut self, + _language_server_id: &LanguageServerId, + ) -> zed::Result { + let prefix = "debugger"; + let artifact = "com.microsoft.java.debug.plugin"; + let latest_version = "0.53.2"; + let jar_name = format!("{artifact}-{latest_version}.jar"); + let jar_path = PathBuf::from(prefix).join(&jar_name); + + if let Some(path) = &self.plugin_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + && path.ends_with(jar_name) + { + return Ok(path.clone()); + } + + download_file( + JAVA_DEBUG_PLUGIN_FORK_URL, + jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, + DownloadedFileType::Uncompressed, + ) + .map_err(|err| { + format!( + "Failed to download java-debug fork from {}: {err}", + JAVA_DEBUG_PLUGIN_FORK_URL + ) + })?; + + self.plugin_path = Some(jar_path.clone()); + Ok(jar_path) + } + + #[allow(unused)] + fn get_or_download_latest_official( + &mut self, + language_server_id: &LanguageServerId, ) -> zed::Result { let prefix = "debugger"; From 046f384079f113c553683351daa22baa68e84d51 Mon Sep 17 00:00:00 2001 From: Alejandro Cardona <114955358+acardonna@users.noreply.github.com> Date: Sat, 1 Nov 2025 03:43:05 -0500 Subject: [PATCH 40/93] Add textobjects.scm for Java (#111) This adds a `textobjects.scm` file to enable Tree-sitter-based text objects for Java. Currently, Zed only supports this functionality in Vim mode. With this addition, you'll be able to treat several Java constructs as text objects and target them with any Vim operator like `d` (delete), `c` (change) or `y` (copy) or select them with `v`, as well as navigate code semantically. Here are the details: - Use `ic` / `ac` to work **i**nside or **a**round a `class`, `interface`, `enum`, `record` and `annotation` - Use `if` / `af` to work **i**nside or **a**round a `method`, `lambda` and `constructor` - Use `igc` / `agc` to work **i**nside or **a**round a `single-line comment`, `multi-line comment`and `javadoc comment` - Use `]m` / `[m` to go the next or previous `method` or `constructor` - Use `]]` / `[[`to go the next or previous class-like construct (a `class`, `interface`, etc.). - Use `]/` / `[/` to go to the next or previous `comment`. If you are interested in knowing more about these features, you can find more information in the official Zed documentation for Tree-sitter and for Text Objects. The Default Vim Bindings file in Zed is also a good resource. There are good built-in text objects like `i` for indentation, `b` for blocks or `[x` / `]x` for selecting greater or smaller syntax nodes. --- languages/java/textobjects.scm | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 languages/java/textobjects.scm diff --git a/languages/java/textobjects.scm b/languages/java/textobjects.scm new file mode 100644 index 0000000..c5cd229 --- /dev/null +++ b/languages/java/textobjects.scm @@ -0,0 +1,65 @@ +; methods +(method_declaration) @function.around +(method_declaration + body: (block + "{" (_)* @function.inside "}" + )) + +; constructors +(constructor_declaration) @function.around +(constructor_declaration + body: (constructor_body + "{" (_)* @function.inside "}" + )) + +; lambdas +(lambda_expression) @function.around +(lambda_expression + body: (block + "{" (_)* @function.inside "}" + )) +(lambda_expression + body: (_) @function.inside) + + +; classes +(class_declaration) @class.around +(class_declaration + body: (class_body + "{" (_)* @class.inside "}" + )) + +; interfaces +(interface_declaration) @class.around +(interface_declaration + body: (interface_body + "{" (_)* @class.inside "}" + )) + +; enums +(enum_declaration) @class.around +(enum_declaration + body: (enum_body + "{" + _* @class.inside + "}" + )) + +; records +(record_declaration) @class.around +(record_declaration + body: (class_body + "{" (_)* @class.inside "}" + )) + +; annotations +(annotation_type_declaration) @class.around +(annotation_type_declaration + (annotation_type_body + "{" (_)* @class.inside "}" + )) + + +; comments +((line_comment)+) @comment.around +(block_comment) @comment.around From 1991fc1eb9482f2da0eb187eaf411e5eb09f50fe Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sun, 2 Nov 2025 09:46:37 +0100 Subject: [PATCH 41/93] chore: code logic separation and clean up (#114) Improving the repo code readability and maintainability by: - Dividing the code into logic blocks. - Before, the bulk of the code was under `java.rs`. Now that code has been aggregated into specific files - Deduplicate code into reusable functions - General code that can be used by multiple files has been grouped into a single `util.rs` file Feature: - Add `~` expansion for Unix-like systems under `util.rs` --- src/config.rs | 37 ++++ src/debugger.rs | 16 +- src/java.rs | 545 +++--------------------------------------------- src/jdtls.rs | 388 ++++++++++++++++++++++++++++++++++ src/lsp.rs | 16 +- src/util.rs | 190 +++++++++++++++++ 6 files changed, 663 insertions(+), 529 deletions(-) create mode 100644 src/config.rs create mode 100644 src/jdtls.rs create mode 100644 src/util.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ecb0a02 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,37 @@ +use zed_extension_api::{Worktree, serde_json::Value}; + +use crate::util::expand_home_path; + +pub fn get_java_home(configuration: &Option, worktree: &Worktree) -> Option { + // try to read the value from settings + if let Some(configuration) = configuration + && let Some(java_home) = configuration.pointer("/java/home").and_then(|x| x.as_str()) { + match expand_home_path(worktree, java_home.to_string()) { + Ok(home_path) => return Some(home_path), + Err(err) => { + println!("{}", err); + } + }; + } + + // try to read the value from env + match worktree + .shell_env() + .into_iter() + .find(|(k, _)| k == "JAVA_HOME") + { + Some((_, value)) if !value.is_empty() => Some(value), + _ => None, + } +} + +pub fn is_lombok_enabled(configuration: &Option) -> bool { + configuration + .as_ref() + .and_then(|configuration| { + configuration + .pointer("/java/jdt/ls/lombokSupport/enabled") + .and_then(|enabled| enabled.as_bool()) + }) + .unwrap_or(false) +} diff --git a/src/debugger.rs b/src/debugger.rs index ae62014..5650152 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env, fs, path::PathBuf}; +use std::{collections::HashMap, fs, path::PathBuf}; use serde::{Deserialize, Serialize}; use zed_extension_api::{ @@ -9,7 +9,10 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::lsp::LspWrapper; +use crate::{ + lsp::LspWrapper, + util::{get_curr_dir, path_to_string}, +}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -53,8 +56,6 @@ const RUNTIME_SCOPE: &str = "$Runtime"; const SCOPES: [&str; 3] = [TEST_SCOPE, AUTO_SCOPE, RUNTIME_SCOPE]; -const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; - const JAVA_DEBUG_PLUGIN_FORK_URL: &str = "https://github.com/zed-industries/java-debug/releases/download/0.53.2/com.microsoft.java.debug.plugin-0.53.2.jar"; const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/java/com.microsoft.java.debug.plugin/maven-metadata.xml"; @@ -105,7 +106,7 @@ impl Debugger { download_file( JAVA_DEBUG_PLUGIN_FORK_URL, - jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, + &path_to_string(jar_path.clone())?, DownloadedFileType::Uncompressed, ) .map_err(|err| { @@ -215,7 +216,7 @@ impl Debugger { download_file( url.as_str(), - jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, + &path_to_string(&jar_path)?, DownloadedFileType::Uncompressed, ) .map_err(|err| format!("Failed to download {url} {err}"))?; @@ -345,8 +346,7 @@ impl Debugger { &self, initialization_options: Option, ) -> zed::Result { - let current_dir = - env::current_dir().map_err(|err| format!("could not get current dir: {err}"))?; + let current_dir = get_curr_dir()?; let canonical_path = Value::String( current_dir diff --git a/src/java.rs b/src/java.rs index 7889da0..7bc8b11 100644 --- a/src/java.rs +++ b/src/java.rs @@ -1,37 +1,42 @@ +mod config; mod debugger; +mod jdtls; mod lsp; +mod util; + use std::{ - collections::BTreeSet, env, - fs::{self, create_dir}, - path::{Path, PathBuf}, + fs::{self, metadata}, + path::PathBuf, str::FromStr, }; -use regex::Regex; -use sha1::{Digest, Sha1}; use zed_extension_api::{ - self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, - DownloadedFileType, Extension, LanguageServerId, LanguageServerInstallationStatus, Os, - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, Worktree, - current_platform, download_file, - http_client::{HttpMethod, HttpRequest, fetch}, + self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, Extension, + LanguageServerId, LanguageServerInstallationStatus, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, Worktree, lsp::{Completion, CompletionKind}, - make_file_executable, - process::Command, register_extension, - serde_json::{self, Value, json}, + serde_json::{Value, json}, set_language_server_installation_status, settings::LspSettings, }; -use crate::{debugger::Debugger, lsp::LspWrapper}; +use crate::{ + config::{get_java_home, is_lombok_enabled}, + debugger::Debugger, + jdtls::{ + build_jdtls_launch_args, find_latest_local_jdtls, find_latest_local_lombok, + get_jdtls_launcher_from_path, try_to_fetch_and_install_latest_jdtls, + try_to_fetch_and_install_latest_lombok, + }, + lsp::LspWrapper, + util::path_to_string, +}; const PROXY_FILE: &str = include_str!("proxy.mjs"); const DEBUG_ADAPTER_NAME: &str = "Java"; -const PATH_TO_STR_ERROR: &str = "failed to convert path to string"; -const JDTLS_INSTALL_PATH: &str = "jdtls"; -const LOMBOK_INSTALL_PATH: &str = "lombok"; +const LSP_INIT_ERROR: &str = "Lsp client is not initialized yet"; struct Java { cached_binary_path: Option, @@ -43,14 +48,14 @@ impl Java { fn lsp(&mut self) -> zed::Result<&LspWrapper> { self.integrations .as_ref() - .ok_or("Lsp client is not initialized yet".to_owned()) + .ok_or(LSP_INIT_ERROR.to_string()) .map(|v| &v.0) } fn debugger(&mut self) -> zed::Result<&mut Debugger> { self.integrations .as_mut() - .ok_or("Lsp client is not initialized yet".to_owned()) + .ok_or(LSP_INIT_ERROR.to_string()) .map(|v| &mut v.1) } @@ -71,24 +76,18 @@ impl Java { // Use cached path if exists if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + && metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } - let binary_name = match current_platform().0 { - Os::Windows => "jdtls.bat", - _ => "jdtls", - }; - // Check for latest version - set_language_server_installation_status( language_server_id, &LanguageServerInstallationStatus::CheckingForUpdate, ); - match try_to_fetch_and_install_latest_jdtls(binary_name, language_server_id) { + match try_to_fetch_and_install_latest_jdtls(language_server_id) { Ok(path) => { self.cached_binary_path = Some(path.clone()); Ok(path) @@ -114,7 +113,7 @@ impl Java { match try_to_fetch_and_install_latest_lombok(language_server_id) { Ok(path) => { self.cached_lombok_path = Some(path.clone()); - return Ok(path); + Ok(path) } Err(e) => { if let Some(local_version) = find_latest_local_lombok() { @@ -128,258 +127,6 @@ impl Java { } } -fn try_to_fetch_and_install_latest_jdtls( - binary_name: &str, - language_server_id: &LanguageServerId, -) -> zed::Result { - // Yeah, this part's all pretty terrible... - // Note to self: make it good eventually - let downloads_html = String::from_utf8( - fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url("https://download.eclipse.org/jdtls/milestones/") - .build()?, - ) - .map_err(|err| format!("failed to get available versions: {err}"))? - .body, - ) - .map_err(|err| format!("could not get string from downloads page response body: {err}"))?; - let mut versions = BTreeSet::new(); - let mut number_buffer = String::new(); - let mut version_buffer: (Option, Option, Option) = (None, None, None); - - for char in downloads_html.chars() { - if char.is_numeric() { - number_buffer.push(char); - } else if char == '.' { - if version_buffer.0.is_none() && !number_buffer.is_empty() { - version_buffer.0 = Some( - number_buffer - .parse() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - ); - } else if version_buffer.1.is_none() && !number_buffer.is_empty() { - version_buffer.1 = Some( - number_buffer - .parse() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - ); - } else { - version_buffer = (None, None, None); - } - - number_buffer.clear(); - } else { - if version_buffer.0.is_some() - && version_buffer.1.is_some() - && version_buffer.2.is_none() - { - versions.insert(( - version_buffer.0.ok_or("no major version number")?, - version_buffer.1.ok_or("no minor version number")?, - number_buffer - .parse::() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - )); - } - - number_buffer.clear(); - version_buffer = (None, None, None); - } - } - - let (major, minor, patch) = versions.last().ok_or("no available versions")?; - let latest_version = format!("{major}.{minor}.{patch}"); - let latest_version_build = String::from_utf8( - fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url(format!( - "https://download.eclipse.org/jdtls/milestones/{latest_version}/latest.txt" - )) - .build()?, - ) - .map_err(|err| format!("failed to get latest version's build: {err}"))? - .body, - ) - .map_err(|err| { - format!("attempt to get latest version's build resulted in a malformed response: {err}") - })?; - let latest_version_build = latest_version_build.trim_end(); - let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - // Exclude ".tar.gz" - let build_directory = &latest_version_build[..latest_version_build.len() - 7]; - let build_path = prefix.join(build_directory); - let binary_path = build_path.join("bin").join(binary_name); - - // If latest version isn't installed, - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - download_file( - &format!( - "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", - ), - build_path.to_str().ok_or(PATH_TO_STR_ERROR)?, - DownloadedFileType::GzipTar, - )?; - make_file_executable(binary_path.to_str().ok_or(PATH_TO_STR_ERROR)?)?; - - // ...and delete other versions - - // This step is expected to fail sometimes, and since we don't know - // how to fix it yet, we just carry on so the user doesn't have to - // restart the language server. - match fs::read_dir(prefix) { - Ok(entries) => { - for entry in entries { - match entry { - Ok(entry) => { - if entry.file_name().to_str() != Some(build_directory) - && let Err(err) = fs::remove_dir_all(entry.path()) - { - println!("failed to remove directory entry: {err}"); - } - } - Err(err) => println!("failed to load directory entry: {err}"), - } - } - } - Err(err) => println!("failed to list prefix directory: {err}"), - } - } - - // else return jdtls base path - Ok(build_path) -} - -fn find_latest_local_jdtls() -> Option { - let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - // walk the dir where we install jdtls - fs::read_dir(&prefix) - .map(|entries| { - entries - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - // get the most recently created subdirectory - .filter_map(|path| { - let created_time = fs::metadata(&path).and_then(|meta| meta.created()).ok()?; - Some((path, created_time)) - }) - .max_by_key(|&(_, time)| time) - // and return it - .map(|(path, _)| path) - }) - .ok() - .flatten() -} - -fn try_to_fetch_and_install_latest_lombok( - language_server_id: &LanguageServerId, -) -> zed::Result { - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - - let tags_response_body = serde_json::from_slice::( - &fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url("https://api.github.com/repos/projectlombok/lombok/tags") - .build()?, - ) - .map_err(|err| format!("failed to fetch GitHub tags: {err}"))? - .body, - ) - .map_err(|err| format!("failed to deserialize GitHub tags response: {err}"))?; - let latest_version = &tags_response_body - .as_array() - .and_then(|tag| { - tag.first().and_then(|latest_tag| { - latest_tag - .get("name") - .and_then(|tag_name| tag_name.as_str()) - }) - }) - // Exclude 'v' at beginning - .ok_or("malformed GitHub tags response")?[1..]; - let prefix = LOMBOK_INSTALL_PATH; - let jar_name = format!("lombok-{latest_version}.jar"); - let jar_path = Path::new(prefix).join(&jar_name); - - // If latest version isn't installed, - if !fs::metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - create_dir(prefix).map_err(|err| err.to_string())?; - download_file( - &format!("https://projectlombok.org/downloads/{jar_name}"), - jar_path.to_str().ok_or(PATH_TO_STR_ERROR)?, - DownloadedFileType::Uncompressed, - )?; - - // ...and delete other versions - - // This step is expected to fail sometimes, and since we don't know - // how to fix it yet, we just carry on so the user doesn't have to - // restart the language server. - match fs::read_dir(prefix) { - Ok(entries) => { - for entry in entries { - match entry { - Ok(entry) => { - if entry.file_name().to_str() != Some(&jar_name) - && let Err(err) = fs::remove_dir_all(entry.path()) - { - println!("failed to remove directory entry: {err}"); - } - } - Err(err) => println!("failed to load directory entry: {err}"), - } - } - } - Err(err) => println!("failed to list prefix directory: {err}"), - } - } - - // else use it - Ok(jar_path) -} - -fn find_latest_local_lombok() -> Option { - let prefix = PathBuf::from(LOMBOK_INSTALL_PATH); - // walk the dir where we install lombok - fs::read_dir(&prefix) - .map(|entries| { - entries - .filter_map(Result::ok) - .map(|entry| entry.path()) - // get the most recently created jar file - .filter(|path| { - path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("jar") - }) - .filter_map(|path| { - let created_time = fs::metadata(&path).and_then(|meta| meta.created()).ok()?; - Some((path, created_time)) - }) - .max_by_key(|&(_, time)| time) - .map(|(path, _)| path) - }) - .ok() - .flatten() -} - impl Extension for Java { fn new() -> Self where @@ -520,22 +267,10 @@ impl Extension for Java { ]; // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true - let lombok_enabled = configuration - .as_ref() - .and_then(|configuration| { - configuration - .pointer("/java/jdt/ls/lombokSupport/enabled") - .and_then(|enabled| enabled.as_bool()) - }) - .unwrap_or(false); - - let lombok_jvm_arg = if lombok_enabled { + let lombok_jvm_arg = if is_lombok_enabled(&configuration) { let lombok_jar_path = self.lombok_jar_path(language_server_id)?; - let canonical_lombok_jar_path = current_dir - .join(lombok_jar_path) - .to_str() - .ok_or(PATH_TO_STR_ERROR)? - .to_string(); + let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; + Some(format!("-javaagent:{canonical_lombok_jar_path}")) } else { None @@ -549,9 +284,9 @@ impl Extension for Java { } } else { // otherwise we launch ourselves - args.extend(self.build_jdtls_launch_args( + args.extend(build_jdtls_launch_args( + &self.language_server_binary_path(language_server_id, worktree)?, &configuration, - language_server_id, worktree, lombok_jvm_arg.into_iter().collect(), )?); @@ -731,220 +466,4 @@ impl Extension for Java { } } -impl Java { - fn build_jdtls_launch_args( - &mut self, - configuration: &Option, - language_server_id: &LanguageServerId, - worktree: &Worktree, - jvm_args: Vec, - ) -> zed::Result> { - if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) { - return Ok(vec![jdtls_launcher]); - } - - let java_executable = get_java_executable(configuration, worktree)?; - let java_major_version = get_java_major_version(&java_executable)?; - if java_major_version < 21 { - return Err("JDTLS requires at least Java 21. If you need to run a JVM < 21, you can specify a different one for JDTLS to use by specifying lsp.jdtls.settings.java.home in the settings".to_string()); - } - - let extension_workdir = env::current_dir().map_err(|_e| "Could not get current dir")?; - - let jdtls_base_path = - extension_workdir.join(self.language_server_binary_path(language_server_id, worktree)?); - - let shared_config_path = get_shared_config_path(&jdtls_base_path); - let jar_path = find_equinox_launcher(&jdtls_base_path)?; - let jdtls_data_path = get_jdtls_data_path(worktree)?; - - let mut args = vec![ - get_java_executable(configuration, worktree).and_then(path_to_string)?, - "-Declipse.application=org.eclipse.jdt.ls.core.id1".to_string(), - "-Dosgi.bundles.defaultStartLevel=4".to_string(), - "-Declipse.product=org.eclipse.jdt.ls.core.product".to_string(), - "-Dosgi.checkConfiguration=true".to_string(), - format!( - "-Dosgi.sharedConfiguration.area={}", - path_to_string(shared_config_path)? - ), - "-Dosgi.sharedConfiguration.area.readOnly=true".to_string(), - "-Dosgi.configuration.cascaded=true".to_string(), - "-Xms1G".to_string(), - "--add-modules=ALL-SYSTEM".to_string(), - "--add-opens".to_string(), - "java.base/java.util=ALL-UNNAMED".to_string(), - "--add-opens".to_string(), - "java.base/java.lang=ALL-UNNAMED".to_string(), - ]; - args.extend(jvm_args); - args.extend(vec![ - "-jar".to_string(), - path_to_string(jar_path)?, - "-data".to_string(), - path_to_string(jdtls_data_path)?, - ]); - if java_major_version >= 24 { - args.push("-Djdk.xml.maxGeneralEntitySizeLimit=0".to_string()); - args.push("-Djdk.xml.totalEntitySizeLimit=0".to_string()); - } - Ok(args) - } -} - -fn path_to_string(path: PathBuf) -> zed::Result { - path.into_os_string() - .into_string() - .map_err(|_| PATH_TO_STR_ERROR.to_string()) -} - -fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { - // Note: the JDTLS data path is where JDTLS stores its own caches. - // In the unlikely event we can't find the canonical OS-Level cache-path, - // we fall back to the the extension's workdir, which may never get cleaned up. - // In future we may want to deliberately manage caches to be able to force-clean them. - - let mut env_iter = worktree.shell_env().into_iter(); - let base_cachedir = match current_platform().0 { - Os::Mac => env_iter - .find(|(k, _)| k == "HOME") - .map(|(_, v)| PathBuf::from(v).join("Library").join("Caches")), - Os::Linux => env_iter - .find(|(k, _)| k == "HOME") - .map(|(_, v)| PathBuf::from(v).join(".cache")), - Os::Windows => env_iter - .find(|(k, _)| k == "APPDATA") - .map(|(_, v)| PathBuf::from(v)), - } - .unwrap_or_else(|| { - env::current_dir() - .expect("should be able to get extension workdir") - .join("caches") - }); - - // caches are unique per worktree-root-path - let cache_key = worktree.root_path(); - - let hex_digest = get_sha1_hex(&cache_key); - let unique_dir_name = format!("jdtls-{}", hex_digest); - Ok(base_cachedir.join(unique_dir_name)) -} - -fn get_sha1_hex(input: &str) -> String { - let mut hasher = Sha1::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - hex::encode(result) -} - -fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { - let jdtls_executable_filename = match current_platform().0 { - Os::Windows => "jdtls.bat", - _ => "jdtls", - }; - - worktree.which(jdtls_executable_filename) -} - -fn get_java_executable(configuration: &Option, worktree: &Worktree) -> zed::Result { - let java_executable_filename = match current_platform().0 { - Os::Windows => "java.exe", - _ => "java", - }; - - // Get executable from $JAVA_HOME - if let Some(java_home) = get_java_home(configuration, worktree) { - let java_executable = PathBuf::from(java_home) - .join("bin") - .join(java_executable_filename); - return Ok(java_executable); - } - // If we can't, try to get it from $PATH - worktree - .which(java_executable_filename) - .map(PathBuf::from) - .ok_or_else(|| "Could not find Java executable in JAVA_HOME or on PATH".to_string()) -} - -fn get_java_home(configuration: &Option, worktree: &Worktree) -> Option { - // try to read the value from settings - if let Some(configuration) = configuration { - if let Some(java_home) = configuration - .pointer("/java/home") - .and_then(|java_home_value| java_home_value.as_str()) - { - return Some(java_home.to_string()); - } - } - - // try to read the value from env - match worktree - .shell_env() - .into_iter() - .find(|(k, _)| k == "JAVA_HOME") - { - Some((_, value)) if !value.is_empty() => Some(value), - _ => None, - } -} - -fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { - let program = java_executable - .to_str() - .ok_or_else(|| "Could not convert Java executable path to string".to_string())?; - let output_bytes = Command::new(program).arg("-version").output()?.stderr; - let output = String::from_utf8(output_bytes).map_err(|e| e.to_string())?; - - let major_version_regex = - Regex::new(r#"version\s"(?P\d+)(\.\d+\.\d+(_\d+)?)?"#).map_err(|e| e.to_string())?; - let major_version = major_version_regex - .captures_iter(&output) - .find_map(|c| c.name("major").and_then(|m| m.as_str().parse::().ok())); - - if let Some(major_version) = major_version { - Ok(major_version) - } else { - Err("Could not determine Java major version".to_string()) - } -} - -fn find_equinox_launcher(jdtls_base_directory: &PathBuf) -> Result { - let plugins_dir = jdtls_base_directory.join("plugins"); - - // if we have `org.eclipse.equinox.launcher.jar` use that - let specific_launcher = plugins_dir.join("org.eclipse.equinox.launcher.jar"); - if specific_launcher.is_file() { - return Ok(specific_launcher); - } - - // else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar' - let entries = fs::read_dir(&plugins_dir) - .map_err(|e| format!("Failed to read plugins directory: {}", e))?; - - entries - .filter_map(Result::ok) - .map(|entry| entry.path()) - .find(|path| { - path.is_file() - && path - .file_name() - .and_then(|s| s.to_str()) - .map_or(false, |s| { - s.starts_with("org.eclipse.equinox.launcher_") && s.ends_with(".jar") - }) - }) - .ok_or_else(|| "Cannot find equinox launcher".to_string()) -} - -fn get_shared_config_path(jdtls_base_directory: &PathBuf) -> PathBuf { - // Note: JDTLS also provides config_linux_arm and config_mac_arm (and others), - // but does not use them in their own launch script. It may be worth investigating if we should use them when appropriate. - let config_to_use = match current_platform().0 { - Os::Linux => "config_linux", - Os::Mac => "config_mac", - Os::Windows => "config_win", - }; - jdtls_base_directory.join(config_to_use) -} - register_extension!(Java); diff --git a/src/jdtls.rs b/src/jdtls.rs new file mode 100644 index 0000000..ceccfa5 --- /dev/null +++ b/src/jdtls.rs @@ -0,0 +1,388 @@ +use std::{ + collections::BTreeSet, + env::current_dir, + fs::{create_dir, metadata, read_dir}, + path::{Path, PathBuf}, +}; + +use sha1::{Digest, Sha1}; +use zed_extension_api::{ + self as zed, DownloadedFileType, LanguageServerId, LanguageServerInstallationStatus, Os, + Worktree, current_platform, download_file, + http_client::{HttpMethod, HttpRequest, fetch}, + make_file_executable, + serde_json::Value, + set_language_server_installation_status, +}; + +use crate::util::{ + get_curr_dir, get_java_executable, get_java_major_version, path_to_string, + remove_all_files_except, +}; + +const JDTLS_INSTALL_PATH: &str = "jdtls"; +const LOMBOK_INSTALL_PATH: &str = "lombok"; + +// Errors + +const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java 21. If you need to run a JVM < 21, you can specify a different one for JDTLS to use by specifying lsp.jdtls.settings.java.home in the settings"; + +pub fn build_jdtls_launch_args( + jdtls_path: &PathBuf, + configuration: &Option, + worktree: &Worktree, + jvm_args: Vec, +) -> zed::Result> { + if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) { + return Ok(vec![jdtls_launcher]); + } + + let java_executable = get_java_executable(configuration, worktree)?; + let java_major_version = get_java_major_version(&java_executable)?; + if java_major_version < 21 { + return Err(JAVA_VERSION_ERROR.to_string()); + } + + let extension_workdir = get_curr_dir()?; + + let jdtls_base_path = extension_workdir.join(jdtls_path); + + let shared_config_path = get_shared_config_path(&jdtls_base_path); + let jar_path = find_equinox_launcher(&jdtls_base_path)?; + let jdtls_data_path = get_jdtls_data_path(worktree)?; + + let mut args = vec![ + path_to_string(java_executable)?, + "-Declipse.application=org.eclipse.jdt.ls.core.id1".to_string(), + "-Dosgi.bundles.defaultStartLevel=4".to_string(), + "-Declipse.product=org.eclipse.jdt.ls.core.product".to_string(), + "-Dosgi.checkConfiguration=true".to_string(), + format!( + "-Dosgi.sharedConfiguration.area={}", + path_to_string(shared_config_path)? + ), + "-Dosgi.sharedConfiguration.area.readOnly=true".to_string(), + "-Dosgi.configuration.cascaded=true".to_string(), + "-Xms1G".to_string(), + "--add-modules=ALL-SYSTEM".to_string(), + "--add-opens".to_string(), + "java.base/java.util=ALL-UNNAMED".to_string(), + "--add-opens".to_string(), + "java.base/java.lang=ALL-UNNAMED".to_string(), + ]; + args.extend(jvm_args); + args.extend(vec![ + "-jar".to_string(), + path_to_string(jar_path)?, + "-data".to_string(), + path_to_string(jdtls_data_path)?, + ]); + if java_major_version >= 24 { + args.push("-Djdk.xml.maxGeneralEntitySizeLimit=0".to_string()); + args.push("-Djdk.xml.totalEntitySizeLimit=0".to_string()); + } + Ok(args) +} + +pub fn find_latest_local_jdtls() -> Option { + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + // walk the dir where we install jdtls + read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + // get the most recently created subdirectory + .filter_map(|path| { + let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + // and return it + .map(|(path, _)| path) + }) + .ok() + .flatten() +} + +pub fn find_latest_local_lombok() -> Option { + let prefix = PathBuf::from(LOMBOK_INSTALL_PATH); + // walk the dir where we install lombok + read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + // get the most recently created jar file + .filter(|path| { + path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("jar") + }) + .filter_map(|path| { + let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + .map(|(path, _)| path) + }) + .ok() + .flatten() +} + +pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { + let jdtls_executable_filename = match current_platform().0 { + Os::Windows => "jdtls.bat", + _ => "jdtls", + }; + + worktree.which(jdtls_executable_filename) +} + +pub fn try_to_fetch_and_install_latest_jdtls( + language_server_id: &LanguageServerId, +) -> zed::Result { + // Yeah, this part's all pretty terrible... + // Note to self: make it good eventually + let downloads_html = String::from_utf8( + fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url("https://download.eclipse.org/jdtls/milestones/") + .build()?, + ) + .map_err(|err| format!("failed to get available versions: {err}"))? + .body, + ) + .map_err(|err| format!("could not get string from downloads page response body: {err}"))?; + let mut versions = BTreeSet::new(); + let mut number_buffer = String::new(); + let mut version_buffer: (Option, Option, Option) = (None, None, None); + + for char in downloads_html.chars() { + if char.is_numeric() { + number_buffer.push(char); + } else if char == '.' { + if version_buffer.0.is_none() && !number_buffer.is_empty() { + version_buffer.0 = Some( + number_buffer + .parse() + .map_err(|err| format!("could not parse number buffer: {err}"))?, + ); + } else if version_buffer.1.is_none() && !number_buffer.is_empty() { + version_buffer.1 = Some( + number_buffer + .parse() + .map_err(|err| format!("could not parse number buffer: {err}"))?, + ); + } else { + version_buffer = (None, None, None); + } + + number_buffer.clear(); + } else { + if version_buffer.0.is_some() + && version_buffer.1.is_some() + && version_buffer.2.is_none() + { + versions.insert(( + version_buffer.0.ok_or("no major version number")?, + version_buffer.1.ok_or("no minor version number")?, + number_buffer + .parse::() + .map_err(|err| format!("could not parse number buffer: {err}"))?, + )); + } + + number_buffer.clear(); + version_buffer = (None, None, None); + } + } + + let (major, minor, patch) = versions.last().ok_or("no available versions")?; + let latest_version = format!("{major}.{minor}.{patch}"); + let latest_version_build = String::from_utf8( + fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url(format!( + "https://download.eclipse.org/jdtls/milestones/{latest_version}/latest.txt" + )) + .build()?, + ) + .map_err(|err| format!("failed to get latest version's build: {err}"))? + .body, + ) + .map_err(|err| { + format!("attempt to get latest version's build resulted in a malformed response: {err}") + })?; + let latest_version_build = latest_version_build.trim_end(); + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + // Exclude ".tar.gz" + let build_directory = &latest_version_build[..latest_version_build.len() - 7]; + let build_path = prefix.join(build_directory); + let binary_path = build_path.join("bin").join(get_binary_name()); + + // If latest version isn't installed, + if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { + // then download it... + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + download_file( + &format!( + "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", + ), + path_to_string(build_path.clone())?.as_str(), + DownloadedFileType::GzipTar, + )?; + make_file_executable(path_to_string(binary_path)?.as_str())?; + + // ...and delete other versions + let _ = remove_all_files_except(prefix, build_directory); + } + + // return jdtls base path + Ok(build_path) +} + +pub fn try_to_fetch_and_install_latest_lombok( + language_server_id: &LanguageServerId, +) -> zed::Result { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + let tags_response_body = serde_json::from_slice::( + &fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url("https://api.github.com/repos/projectlombok/lombok/tags") + .build()?, + ) + .map_err(|err| format!("failed to fetch GitHub tags: {err}"))? + .body, + ) + .map_err(|err| format!("failed to deserialize GitHub tags response: {err}"))?; + let latest_version = &tags_response_body + .as_array() + .and_then(|tag| { + tag.first().and_then(|latest_tag| { + latest_tag + .get("name") + .and_then(|tag_name| tag_name.as_str()) + }) + }) + // Exclude 'v' at beginning + .ok_or("malformed GitHub tags response")?[1..]; + let prefix = LOMBOK_INSTALL_PATH; + let jar_name = format!("lombok-{latest_version}.jar"); + let jar_path = Path::new(prefix).join(&jar_name); + + // If latest version isn't installed, + if !metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { + // then download it... + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + create_dir(prefix).map_err(|err| err.to_string())?; + download_file( + &format!("https://projectlombok.org/downloads/{jar_name}"), + path_to_string(jar_path.clone())?.as_str(), + DownloadedFileType::Uncompressed, + )?; + + // ...and delete other versions + + let _ = remove_all_files_except(prefix, jar_name.as_str()); + } + + // else use it + Ok(jar_path) +} + +fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result { + let plugins_dir = jdtls_base_directory.join("plugins"); + + // if we have `org.eclipse.equinox.launcher.jar` use that + let specific_launcher = plugins_dir.join("org.eclipse.equinox.launcher.jar"); + if specific_launcher.is_file() { + return Ok(specific_launcher); + } + + // else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar' + let entries = + read_dir(&plugins_dir).map_err(|e| format!("Failed to read plugins directory: {}", e))?; + + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .find(|path| { + path.is_file() + && path.file_name().and_then(|s| s.to_str()).is_some_and(|s| { + s.starts_with("org.eclipse.equinox.launcher_") && s.ends_with(".jar") + }) + }) + .ok_or_else(|| "Cannot find equinox launcher".to_string()) +} + +fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { + // Note: the JDTLS data path is where JDTLS stores its own caches. + // In the unlikely event we can't find the canonical OS-Level cache-path, + // we fall back to the the extension's workdir, which may never get cleaned up. + // In future we may want to deliberately manage caches to be able to force-clean them. + + let mut env_iter = worktree.shell_env().into_iter(); + let base_cachedir = match current_platform().0 { + Os::Mac => env_iter + .find(|(k, _)| k == "HOME") + .map(|(_, v)| PathBuf::from(v).join("Library").join("Caches")), + Os::Linux => env_iter + .find(|(k, _)| k == "HOME") + .map(|(_, v)| PathBuf::from(v).join(".cache")), + Os::Windows => env_iter + .find(|(k, _)| k == "APPDATA") + .map(|(_, v)| PathBuf::from(v)), + } + .unwrap_or_else(|| { + current_dir() + .expect("should be able to get extension workdir") + .join("caches") + }); + + // caches are unique per worktree-root-path + let cache_key = worktree.root_path(); + + let hex_digest = get_sha1_hex(&cache_key); + let unique_dir_name = format!("jdtls-{}", hex_digest); + Ok(base_cachedir.join(unique_dir_name)) +} + +fn get_binary_name() -> &'static str { + match current_platform().0 { + Os::Windows => "jdtls.bat", + _ => "jdtls", + } +} + +fn get_sha1_hex(input: &str) -> String { + let mut hasher = Sha1::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) +} + +fn get_shared_config_path(jdtls_base_directory: &Path) -> PathBuf { + // Note: JDTLS also provides config_linux_arm and config_mac_arm (and others), + // but does not use them in their own launch script. It may be worth investigating if we should use them when appropriate. + let config_to_use = match current_platform().0 { + Os::Linux => "config_linux", + Os::Mac => "config_mac", + Os::Windows => "config_win", + }; + jdtls_base_directory.join(config_to_use) +} diff --git a/src/lsp.rs b/src/lsp.rs index c567483..73a78da 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -12,14 +12,14 @@ use zed_extension_api::{ serde_json::{self, Map, Value}, }; -/** - * `proxy.mjs` starts an HTTP server and writes its port to - * `${workdir}/proxy/${hex(project_root)}`. - * - * This allows us to send LSP requests directly from the Java extension. - * It’s a temporary workaround until `zed_extension_api` - * provides the ability to send LSP requests directly. -*/ +/// +/// `proxy.mjs` starts an HTTP server and writes its port to +/// `${workdir}/proxy/${hex(project_root)}`. +/// +/// This allows us to send LSP requests directly from the Java extension. +/// It’s a temporary workaround until `zed_extension_api` +/// provides the ability to send LSP requests directly. +/// pub struct LspClient { workspace: String, } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..2c44c2e --- /dev/null +++ b/src/util.rs @@ -0,0 +1,190 @@ +use regex::Regex; +use std::{ + env::current_dir, + fs, + path::{Path, PathBuf}, +}; +use zed_extension_api::{self as zed, Command, Os, Worktree, current_platform, serde_json::Value}; + +use crate::config::get_java_home; + +// Errors +const EXPAND_ERROR: &str = "Failed to expand ~"; +const CURR_DIR_ERROR: &str = "Could not get current dir"; +const DIR_ENTRY_LOAD_ERROR: &str = "Failed to load directory entry"; +const DIR_ENTRY_RM_ERROR: &str = "Failed to remove directory entry"; +const DIR_ENTRY_LS_ERROR: &str = "Failed to list prefix directory"; +const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; +const JAVA_EXEC_ERROR: &str = "Failed to convert Java executable path to string"; +const JAVA_VERSION_ERROR: &str = "Failed to determine Java major version"; +const JAVA_EXEC_NOT_FOUND_ERROR: &str = "Could not find Java executable in JAVA_HOME or on PATH"; + +/// Expand ~ on Unix-like systems +/// +/// # Arguments +/// +/// * [`worktree`] Zed extension worktree with access to ENV +/// * [`path`] path to expand +/// +/// # Returns +/// +/// On Unix-like systems ~ is replaced with the value stored in HOME +/// +/// On Windows systems [`path`] is returned untouched +pub fn expand_home_path(worktree: &Worktree, path: String) -> zed::Result { + match zed::current_platform() { + (Os::Windows, _) => Ok(path), + (_, _) => worktree + .shell_env() + .iter() + .find(|&(key, _)| key == "HOME") + .map_or_else( + || Err(EXPAND_ERROR.to_string()), + |(_, value)| Ok(path.replace("~", value)), + ), + } +} + +/// Get the extension current directory +/// +/// # Returns +/// +/// The [`PathBuf`] of the extension directory +/// +/// # Errors +/// +/// This functoin will return an error if it was not possible to retrieve the current directory +pub fn get_curr_dir() -> zed::Result { + current_dir().map_err(|_| CURR_DIR_ERROR.to_string()) +} + +/// Retrieve the path to a java exec either: +/// - defined by the user in `settings.json` +/// - from PATH +/// - from JAVA_HOME +/// +/// # Arguments +/// +/// * [`configuration`] a JSON object representing the user configuration +/// * [`worktree`] Zed extension worktree +/// +/// # Returns +/// +/// Returns the path to the java exec file +/// +/// # Errors +/// +/// This function will return an error if neither PATH or JAVA_HOME led +/// to a java exec file +pub fn get_java_executable( + configuration: &Option, + worktree: &Worktree, +) -> zed::Result { + let java_executable_filename = match current_platform().0 { + Os::Windows => "java.exe", + _ => "java", + }; + + // Get executable from $JAVA_HOME + if let Some(java_home) = get_java_home(configuration, worktree) { + let java_executable = PathBuf::from(java_home) + .join("bin") + .join(java_executable_filename); + return Ok(java_executable); + } + // If we can't, try to get it from $PATH + worktree + .which(java_executable_filename) + .map(PathBuf::from) + .ok_or_else(|| JAVA_EXEC_NOT_FOUND_ERROR.to_string()) +} + +/// Retrieve the java major version accessible by the extension +/// +/// # Arguments +/// +/// * [`java_executable`] the path to a java exec file +/// +/// # Returns +/// +/// Returns the java major version +/// +/// # Errors +/// +/// This function will return an error if: +/// +/// * [`java_executable`] can't be converted into a String +/// * No major version can be determined +pub fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { + let program = path_to_string(java_executable).map_err(|_| JAVA_EXEC_ERROR.to_string())?; + let output_bytes = Command::new(program).arg("-version").output()?.stderr; + let output = String::from_utf8(output_bytes).map_err(|e| e.to_string())?; + + let major_version_regex = + Regex::new(r#"version\s"(?P\d+)(\.\d+\.\d+(_\d+)?)?"#).map_err(|e| e.to_string())?; + let major_version = major_version_regex + .captures_iter(&output) + .find_map(|c| c.name("major").and_then(|m| m.as_str().parse::().ok())); + + if let Some(major_version) = major_version { + Ok(major_version) + } else { + Err(JAVA_VERSION_ERROR.to_string()) + } +} + +/// Convert [`path`] into [`String`] +/// +/// # Arguments +/// +/// * [`path`] the path of type [`AsRef`] to convert +/// +/// # Returns +/// +/// Returns a String representing [`path`] +/// +/// # Errors +/// +/// This function will return an error when the string conversion fails +pub fn path_to_string>(path: P) -> zed::Result { + path.as_ref() + .to_path_buf() + .into_os_string() + .into_string() + .map_err(|_| PATH_TO_STR_ERROR.to_string()) +} + +/// Remove all files or directories that aren't equal to [`filename`]. +/// +/// This function scans the directory given by [`prefix`] and removes any +/// file or directory whose name does not exactly match [`filename`]. +/// +/// # Arguments +/// +/// * [`prefix`] - The path to the directory to clean. See [`AsRef`] for supported types. +/// * [`filename`] - The name of the file to keep. +/// +/// # Returns +/// +/// Returns `Ok(())` on success, even if some removals fail (errors are printed to stdout). +pub fn remove_all_files_except>(prefix: P, filename: &str) -> zed::Result<()> { + match fs::read_dir(prefix) { + Ok(entries) => { + for entry in entries { + match entry { + Ok(entry) => { + if entry.file_name().to_str() != Some(filename) + && let Err(err) = fs::remove_dir_all(entry.path()) + { + println!("{msg}: {err}", msg = DIR_ENTRY_RM_ERROR, err = err); + } + } + Err(err) => println!("{msg}: {err}", msg = DIR_ENTRY_LOAD_ERROR, err = err), + } + } + } + Err(err) => println!("{msg}: {err}", msg = DIR_ENTRY_LS_ERROR, err = err), + } + + Ok(()) +} From 0e5cac6b094491091ce8ad388b0b9c3750182ba4 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:09:08 +0100 Subject: [PATCH 42/93] Simplify configuration and docs (#115) Closes #99 - add top-level configuration values for lombok_support and java_home - change lombok_support default to `true` - overhaul the `README` --- README.md | 220 +++++++++++++++++++++++++++++--------------------- src/config.rs | 25 +++--- 2 files changed, 141 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 8558bbc..50b254d 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,139 @@ -# Zed Java +# Java Extension for Zed -This extension adds support for the Java language. +This extension adds support for the Java language to [Zed](https://zed.dev). It is using the [Eclipse JDT Language Server](https://projects.eclipse.org/projects/eclipse.jdt.ls) (JDTLS for short) to provide completions, code-actions and diagnostics. -## Configuration Options +## Quick Start -If [Lombok] support is enabled via [JDTLS] configuration option -(`settings.java.jdt.ls.lombokSupport.enabled`), this -extension will download and add [Lombok] as a javaagent to the JVM arguments for -[JDTLS]. +Install the extension via Zeds extension manager. It should work out of the box for most people. However, there are some things to know: -There are also many more options you can pass directly to the language server, -for example: +- 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). + +- 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. + +- You can provide a **custom launch script for JDTLS**, by adding an executable named `jdtls` (or `jdtls.bat` on Windows) to your `$PATH` environment variable. If this is present, the extension will skip downloading and launching a managed instance and use the one from the environment. + +- To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`. + +Here is a common `settings.json` including the above mentioned configurations: + +```jsonc +"lsp": { + "jdtls": { + "settings": { + "java_home": "/path/to/your/JDK21+", + "lombok_support": true, + } + } +} +``` + +## Debugger + +Debug support is enabled via our [Fork of Java Debug](https://github.com/zed-industries/java-debug), which the extension will automatically download and start for you. Please refer to the [Zed Documentation](https://zed.dev/docs/debugger#getting-started) for general information about how debugging works in Zed. + +To get started with Java, click the `edit debug.json` button in the Debug menu, and replace the contents of the file with the following: +```jsonc +[ + { + "adapter": "Java", + "request": "launch", + "label": "Launch Debugger", + // if your project has multiple entry points, specify the one to use: + // "mainClass": "com.myorganization.myproject.MyMainClass", + // + // this effectively sets a breakpoint at your program entry: + "stopOnEntry": true, + // the working directory for the debug process + "cwd": "$ZED_WORKTREE_ROOT" + } +] +``` + +You should then be able to start a new Debug Session with the "Launch Debugger" scenario from the debug menu. + +## Launch Scripts (aka Tasks) in Windows + +This extension provides tasks for running your application and tests from within Zed via little play buttons next to tests/entry points. However, due to current limitiations of Zed's extension interface, we can not provide scripts that will work across Maven and Gradle on both Windows and Unix-compatible systems, so out of the box the launch scripts only work on Mac and Linux. + +There is a fairly straightforward fix that you can apply to make it work on Windows by supplying your own task scripts. Please see [this Issue](https://github.com/zed-extensions/java/issues/94) for information on how to do that and read the [Tasks section in Zeds documentation](https://zed.dev/docs/tasks) for more information. + +## Advanced Configuration/JDTLS initialization Options +JDTLS provides many configuration options that can be passed via the `initialize` LSP-request. The extension will pass the JSON-object from `lsp.jdtls.settings.initialization_options` in your settings on to JDTLS. Please refer to the [JDTLS Configuration Wiki Page](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request) for the available options and values. Below is an example `settings.json` that would pass on the example configuration from the above wiki page to JDTLS: ```jsonc { "lsp": { "jdtls": { - "initialization_options": { - "bundles": [], - "workspaceFolders": ["file:///home/snjeza/Project"], - }, "settings": { - "java": { - "home": "/usr/local/jdk-9.0.1", - "errors": { - "incompleteClasspath": { - "severity": "warning", - }, - }, - "configuration": { - "updateBuildConfiguration": "interactive", - "maven": { - "userSettings": null, - }, - }, - "trace": { - "server": "verbose", - }, - "import": { - "gradle": { - "enabled": true, - }, - "maven": { - "enabled": true, - }, - "exclusions": [ - "**/node_modules/**", - "**/.metadata/**", - "**/archetype-resources/**", - "**/META-INF/maven/**", - "/**/test/**", - ], - }, - "jdt": { - "ls": { - "lombokSupport": { - "enabled": false, // Set this to true to enable lombok support + // this will be sent to JDTLS as initializationOptions: + "initialization_options": { + "bundles": [], + // use this if your zed project root folder is not the same as the java project root: + "workspaceFolders": ["file:///home/snjeza/Project"], + "settings": { + "java": { + "home": "/usr/local/jdk-9.0.1", + "errors": { + "incompleteClasspath": { + "severity": "warning" + } + }, + "configuration": { + "updateBuildConfiguration": "interactive", + "maven": { + "userSettings": null + } + }, + "import": { + "gradle": { + "enabled": true + }, + "maven": { + "enabled": true + }, + "exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**", + "/**/test/**" + ] + }, + "referencesCodeLens": { + "enabled": false + }, + "signatureHelp": { + "enabled": false }, - }, - }, - "referencesCodeLens": { - "enabled": false, - }, - "signatureHelp": { - "enabled": false, - }, - "implementationsCodeLens": { - "enabled": false, - }, - "format": { - "enabled": true, - }, - "saveActions": { - "organizeImports": false, - }, - "contentProvider": { - "preferred": null, - }, - "autobuild": { - "enabled": false, - }, - "completion": { - "favoriteStaticMembers": [ - "org.junit.Assert.*", - "org.junit.Assume.*", - "org.junit.jupiter.api.Assertions.*", - "org.junit.jupiter.api.Assumptions.*", - "org.junit.jupiter.api.DynamicContainer.*", - "org.junit.jupiter.api.DynamicTest.*", - ], - "importOrder": ["java", "javax", "com", "org"], - }, - }, - }, - }, - }, + "implementationCodeLens": "all", + "format": { + "enabled": true + }, + "saveActions": { + "organizeImports": false + }, + "contentProvider": { + "preferred": null + }, + "autobuild": { + "enabled": false + }, + "completion": { + "favoriteStaticMembers": [ + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*" + ], + "importOrder": ["java", "javax", "com", "org"] + } + } + } + } + } + } + } } ``` - -*Example taken from JDTLS's [configuration options wiki page].* - -You can see all the options JDTLS accepts [here][configuration options wiki page]. - -[JDTLS]: https://github.com/eclipse-jdtls/eclipse.jdt.ls -[configuration options wiki page]: https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request -[Lombok]: https://projectlombok.org diff --git a/src/config.rs b/src/config.rs index ecb0a02..4f677b0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,14 +5,18 @@ use crate::util::expand_home_path; pub fn get_java_home(configuration: &Option, worktree: &Worktree) -> Option { // try to read the value from settings if let Some(configuration) = configuration - && let Some(java_home) = configuration.pointer("/java/home").and_then(|x| x.as_str()) { - match expand_home_path(worktree, java_home.to_string()) { - Ok(home_path) => return Some(home_path), - Err(err) => { - println!("{}", err); - } - }; - } + && let Some(java_home) = configuration + .pointer("/java_home") + .or_else(|| configuration.pointer("/java/home")) // legacy support + .and_then(|x| x.as_str()) + { + match expand_home_path(worktree, java_home.to_string()) { + Ok(home_path) => return Some(home_path), + Err(err) => { + println!("{}", err); + } + }; + } // try to read the value from env match worktree @@ -30,8 +34,9 @@ pub fn is_lombok_enabled(configuration: &Option) -> bool { .as_ref() .and_then(|configuration| { configuration - .pointer("/java/jdt/ls/lombokSupport/enabled") + .pointer("/lombok_support") + .or_else(|| configuration.pointer("/java/jdt/ls/lombokSupport/enabled")) // legacy support .and_then(|enabled| enabled.as_bool()) }) - .unwrap_or(false) + .unwrap_or(true) } From 7f63471025c8494cf1fb8642fb3302c89e4a3070 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:06:30 +0100 Subject: [PATCH 43/93] chore: bump patch versions of dependencies to latest, and this crate to 6.6.0 (#119) --- Cargo.lock | 29 ++++++++++++++++++++--------- Cargo.toml | 8 ++++---- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e300cfd..479c79e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -488,9 +488,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -532,18 +532,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -552,14 +562,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -864,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.5.0" +version = "6.6.0" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index c27e0e8..acd93ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.5.0" +version = "6.6.0" edition = "2024" publish = false license = "Apache-2.0" @@ -12,8 +12,8 @@ crate-type = ["cdylib"] [dependencies] hex = "0.4.3" -regex = "1.12.1" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" +regex = "1.12.2" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" sha1 = "0.10.6" zed_extension_api = "0.7.0" From 42da742a52ef1ccb216b310e6698a92d6928fbcd Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:57:58 +0100 Subject: [PATCH 44/93] Bump version to 6.6.0 (#120) --- extension.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension.toml b/extension.toml index d9feca8..3706412 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.5.0" +version = "6.6.0" schema_version = 1 authors = [ "Valentine Briese ", From 430d1671e8dfb5dd11777cf5933fce71914db468 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:45:27 +0100 Subject: [PATCH 45/93] JDK auto download opt-in option (#117) Closes [#113](https://github.com/zed-extensions/java/issues/113) Introduce the option to let the extension retrieve a working version of JDK in the instance no java exec has been found in the PATH or at JAVA_HOME. An auto download is also triggered if the user installed version does not respect the minimum version required by JDTLS. --- README.md | 3 ++ src/config.rs | 11 +++++ src/java.rs | 2 + src/jdk.rs | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/jdtls.rs | 22 ++++++--- src/util.rs | 46 ++++++++++++++----- 6 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 src/jdk.rs diff --git a/README.md b/README.md index 50b254d..4eb028a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Install the extension via Zeds extension manager. It should work out of the box - To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`. +- The option to let the extension automatically download a version of OpenJDK can be enabled by setting `jdk_auto_download` to `true`. When enabled, the extension will only download a JDK if no valid java_home is provided or if the specified one does not meet the minimum version requirement. User-provided JDKs **always** take precedence. + Here is a common `settings.json` including the above mentioned configurations: ```jsonc @@ -22,6 +24,7 @@ Here is a common `settings.json` including the above mentioned configurations: "settings": { "java_home": "/path/to/your/JDK21+", "lombok_support": true, + "jdk_auto_download": false } } } diff --git a/src/config.rs b/src/config.rs index 4f677b0..3dc9fe3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,17 @@ pub fn get_java_home(configuration: &Option, worktree: &Worktree) -> Opti } } +pub fn is_java_autodownload(configuration: &Option) -> bool { + configuration + .as_ref() + .and_then(|configuration| { + configuration + .pointer("/jdk_auto_download") + .and_then(|enabled| enabled.as_bool()) + }) + .unwrap_or(false) +} + pub fn is_lombok_enabled(configuration: &Option) -> bool { configuration .as_ref() diff --git a/src/java.rs b/src/java.rs index 7bc8b11..86745aa 100644 --- a/src/java.rs +++ b/src/java.rs @@ -1,5 +1,6 @@ mod config; mod debugger; +mod jdk; mod jdtls; mod lsp; mod util; @@ -289,6 +290,7 @@ impl Extension for Java { &configuration, worktree, lombok_jvm_arg.into_iter().collect(), + language_server_id, )?); } diff --git a/src/jdk.rs b/src/jdk.rs new file mode 100644 index 0000000..0e128c3 --- /dev/null +++ b/src/jdk.rs @@ -0,0 +1,124 @@ +use std::path::{Path, PathBuf}; + +use zed_extension_api::{ + self as zed, Architecture, DownloadedFileType, LanguageServerId, + LanguageServerInstallationStatus, Os, current_platform, download_file, + set_language_server_installation_status, +}; + +use crate::util::{get_curr_dir, path_to_string, remove_all_files_except}; + +// Errors +const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory"; +const NO_JDK_DIR_ERROR: &str = "No match for jdk or corretto in the extracted directory"; + +const CORRETTO_REPO: &str = "corretto/corretto-25"; +const CORRETTO_UNIX_URL_TEMPLATE: &str = "https://corretto.aws/downloads/resources/{version}/amazon-corretto-{version}-{platform}-{arch}.tar.gz"; +const CORRETTO_WINDOWS_URL_TEMPLATE: &str = "https://corretto.aws/downloads/resources/{version}/amazon-corretto-{version}-{platform}-{arch}-jdk.zip"; + +fn build_corretto_url(version: &str, platform: &str, arch: &str) -> String { + let template = match zed::current_platform().0 { + Os::Windows => CORRETTO_WINDOWS_URL_TEMPLATE, + _ => CORRETTO_UNIX_URL_TEMPLATE, + }; + + template + .replace("{version}", version) + .replace("{platform}", platform) + .replace("{arch}", arch) +} + +// For now keep in this file as they are not used anywhere else +// otherwise move to util +fn get_architecture() -> zed::Result { + match zed::current_platform() { + (_, Architecture::Aarch64) => Ok("aarch64".to_string()), + (_, Architecture::X86) => Ok("x86".to_string()), + (_, Architecture::X8664) => Ok("x64".to_string()), + } +} + +fn get_platform() -> zed::Result { + match zed::current_platform() { + (Os::Mac, _) => Ok("macosx".to_string()), + (Os::Linux, _) => Ok("linux".to_string()), + (Os::Windows, _) => Ok("windows".to_string()), + } +} + +pub fn try_to_fetch_and_install_latest_jdk( + language_server_id: &LanguageServerId, +) -> zed::Result { + let version = zed::latest_github_release( + CORRETTO_REPO, + zed_extension_api::GithubReleaseOptions { + require_assets: false, + pre_release: false, + }, + )? + .version; + + let jdk_path = get_curr_dir()?.join("jdk"); + let install_path = jdk_path.join(&version); + + // Check for updates, if same version is already downloaded skip download + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + if !install_path.exists() { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + let platform = get_platform()?; + let arch = get_architecture()?; + + download_file( + build_corretto_url(&version, &platform, &arch).as_str(), + path_to_string(install_path.clone())?.as_str(), + match zed::current_platform().0 { + Os::Windows => DownloadedFileType::Zip, + _ => DownloadedFileType::GzipTar, + }, + )?; + + // Remove older versions + let _ = remove_all_files_except(jdk_path, version.as_str()); + } + + // Depending on the platform the name of the extracted dir might differ + // Rather than hard coding, extract it dynamically + let extracted_dir = get_extracted_dir(&install_path)?; + + Ok(install_path + .join(extracted_dir) + .join(match current_platform().0 { + Os::Mac => "Contents/Home/bin", + _ => "bin", + })) +} + +fn get_extracted_dir(path: &Path) -> zed::Result { + let Ok(mut entries) = path.read_dir() else { + return Err(JDK_DIR_ERROR.to_string()); + }; + + match entries.find_map(|entry| { + let entry = entry.ok()?; + let file_name = entry.file_name(); + let name_str = file_name.to_string_lossy().to_string(); + + if name_str.contains("jdk") || name_str.contains("corretto") { + Some(name_str) + } else { + None + } + }) { + Some(dir_path) => Ok(dir_path), + None => Err(NO_JDK_DIR_ERROR.to_string()), + } +} diff --git a/src/jdtls.rs b/src/jdtls.rs index ceccfa5..200446b 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,9 +15,13 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::util::{ - get_curr_dir, get_java_executable, get_java_major_version, path_to_string, - remove_all_files_except, +use crate::{ + config::is_java_autodownload, + jdk::try_to_fetch_and_install_latest_jdk, + util::{ + get_curr_dir, get_java_exec_name, get_java_executable, get_java_major_version, + path_to_string, remove_all_files_except, + }, }; const JDTLS_INSTALL_PATH: &str = "jdtls"; @@ -25,22 +29,28 @@ const LOMBOK_INSTALL_PATH: &str = "lombok"; // Errors -const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java 21. If you need to run a JVM < 21, you can specify a different one for JDTLS to use by specifying lsp.jdtls.settings.java.home in the settings"; +const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java version 21 to run. You can either specify a different JDK to use by configuring lsp.jdtls.settings.java_home to point to a different JDK, or set lsp.jdtls.settings.jdk_auto_download to true to let the extension automatically download one for you."; pub fn build_jdtls_launch_args( jdtls_path: &PathBuf, configuration: &Option, worktree: &Worktree, jvm_args: Vec, + language_server_id: &LanguageServerId, ) -> zed::Result> { if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) { return Ok(vec![jdtls_launcher]); } - let java_executable = get_java_executable(configuration, worktree)?; + let mut java_executable = get_java_executable(configuration, worktree, language_server_id)?; let java_major_version = get_java_major_version(&java_executable)?; if java_major_version < 21 { - return Err(JAVA_VERSION_ERROR.to_string()); + if is_java_autodownload(configuration) { + java_executable = + try_to_fetch_and_install_latest_jdk(language_server_id)?.join(get_java_exec_name()); + } else { + return Err(JAVA_VERSION_ERROR.to_string()); + } } let extension_workdir = get_curr_dir()?; diff --git a/src/util.rs b/src/util.rs index 2c44c2e..adc8b62 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,9 +4,14 @@ use std::{ fs, path::{Path, PathBuf}, }; -use zed_extension_api::{self as zed, Command, Os, Worktree, current_platform, serde_json::Value}; +use zed_extension_api::{ + self as zed, Command, LanguageServerId, Os, Worktree, current_platform, serde_json::Value, +}; -use crate::config::get_java_home; +use crate::{ + config::{get_java_home, is_java_autodownload}, + jdk::try_to_fetch_and_install_latest_jdk, +}; // Errors const EXPAND_ERROR: &str = "Failed to expand ~"; @@ -59,9 +64,10 @@ pub fn get_curr_dir() -> zed::Result { } /// Retrieve the path to a java exec either: -/// - defined by the user in `settings.json` +/// - defined by the user in `settings.json` under option `java_home` /// - from PATH /// - from JAVA_HOME +/// - from the bundled OpenJDK if option `jdk_auto_download` is true /// /// # Arguments /// @@ -79,11 +85,9 @@ pub fn get_curr_dir() -> zed::Result { pub fn get_java_executable( configuration: &Option, worktree: &Worktree, + language_server_id: &LanguageServerId, ) -> zed::Result { - let java_executable_filename = match current_platform().0 { - Os::Windows => "java.exe", - _ => "java", - }; + let java_executable_filename = get_java_exec_name(); // Get executable from $JAVA_HOME if let Some(java_home) = get_java_home(configuration, worktree) { @@ -93,10 +97,30 @@ pub fn get_java_executable( return Ok(java_executable); } // If we can't, try to get it from $PATH - worktree - .which(java_executable_filename) - .map(PathBuf::from) - .ok_or_else(|| JAVA_EXEC_NOT_FOUND_ERROR.to_string()) + if let Some(java_home) = worktree.which(java_executable_filename.as_str()) { + return Ok(PathBuf::from(java_home)); + } + + // If the user has set the option, retrieve the latest version of Corretto (OpenJDK) + if is_java_autodownload(configuration) { + return Ok( + try_to_fetch_and_install_latest_jdk(language_server_id)?.join(java_executable_filename) + ); + } + + Err(JAVA_EXEC_NOT_FOUND_ERROR.to_string()) +} + +/// Retrieve the executable name for Java on this platform +/// +/// # Returns +/// +/// Returns the executable java name +pub fn get_java_exec_name() -> String { + match current_platform().0 { + Os::Windows => "java.exe".to_string(), + _ => "java".to_string(), + } } /// Retrieve the java major version accessible by the extension From 55b8074d137f14645fb8de5a464a43a61422b8f2 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:36:04 +0100 Subject: [PATCH 46/93] Move Java struct initialization out of language_server_binary_path (#123) Closes #121 - In the instance language_server_binary_path is not called, due to user provided JDTLS in the PATH, the extension would have failed as both lsp and debugger were None --- src/java.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/java.rs b/src/java.rs index 86745aa..95e9fcd 100644 --- a/src/java.rs +++ b/src/java.rs @@ -60,11 +60,7 @@ impl Java { .map(|v| &mut v.1) } - fn language_server_binary_path( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result { + fn init(&mut self, worktree: &Worktree) { // Initialize lsp client and debugger if self.integrations.is_none() { @@ -73,7 +69,12 @@ impl Java { self.integrations = Some((lsp, debugger)); } + } + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + ) -> zed::Result { // Use cached path if exists if let Some(path) = &self.cached_binary_path @@ -277,6 +278,8 @@ impl Extension for Java { None }; + self.init(worktree); + if let Some(launcher) = get_jdtls_launcher_from_path(worktree) { // if the user has `jdtls(.bat)` on their PATH, we use that args.push(launcher); @@ -286,7 +289,7 @@ impl Extension for Java { } else { // otherwise we launch ourselves args.extend(build_jdtls_launch_args( - &self.language_server_binary_path(language_server_id, worktree)?, + &self.language_server_binary_path(language_server_id)?, &configuration, worktree, lombok_jvm_arg.into_iter().collect(), From 0c67d8c59ea2294e25b2364561a9828ccbf8a4ca Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Thu, 6 Nov 2025 18:05:00 +0100 Subject: [PATCH 47/93] chore: version retrieval from tags (#118) Improve code maintainability by adopting a unified method to retrieve the latest version from git tags. - Extracted into a function the code to retrieve all repository's tags and from the latest retrieve the release version --- src/jdtls.rs | 144 +++++++++++++++------------------------------------ src/util.rs | 57 ++++++++++++++++++++ 2 files changed, 98 insertions(+), 103 deletions(-) diff --git a/src/jdtls.rs b/src/jdtls.rs index 200446b..338ba45 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -1,5 +1,4 @@ use std::{ - collections::BTreeSet, env::current_dir, fs::{create_dir, metadata, read_dir}, path::{Path, PathBuf}, @@ -20,16 +19,19 @@ use crate::{ jdk::try_to_fetch_and_install_latest_jdk, util::{ get_curr_dir, get_java_exec_name, get_java_executable, get_java_major_version, - path_to_string, remove_all_files_except, + get_latest_versions_from_tag, path_to_string, remove_all_files_except, }, }; const JDTLS_INSTALL_PATH: &str = "jdtls"; +const JDTLS_REPO: &str = "eclipse-jdtls/eclipse.jdt.ls"; const LOMBOK_INSTALL_PATH: &str = "lombok"; +const LOMBOK_REPO: &str = "projectlombok/lombok"; // Errors const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java version 21 to run. You can either specify a different JDK to use by configuring lsp.jdtls.settings.java_home to point to a different JDK, or set lsp.jdtls.settings.jdk_auto_download to true to let the extension automatically download one for you."; +const JDTLS_VERION_ERROR: &str = "No version to fallback to"; pub fn build_jdtls_launch_args( jdtls_path: &PathBuf, @@ -151,85 +153,24 @@ pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { pub fn try_to_fetch_and_install_latest_jdtls( language_server_id: &LanguageServerId, ) -> zed::Result { - // Yeah, this part's all pretty terrible... - // Note to self: make it good eventually - let downloads_html = String::from_utf8( - fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url("https://download.eclipse.org/jdtls/milestones/") - .build()?, - ) - .map_err(|err| format!("failed to get available versions: {err}"))? - .body, - ) - .map_err(|err| format!("could not get string from downloads page response body: {err}"))?; - let mut versions = BTreeSet::new(); - let mut number_buffer = String::new(); - let mut version_buffer: (Option, Option, Option) = (None, None, None); - - for char in downloads_html.chars() { - if char.is_numeric() { - number_buffer.push(char); - } else if char == '.' { - if version_buffer.0.is_none() && !number_buffer.is_empty() { - version_buffer.0 = Some( - number_buffer - .parse() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - ); - } else if version_buffer.1.is_none() && !number_buffer.is_empty() { - version_buffer.1 = Some( - number_buffer - .parse() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - ); - } else { - version_buffer = (None, None, None); - } - - number_buffer.clear(); - } else { - if version_buffer.0.is_some() - && version_buffer.1.is_some() - && version_buffer.2.is_none() - { - versions.insert(( - version_buffer.0.ok_or("no major version number")?, - version_buffer.1.ok_or("no minor version number")?, - number_buffer - .parse::() - .map_err(|err| format!("could not parse number buffer: {err}"))?, - )); - } - - number_buffer.clear(); - version_buffer = (None, None, None); - } - } + let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; + + let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) + .map_or_else( + |_| { + second_last + .as_ref() + .ok_or(JDTLS_VERION_ERROR.to_string()) + .and_then(|fallback| download_jdtls_milestone(fallback)) + .map(|milestone| (second_last.unwrap(), milestone.trim_end().to_string())) + }, + |milestone| Ok((last, milestone.trim_end().to_string())), + )?; - let (major, minor, patch) = versions.last().ok_or("no available versions")?; - let latest_version = format!("{major}.{minor}.{patch}"); - let latest_version_build = String::from_utf8( - fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url(format!( - "https://download.eclipse.org/jdtls/milestones/{latest_version}/latest.txt" - )) - .build()?, - ) - .map_err(|err| format!("failed to get latest version's build: {err}"))? - .body, - ) - .map_err(|err| { - format!("attempt to get latest version's build resulted in a malformed response: {err}") - })?; - let latest_version_build = latest_version_build.trim_end(); let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - // Exclude ".tar.gz" - let build_directory = &latest_version_build[..latest_version_build.len() - 7]; - let build_path = prefix.join(build_directory); + + let build_directory = latest_version_build.replace(".tar.gz", ""); + let build_path = prefix.join(&build_directory); let binary_path = build_path.join("bin").join(get_binary_name()); // If latest version isn't installed, @@ -242,7 +183,7 @@ pub fn try_to_fetch_and_install_latest_jdtls( ); download_file( &format!( - "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}", + "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" ), path_to_string(build_path.clone())?.as_str(), DownloadedFileType::GzipTar, @@ -250,7 +191,7 @@ pub fn try_to_fetch_and_install_latest_jdtls( make_file_executable(path_to_string(binary_path)?.as_str())?; // ...and delete other versions - let _ = remove_all_files_except(prefix, build_directory); + let _ = remove_all_files_except(prefix, build_directory.as_str()); } // return jdtls base path @@ -265,28 +206,7 @@ pub fn try_to_fetch_and_install_latest_lombok( &LanguageServerInstallationStatus::CheckingForUpdate, ); - let tags_response_body = serde_json::from_slice::( - &fetch( - &HttpRequest::builder() - .method(HttpMethod::Get) - .url("https://api.github.com/repos/projectlombok/lombok/tags") - .build()?, - ) - .map_err(|err| format!("failed to fetch GitHub tags: {err}"))? - .body, - ) - .map_err(|err| format!("failed to deserialize GitHub tags response: {err}"))?; - let latest_version = &tags_response_body - .as_array() - .and_then(|tag| { - tag.first().and_then(|latest_tag| { - latest_tag - .get("name") - .and_then(|tag_name| tag_name.as_str()) - }) - }) - // Exclude 'v' at beginning - .ok_or("malformed GitHub tags response")?[1..]; + let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO)?; let prefix = LOMBOK_INSTALL_PATH; let jar_name = format!("lombok-{latest_version}.jar"); let jar_path = Path::new(prefix).join(&jar_name); @@ -315,6 +235,24 @@ pub fn try_to_fetch_and_install_latest_lombok( Ok(jar_path) } +fn download_jdtls_milestone(version: &str) -> zed::Result { + String::from_utf8( + fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url(format!( + "https://download.eclipse.org/jdtls/milestones/{version}/latest.txt" + )) + .build()?, + ) + .map_err(|err| format!("failed to get latest version's build: {err}"))? + .body, + ) + .map_err(|err| { + format!("attempt to get latest version's build resulted in a malformed response: {err}") + }) +} + fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result { let plugins_dir = jdtls_base_directory.join("plugins"); diff --git a/src/util.rs b/src/util.rs index adc8b62..96304f5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,6 +6,7 @@ use std::{ }; use zed_extension_api::{ self as zed, Command, LanguageServerId, Os, Worktree, current_platform, serde_json::Value, + http_client::{HttpMethod, HttpRequest, fetch} }; use crate::{ @@ -23,6 +24,9 @@ const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; const JAVA_EXEC_ERROR: &str = "Failed to convert Java executable path to string"; const JAVA_VERSION_ERROR: &str = "Failed to determine Java major version"; const JAVA_EXEC_NOT_FOUND_ERROR: &str = "Could not find Java executable in JAVA_HOME or on PATH"; +const TAG_RETRIEVAL_ERROR: &str = "Failed to fetch GitHub tags"; +const TAG_RESPONSE_ERROR: &str = "Failed to deserialize GitHub tags response"; +const TAG_UNEXPECTED_FORMAT_ERROR: &str = "Malformed GitHub tags response"; /// Expand ~ on Unix-like systems /// @@ -157,6 +161,59 @@ pub fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { } } +/// Retrieve the latest and second latest versions from the repo tags +/// +/// # Arguments +/// +/// * [`repo`] The GitHub repository from which to retrieve the tags +/// +/// # Returns +/// +/// A tuple containing the latest version, and optionally, the second latest version if available +/// +/// # Errors +/// +/// This function will return an error if: +/// * Could not fetch tags from Github +/// * Failed to deserialize response +/// * Unexpected Github response format +pub fn get_latest_versions_from_tag(repo: &str) -> zed::Result<(String, Option)> { + let tags_response_body = serde_json::from_slice::( + &fetch( + &HttpRequest::builder() + .method(HttpMethod::Get) + .url(format!("https://api.github.com/repos/{repo}/tags")) + .build()?, + ) + .map_err(|err| format!("{TAG_RETRIEVAL_ERROR}: {err}"))? + .body, + ) + .map_err(|err| format!("{TAG_RESPONSE_ERROR}: {err}"))?; + + let latest_version = get_tag_at(&tags_response_body, 0); + let second_version = get_tag_at(&tags_response_body, 1); + + if latest_version.is_none() { + return Err(TAG_UNEXPECTED_FORMAT_ERROR.to_string()); + } + + Ok(( + latest_version.unwrap().to_string(), + second_version.map(|second| second.to_string()), + )) +} + +fn get_tag_at(github_tags: &Value, index: usize) -> Option<&str> { + github_tags.as_array().and_then(|tag| { + tag.get(index).and_then(|latest_tag| { + latest_tag + .get("name") + .and_then(|tag_name| tag_name.as_str()) + .map(|val| &val[1..]) + }) + }) +} + /// Convert [`path`] into [`String`] /// /// # Arguments From e1ca2cbe73a7dcb3635b0d06d16b7ae79fb81fe3 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:20:17 +0100 Subject: [PATCH 48/93] Recreate the debugger path before downloading the debugger.jar (#126) Fixes #124 When downloading the debugger-adapter-jar, we need to ensure the path exists. --- src/debugger.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/debugger.rs b/src/debugger.rs index 5650152..2e3f05a 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -104,6 +104,9 @@ impl Debugger { return Ok(path.clone()); } + fs::remove_dir_all(prefix).map_err(|err| err.to_string())?; + fs::create_dir(prefix).map_err(|err| err.to_string())?; + download_file( JAVA_DEBUG_PLUGIN_FORK_URL, &path_to_string(jar_path.clone())?, From 68afc389792451ca16e979072454f98ddb077d5c Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sat, 8 Nov 2025 19:52:06 +0100 Subject: [PATCH 49/93] Bump version to 6.7.0 (#127) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 479c79e..dce072b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.6.0" +version = "6.7.0" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index acd93ec..8cbccb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.6.0" +version = "6.7.0" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 3706412..815784b 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.6.0" +version = "6.7.0" schema_version = 1 authors = [ "Valentine Briese ", From d896abeb614f245f0ec08c4e5ba35859aebf896d Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:39:53 +0100 Subject: [PATCH 50/93] 129 fix debugger path (#130) Fixes #129 Previously non-existing debugger path would error and not download the debugger due to remove_dir_all failing on nonexistent directory. --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- src/debugger.rs | 5 ++--- src/util.rs | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dce072b..f30accc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.7.0" +version = "6.7.1" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 8cbccb1..2e8706f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.7.0" +version = "6.7.1" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 815784b..88ea5ba 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.7.0" +version = "6.7.1" schema_version = 1 authors = [ "Valentine Briese ", diff --git a/src/debugger.rs b/src/debugger.rs index 2e3f05a..7c3ed82 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -11,7 +11,7 @@ use zed_extension_api::{ use crate::{ lsp::LspWrapper, - util::{get_curr_dir, path_to_string}, + util::{create_path_if_not_exists, get_curr_dir, path_to_string}, }; #[derive(Serialize, Deserialize, Debug)] @@ -104,8 +104,7 @@ impl Debugger { return Ok(path.clone()); } - fs::remove_dir_all(prefix).map_err(|err| err.to_string())?; - fs::create_dir(prefix).map_err(|err| err.to_string())?; + create_path_if_not_exists(prefix)?; download_file( JAVA_DEBUG_PLUGIN_FORK_URL, diff --git a/src/util.rs b/src/util.rs index 96304f5..645d859 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,8 +5,9 @@ use std::{ path::{Path, PathBuf}, }; use zed_extension_api::{ - self as zed, Command, LanguageServerId, Os, Worktree, current_platform, serde_json::Value, - http_client::{HttpMethod, HttpRequest, fetch} + self as zed, Command, LanguageServerId, Os, Worktree, current_platform, + http_client::{HttpMethod, HttpRequest, fetch}, + serde_json::Value, }; use crate::{ @@ -28,6 +29,33 @@ const TAG_RETRIEVAL_ERROR: &str = "Failed to fetch GitHub tags"; const TAG_RESPONSE_ERROR: &str = "Failed to deserialize GitHub tags response"; const TAG_UNEXPECTED_FORMAT_ERROR: &str = "Malformed GitHub tags response"; +/// Create a Path if it does not exist +/// +/// **Errors** if a file that is not a path exists at the location or read/write access failed for the location +/// +///# Arguments +/// * [`path`] the path to create +/// +///# Returns +/// +/// Ok(()) if the path exists or was created successfully +pub fn create_path_if_not_exists>(path: P) -> zed::Result<()> { + let path_ref = path.as_ref(); + match fs::metadata(path_ref) { + Ok(metadata) => { + if metadata.is_dir() { + Ok(()) + } else { + Err(format!("File exists but is not a path: {:?}", path_ref)) + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + fs::create_dir_all(path_ref).map_err(|e| e.to_string()) + } + Err(e) => Err(e.to_string()), + } +} + /// Expand ~ on Unix-like systems /// /// # Arguments From 89edf245f9e5c6680c20cd29442a7596c3fecb92 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:23:59 +0100 Subject: [PATCH 51/93] Expand usage of secure create_dir function (#131) Cover all insecure instances of create_dir which could have caused the extension to fail with the function `create_path_if_not_exists` --- src/debugger.rs | 2 +- src/jdtls.rs | 9 +++++---- src/util.rs | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 7c3ed82..c6b98ca 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -210,7 +210,7 @@ impl Debugger { language_server_id, &LanguageServerInstallationStatus::Downloading, ); - fs::create_dir(prefix).map_err(|err| err.to_string())?; + create_path_if_not_exists(prefix)?; let url = format!( "https://repo1.maven.org/maven2/com/microsoft/java/{artifact}/{latest_version}/{jar_name}" diff --git a/src/jdtls.rs b/src/jdtls.rs index 338ba45..e48f223 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -1,6 +1,6 @@ use std::{ env::current_dir, - fs::{create_dir, metadata, read_dir}, + fs::{metadata, read_dir}, path::{Path, PathBuf}, }; @@ -18,8 +18,9 @@ use crate::{ config::is_java_autodownload, jdk::try_to_fetch_and_install_latest_jdk, util::{ - get_curr_dir, get_java_exec_name, get_java_executable, get_java_major_version, - get_latest_versions_from_tag, path_to_string, remove_all_files_except, + create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable, + get_java_major_version, get_latest_versions_from_tag, path_to_string, + remove_all_files_except, }, }; @@ -219,7 +220,7 @@ pub fn try_to_fetch_and_install_latest_lombok( language_server_id, &LanguageServerInstallationStatus::Downloading, ); - create_dir(prefix).map_err(|err| err.to_string())?; + create_path_if_not_exists(prefix)?; download_file( &format!("https://projectlombok.org/downloads/{jar_name}"), path_to_string(jar_path.clone())?.as_str(), diff --git a/src/util.rs b/src/util.rs index 645d859..b01bb2c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -28,6 +28,7 @@ const JAVA_EXEC_NOT_FOUND_ERROR: &str = "Could not find Java executable in JAVA_ const TAG_RETRIEVAL_ERROR: &str = "Failed to fetch GitHub tags"; const TAG_RESPONSE_ERROR: &str = "Failed to deserialize GitHub tags response"; const TAG_UNEXPECTED_FORMAT_ERROR: &str = "Malformed GitHub tags response"; +const PATH_IS_NOT_DIR: &str = "File exists but is not a path"; /// Create a Path if it does not exist /// @@ -46,7 +47,7 @@ pub fn create_path_if_not_exists>(path: P) -> zed::Result<()> { if metadata.is_dir() { Ok(()) } else { - Err(format!("File exists but is not a path: {:?}", path_ref)) + Err(format!("{PATH_IS_NOT_DIR}: {:?}", path_ref)) } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { From 1fe6ff35588626444c45bb7c698d85e125269d7c Mon Sep 17 00:00:00 2001 From: HimiCos Date: Fri, 14 Nov 2025 15:44:13 +0800 Subject: [PATCH 52/93] Add configuration options for update checks and custom component paths (#132) **New Configuration Options:** - **`check_updates`**: Controls when to check for updates for JDTLS, Lombok, and Java Debug components - `"always"` (default): Always check for and download the latest version on every startup - `"once"`: Check for updates only if no local installation exists - `"never"`: Never check for updates, only use existing local installations (will error if missing) - Note: Invalid values default to `"always"` - **`jdtls_launcher`**: Path to a custom JDTLS launcher script - When set, the extension uses your provided launcher instead of the managed installation - Overrides automatic downloads; `check_updates` is ignored for JDTLS - **`lombok_jar`**: Path to a custom Lombok JAR file - When set, the extension uses your provided JAR instead of downloading it - Overrides automatic downloads; `check_updates` is ignored for Lombok - **`java_debug_jar`**: Path to a custom Java Debug plugin JAR file - When set, the extension uses your provided debugger instead of downloading it - Overrides automatic downloads; `check_updates` is ignored for the debugger **Behavior:** - User-provided custom paths always take precedence over managed installations - When a custom path is specified for a component, update checks are skipped for that component only - All paths support home directory expansion (e.g., `~/path/to/jar`) --- README.md | 17 ++++++++++- src/config.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ src/debugger.rs | 46 +++++++++++++++++++++++++++++- src/java.rs | 40 ++++++++++++++++++++------ src/jdtls.rs | 20 ++++++++++++- src/util.rs | 40 +++++++++++++++++++++++++- 6 files changed, 227 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4eb028a..62d7d06 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,22 @@ Here is a common `settings.json` including the above mentioned configurations: "settings": { "java_home": "/path/to/your/JDK21+", "lombok_support": true, - "jdk_auto_download": false + "jdk_auto_download": false, + + // Controls when to check for updates for JDTLS, Lombok, and Debugger + // - "always" (default): Always check for and download the latest version + // - "once": Check for updates only if no local installation exists + // - "never": Never check for updates, only use existing local installations (errors if missing) + // + // Note: Invalid values will default to "always" + // If custom paths (below) are provided, check_updates is IGNORED for that component + "check_updates": "always", + + // Use custom installations instead of managed downloads + // When these are set, the extension will not download or manage these components + "jdtls_launcher": "/path/to/your/jdt-language-server/bin/jdtls", + "lombok_jar": "/path/to/your/lombok.jar", + "java_debug_jar": "/path/to/your/com.microsoft.java.debug.plugin.jar" } } } diff --git a/src/config.rs b/src/config.rs index 3dc9fe3..a2f45b6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,14 @@ use zed_extension_api::{Worktree, serde_json::Value}; use crate::util::expand_home_path; +#[derive(Debug, Clone, PartialEq, Default)] +pub enum CheckUpdates { + #[default] + Always, + Once, + Never, +} + pub fn get_java_home(configuration: &Option, worktree: &Worktree) -> Option { // try to read the value from settings if let Some(configuration) = configuration @@ -51,3 +59,71 @@ pub fn is_lombok_enabled(configuration: &Option) -> bool { }) .unwrap_or(true) } + +pub fn get_check_updates(configuration: &Option) -> CheckUpdates { + if let Some(configuration) = configuration + && let Some(mode_str) = configuration + .pointer("/check_updates") + .and_then(|x| x.as_str()) + .map(|s| s.to_lowercase()) + { + return match mode_str.as_str() { + "once" => CheckUpdates::Once, + "never" => CheckUpdates::Never, + "always" => CheckUpdates::Always, + _ => CheckUpdates::default(), + }; + } + CheckUpdates::default() +} + +pub fn get_jdtls_launcher(configuration: &Option, worktree: &Worktree) -> Option { + if let Some(configuration) = configuration + && let Some(launcher_path) = configuration + .pointer("/jdtls_launcher") + .and_then(|x| x.as_str()) + { + match expand_home_path(worktree, launcher_path.to_string()) { + Ok(path) => return Some(path), + Err(err) => { + println!("{}", err); + } + } + } + + None +} + +pub fn get_lombok_jar(configuration: &Option, worktree: &Worktree) -> Option { + if let Some(configuration) = configuration + && let Some(jar_path) = configuration + .pointer("/lombok_jar") + .and_then(|x| x.as_str()) + { + match expand_home_path(worktree, jar_path.to_string()) { + Ok(path) => return Some(path), + Err(err) => { + println!("{}", err); + } + } + } + + None +} + +pub fn get_java_debug_jar(configuration: &Option, worktree: &Worktree) -> Option { + if let Some(configuration) = configuration + && let Some(jar_path) = configuration + .pointer("/java_debug_jar") + .and_then(|x| x.as_str()) + { + match expand_home_path(worktree, jar_path.to_string()) { + Ok(path) => return Some(path), + Err(err) => { + println!("{}", err); + } + } + } + + None +} diff --git a/src/debugger.rs b/src/debugger.rs index c6b98ca..aac5135 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -10,8 +10,9 @@ use zed_extension_api::{ }; use crate::{ + config::get_java_debug_jar, lsp::LspWrapper, - util::{create_path_if_not_exists, get_curr_dir, path_to_string}, + util::{create_path_if_not_exists, get_curr_dir, path_to_string, should_use_local_or_download}, }; #[derive(Serialize, Deserialize, Debug)] @@ -56,10 +57,35 @@ const RUNTIME_SCOPE: &str = "$Runtime"; const SCOPES: [&str; 3] = [TEST_SCOPE, AUTO_SCOPE, RUNTIME_SCOPE]; +const DEBUGGER_INSTALL_PATH: &str = "debugger"; + const JAVA_DEBUG_PLUGIN_FORK_URL: &str = "https://github.com/zed-industries/java-debug/releases/download/0.53.2/com.microsoft.java.debug.plugin-0.53.2.jar"; const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/java/com.microsoft.java.debug.plugin/maven-metadata.xml"; +pub fn find_latest_local_debugger() -> Option { + let prefix = PathBuf::from(DEBUGGER_INSTALL_PATH); + // walk the dir where we install debugger + fs::read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + // get the most recently created jar file + .filter(|path| { + path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("jar") + }) + .filter_map(|path| { + let created_time = fs::metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + .map(|(path, _)| path) + }) + .ok() + .flatten() +} + pub struct Debugger { lsp: LspWrapper, plugin_path: Option, @@ -80,10 +106,28 @@ impl Debugger { pub fn get_or_download( &mut self, language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, ) -> zed::Result { // when the fix to https://github.com/microsoft/java-debug/issues/605 becomes part of an official release // switch back to this: // return self.get_or_download_latest_official(language_server_id); + + // Use user-configured path if provided + if let Some(jar_path) = get_java_debug_jar(configuration, worktree) { + let path = PathBuf::from(&jar_path); + self.plugin_path = Some(path.clone()); + return Ok(path); + } + + // Use local installation if update mode requires it + if let Some(path) = + should_use_local_or_download(configuration, find_latest_local_debugger(), "debugger")? + { + self.plugin_path = Some(path.clone()); + return Ok(path); + } + self.get_or_download_fork(language_server_id) } diff --git a/src/java.rs b/src/java.rs index 95e9fcd..ff43cbe 100644 --- a/src/java.rs +++ b/src/java.rs @@ -24,7 +24,7 @@ use zed_extension_api::{ }; use crate::{ - config::{get_java_home, is_lombok_enabled}, + config::{get_java_home, get_jdtls_launcher, get_lombok_jar, is_lombok_enabled}, debugger::Debugger, jdtls::{ build_jdtls_launch_args, find_latest_local_jdtls, find_latest_local_lombok, @@ -74,6 +74,7 @@ impl Java { fn language_server_binary_path( &mut self, language_server_id: &LanguageServerId, + configuration: &Option, ) -> zed::Result { // Use cached path if exists @@ -89,7 +90,7 @@ impl Java { &LanguageServerInstallationStatus::CheckingForUpdate, ); - match try_to_fetch_and_install_latest_jdtls(language_server_id) { + match try_to_fetch_and_install_latest_jdtls(language_server_id, configuration) { Ok(path) => { self.cached_binary_path = Some(path.clone()); Ok(path) @@ -105,14 +106,27 @@ impl Java { } } - fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { + fn lombok_jar_path( + &mut self, + language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, + ) -> zed::Result { + // Use user-configured path if provided + if let Some(jar_path) = get_lombok_jar(configuration, worktree) { + let path = PathBuf::from(&jar_path); + self.cached_lombok_path = Some(path.clone()); + return Ok(path); + } + + // Use cached path if exists if let Some(path) = &self.cached_lombok_path && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } - match try_to_fetch_and_install_latest_lombok(language_server_id) { + match try_to_fetch_and_install_latest_lombok(language_server_id, configuration) { Ok(path) => { self.cached_lombok_path = Some(path.clone()); Ok(path) @@ -270,7 +284,8 @@ impl Extension for Java { // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true let lombok_jvm_arg = if is_lombok_enabled(&configuration) { - let lombok_jar_path = self.lombok_jar_path(language_server_id)?; + let lombok_jar_path = + self.lombok_jar_path(language_server_id, &configuration, worktree)?; let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; Some(format!("-javaagent:{canonical_lombok_jar_path}")) @@ -280,7 +295,13 @@ impl Extension for Java { self.init(worktree); - if let Some(launcher) = get_jdtls_launcher_from_path(worktree) { + // Check for user-configured JDTLS launcher first + if let Some(launcher) = get_jdtls_launcher(&configuration, worktree) { + args.push(launcher); + if let Some(lombok_jvm_arg) = lombok_jvm_arg { + args.push(format!("--jvm-arg={lombok_jvm_arg}")); + } + } else if let Some(launcher) = get_jdtls_launcher_from_path(worktree) { // if the user has `jdtls(.bat)` on their PATH, we use that args.push(launcher); if let Some(lombok_jvm_arg) = lombok_jvm_arg { @@ -289,7 +310,7 @@ impl Extension for Java { } else { // otherwise we launch ourselves args.extend(build_jdtls_launch_args( - &self.language_server_binary_path(language_server_id)?, + &self.language_server_binary_path(language_server_id, &configuration)?, &configuration, worktree, lombok_jvm_arg.into_iter().collect(), @@ -298,7 +319,10 @@ impl Extension for Java { } // download debugger if not exists - if let Err(err) = self.debugger()?.get_or_download(language_server_id) { + if let Err(err) = + self.debugger()? + .get_or_download(language_server_id, &configuration, worktree) + { println!("Failed to download debugger: {err}"); }; diff --git a/src/jdtls.rs b/src/jdtls.rs index e48f223..0971935 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -20,7 +20,7 @@ use crate::{ util::{ create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable, get_java_major_version, get_latest_versions_from_tag, path_to_string, - remove_all_files_except, + remove_all_files_except, should_use_local_or_download, }, }; @@ -153,7 +153,16 @@ pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { pub fn try_to_fetch_and_install_latest_jdtls( language_server_id: &LanguageServerId, + configuration: &Option, ) -> zed::Result { + // Use local installation if update mode requires it + if let Some(path) = + should_use_local_or_download(configuration, find_latest_local_jdtls(), "jdtls")? + { + return Ok(path); + } + + // Download latest version let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) @@ -201,7 +210,16 @@ pub fn try_to_fetch_and_install_latest_jdtls( pub fn try_to_fetch_and_install_latest_lombok( language_server_id: &LanguageServerId, + configuration: &Option, ) -> zed::Result { + // Use local installation if update mode requires it + if let Some(path) = + should_use_local_or_download(configuration, find_latest_local_lombok(), "lombok")? + { + return Ok(path); + } + + // Download latest version set_language_server_installation_status( language_server_id, &LanguageServerInstallationStatus::CheckingForUpdate, diff --git a/src/util.rs b/src/util.rs index b01bb2c..bb81883 100644 --- a/src/util.rs +++ b/src/util.rs @@ -11,7 +11,7 @@ use zed_extension_api::{ }; use crate::{ - config::{get_java_home, is_java_autodownload}, + config::{CheckUpdates, get_check_updates, get_java_home, is_java_autodownload}, jdk::try_to_fetch_and_install_latest_jdk, }; @@ -29,6 +29,8 @@ const TAG_RETRIEVAL_ERROR: &str = "Failed to fetch GitHub tags"; const TAG_RESPONSE_ERROR: &str = "Failed to deserialize GitHub tags response"; const TAG_UNEXPECTED_FORMAT_ERROR: &str = "Malformed GitHub tags response"; const PATH_IS_NOT_DIR: &str = "File exists but is not a path"; +const NO_LOCAL_INSTALL_NEVER_ERROR: &str = + "Update checks disabled (never) and no local installation found"; /// Create a Path if it does not exist /// @@ -298,3 +300,39 @@ pub fn remove_all_files_except>(prefix: P, filename: &str) -> zed Ok(()) } + +/// Determine whether to use local component or download based on update mode +/// +/// This function handles the common logic for all components (JDTLS, Lombok, Debugger): +/// 1. Apply update check mode (Never/Once/Always) +/// 2. Find local installation if applicable +/// +/// # Arguments +/// * `configuration` - User configuration JSON +/// * `local` - Optional path to local installation +/// * `component_name` - Component name for error messages (e.g., "jdtls", "lombok", "debugger") +/// +/// # Returns +/// * `Ok(Some(PathBuf))` - Local installation should be used +/// * `Ok(None)` - Should download +/// * `Err(String)` - Error message if resolution failed +/// +/// # Errors +/// - Update mode is Never but no local installation found +pub fn should_use_local_or_download( + configuration: &Option, + local: Option, + component_name: &str, +) -> zed::Result> { + match get_check_updates(configuration) { + CheckUpdates::Never => match local { + Some(path) => Ok(Some(path)), + None => Err(format!( + "{} for {}", + NO_LOCAL_INSTALL_NEVER_ERROR, component_name + )), + }, + CheckUpdates::Once => Ok(local), + CheckUpdates::Always => Ok(None), + } +} From cec444df52c84a0890591bffd4b467947e80c6ef Mon Sep 17 00:00:00 2001 From: Alejandro Cardona <114955358+acardonna@users.noreply.github.com> Date: Sat, 15 Nov 2025 09:43:01 -0500 Subject: [PATCH 53/93] Allow Java files to be run without requiring a build tool (#134) - do not require a package declaration for runnables matcher - compile and run with javac/java from $PATH if no build tool is found This PR aims to improve the experience of using Java in Zed, mainly for newcomers to both the language and/or the editor. With this, we make the experience closer to what you find in VSCode or IntelliJ IDEA when it comes to running a Java program. It's true that, at the moment, the extension offers convenient ways of running a Java program and running tests through extension-provided tasks that can either be selected from the tasks menu or by clicking the runnable buttons in the gutter. However, in order to use these convenient functionalities, the user needs to be using a build tool in their project, be it Maven or Gradle. Otherwise, they may find that they need to create custom tasks to obtain a similar experience or just use the JDK CLI. This is a rather a compromising assumption in my opinion, and it creates friction for people that are trying Zed or Java or both for the first time and might not be familiarized with build tools, Zed tasks or using the language CLI to run their program. Or are just a bit lazy (like me) and want a quick way to try something trivial without having to think about any of those things. The scope of these changes are limited to running a Java program, since this is the most common task for the kind of scenarios that I think benefit the main target audience of these changes. This means the test runnables remain unchanged. Only the `main` runnables are touched in this case. So, to summarize the changes: 1. Previously, a file with a main method was only runnable if it declared a package. Now, the `package_declaration` in the `main` runnable queries was made optional, allowing us to generate runnables for any number of files with a main method, even when a package is not declared. 2. Previously, you could only run a Java program if you were using a build tool (either Maven or Gradle). Now, we improve and modified the task to run a Java program so that it compiles the `.java` files with `javac`, put them in the `bin/` classpath, and finally run them with `java` as a fallback for the scenarios when no build tools are present. Finally, I tested this in projects using Maven, Gradle and no build tool at all. Everything worked as expected. Feel free to check it out yourselves though. Happy to hear any feeback! --- languages/java/runnables.scm | 4 ++-- languages/java/tasks.json | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/languages/java/runnables.scm b/languages/java/runnables.scm index d03b5dc..75f786e 100644 --- a/languages/java/runnables.scm +++ b/languages/java/runnables.scm @@ -2,7 +2,7 @@ ( (package_declaration (scoped_identifier) @java_package_name - ) + )? (class_declaration (modifiers) @class-modifier (#match? @class-modifier "public") @@ -24,7 +24,7 @@ ( (package_declaration (scoped_identifier) @java_package_name - ) + )? (class_declaration (modifiers) @class-modifier (#match? @class-modifier "public") diff --git a/languages/java/tasks.json b/languages/java/tasks.json index 05e8cdd..280bfc7 100644 --- a/languages/java/tasks.json +++ b/languages/java/tasks.json @@ -1,17 +1,17 @@ [ - { - "label": "Run $ZED_CUSTOM_java_class_name", - "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then ./mvnw clean compile exec:java -Dexec.mainClass=$c; elif [ -f gradlew ]; then ./gradlew run -PmainClass=$c; else >&2 echo 'No build system found'; exit 1; fi;", - "use_new_terminal": false, - "reveal": "always", - "tags": ["java-main"], - "shell": { - "with_arguments": { - "program": "/bin/sh", - "args": ["-c"] + { + "label": "Run $ZED_CUSTOM_java_class_name", + "command": "pkg=\"${ZED_CUSTOM_java_package_name:}\"; cls=\"$ZED_CUSTOM_java_class_name\"; if [ -n \"$pkg\" ]; then c=\"$pkg.$cls\"; else c=\"$cls\"; fi; if [ -f pom.xml ]; then ./mvnw clean compile exec:java -Dexec.mainClass=\"$c\"; elif [ -f gradlew ]; then ./gradlew run -PmainClass=\"$c\"; else find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java -cp bin \"$c\"; fi;", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-main"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } } - } - }, + }, { "label": "Test $ZED_CUSTOM_java_class_name.$ZED_CUSTOM_java_method_name", "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; m=\"$ZED_CUSTOM_java_method_name\"; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$c#$m\"; elif [ -f gradlew ]; then ./gradlew test --tests $c.$m; else >&2 echo 'No build system found'; exit 1; fi;", From b58e22f48d990a2bba774a41a7db250427e21e7e Mon Sep 17 00:00:00 2001 From: Alejandro Cardona <114955358+acardonna@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:23:37 -0500 Subject: [PATCH 54/93] Improve the outline (#136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds several changes to improve the current state of the outline for multiple Java pieces of code. This will not only be reflected in the outline panel, but also in the buffer symbols, which in my case, is what I prefer to navigate across all the symbols in the current file. At the beginning, my motivation was to solve an issue related to how the methods were appearing in the sticky scroll, but this led me a rabbit hole where I discovered that a lot more was missing in the outline, namely the following: Things that didn't appear in the outline before: 1. Constructors. 2. Any `class`, `interface`, `enum`, `record` and `annotation` (even if nested) that had no modifiers (access and non-access). 3. Fields and methods that had no modifiers (access and non-access). 4. Static blocks. 5. The types of the fields and the return type of methods. 6. Enum declaration. 7. Enum constants. 8. Annotation type declaration. 9. Annotation type elements. 10. The constant fields of an interface. 11. The parameters of a record constructor. And lastly, these changes have as consequence also the fixing of an issue related to the sticky scroll. The purpose of the sticky scroll is to give you context about the class and the method you are currently in, which can be very useful in multiple scenarios. However, there was an issue that when a method or class had an annotation, that annotation would appear in the sticky scroll instead of the whole method declaration, defeating the purpose of the sticky scroll. This feature was recently [introduced](https://github.com/zed-industries/zed/pull/42242#issue-3602402055) and can be enabled in `settings.json`: ```json "sticky_scroll": { "enabled": true } ``` **Limitations I found**: 1. Currently, there's always a space after a parameter type in the outline. Even before these changes, when a method had parameters, even though the parameter types were not shown, there was a space left inside the parenthesis. After trying to solve this with no success, I'm very inclined to believe this is probably an issue on Zed's side. 2. For records, the constructor parameters appear at the top level in the outline. To nest them under the record declaration, we need the entire `record_declaration` marked as `@item`, but marking the entire `record_declaration` as `@item` causes the sticky scroll to show the annotation (because it includes the entire text range starting with any annotation that is present). **Before**: https://github.com/user-attachments/assets/ba836bc7-3259-4e2b-b100-ce0e57c88b67 **Now**: https://github.com/user-attachments/assets/12604ee1-7631-4b75-a95f-a30aff87add8 --- languages/java/outline.scm | 150 ++++++++++++++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 9 deletions(-) diff --git a/languages/java/outline.scm b/languages/java/outline.scm index ffd036e..59a07e2 100644 --- a/languages/java/outline.scm +++ b/languages/java/outline.scm @@ -10,9 +10,10 @@ "final" "strictfp" "static" - ]* @context) + ]* @context)? "class" @context - name: (_) @name) @item + name: (_) @name + body: (_) @item) (record_declaration (modifiers @@ -26,9 +27,10 @@ "final" "strictfp" "static" - ]* @context) + ]* @context)? "record" @context - name: (_) @name) @item + name: (_) @name + body: (_) @item) (interface_declaration (modifiers @@ -41,9 +43,66 @@ "non-sealed" "strictfp" "static" - ]* @context) + ]* @context)? "interface" @context - name: (_) @name) @item + name: (_) @name + body: (_) @item) + +(enum_declaration + (modifiers + [ + "private" + "public" + "protected" + "static" + "final" + "strictfp" + ]* @context)? + "enum" @context + name: (_) @name + body: (_) @item) + +(annotation_type_declaration + (modifiers + [ + "private" + "public" + "protected" + "abstract" + "static" + "strictfp" + ]* @context)? + "@interface" @context + name: (_) @name + body: (_) @item) + +(enum_constant + name: (identifier) @name) @item + +(method_declaration + (modifiers + [ + "private" + "public" + "protected" + "abstract" + "static" + "final" + "native" + "strictfp" + "synchronized" + ]* @context)? + type: (_) @context + name: (_) @name + parameters: (formal_parameters + "(" @context + (formal_parameter + type: (_) @context)? + ("," @context + (formal_parameter + type: (_) @context)?)* + ")" @context) + body: (_) @item) (method_declaration (modifiers @@ -57,11 +116,62 @@ "native" "strictfp" "synchronized" - ]* @context) + ]* @context)? + type: (_) @context name: (_) @name parameters: (formal_parameters "(" @context - ")" @context)) @item + (formal_parameter + type: (_) @context)? + ("," @context + (formal_parameter + type: (_) @context)?)* + ")" @context) + ";" @item) + +(constructor_declaration + (modifiers + [ + "private" + "public" + "protected" + "static" + "final" + ]* @context)? + name: (_) @name + parameters: (formal_parameters + "(" @context + (formal_parameter + type: (_) @context)? + ("," @context + (formal_parameter + type: (_) @context)?)* + ")" @context) + body: (_) @item) + +(compact_constructor_declaration + (modifiers + [ + "private" + "public" + "protected" + ]* @context)? + name: (_) @name + body: (_) @item) + +(annotation_type_element_declaration + (modifiers + [ + "private" + "public" + "protected" + "abstract" + "static" + ]* @context)? + type: (_) @context + name: (_) @name + "(" @context + ")" @context) @item (field_declaration (modifiers @@ -73,6 +183,28 @@ "final" "transient" "volatile" - ]* @context) + ]* @context)? + type: (_) @context declarator: (variable_declarator name: (_) @name)) @item + +(constant_declaration + (modifiers + [ + "public" + "static" + "final" + ]* @context)? + type: (_) @context + declarator: (variable_declarator + name: (_) @name)) @item + +(static_initializer + "static" @context + (block) @item) + +(record_declaration + parameters: (formal_parameters + (formal_parameter + type: (_) @context + name: (_) @name) @item)) From 77b7debd109c526bc1007d31f628735b76c07bd3 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Thu, 27 Nov 2025 21:38:19 +0100 Subject: [PATCH 55/93] fix(lsp): ensure child process terminates when parent exits (#137) Address #41 Implemented a robust cleanup strategy for the spawned LSP process to prevent zombie processes: - Added `detached: false` to the spawn configuration - Implemented a `cleanup` function that attempts a graceful `SIGTERM`, followed by a forced `SIGKILL` after 1s if needed - Added a listener on `stdin` end for graceful shutdown - Added a watchdog interval to monitor the parent process (PPID); if the parent dies ungracefully, the LSP process will now self-terminate. --- src/proxy.mjs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/proxy.mjs b/src/proxy.mjs index f2c0250..55680cd 100644 --- a/src/proxy.mjs +++ b/src/proxy.mjs @@ -1,5 +1,5 @@ import { Buffer } from "node:buffer"; -import { spawn } from "node:child_process"; +import { spawn, exec } from "node:child_process"; import { EventEmitter } from "node:events"; import { existsSync, @@ -27,9 +27,59 @@ const args = process.argv.slice(3); const PROXY_ID = Buffer.from(process.cwd().replace(/\/+$/, "")).toString("hex"); const PROXY_HTTP_PORT_FILE = join(workdir, "proxy", PROXY_ID); -const command = process.platform === "win32" ? `"${bin}"` : bin; +const isWindows = process.platform === "win32"; +const command = isWindows ? `"${bin}"` : bin; + +const lsp = spawn(command, args, { + shell: isWindows, + detached: false +}); + +function cleanup() { + if (!lsp || lsp.killed || lsp.exitCode !== null) { + return; + } + + if (isWindows) { + // Windows: Use taskkill to kill the process tree (cmd.exe + the child) + // /T = Tree kill (child processes), /F = Force + exec(`taskkill /pid ${lsp.pid} /T /F`); + } + else { + lsp.kill('SIGTERM'); + setTimeout(() => { + if (!lsp.killed && lsp.exitCode === null) { + lsp.kill('SIGKILL'); + } + }, 1000); + } +} + +// Handle graceful IDE shutdown via stdin close +process.stdin.on('end', () => { + cleanup(); + process.exit(0); +}); +// Ensure node is monitoring the pipe +process.stdin.resume(); + +// Fallback: monitor parent process for ungraceful shutdown +setInterval(() => { + try { + // Check if parent is still alive + process.kill(process.ppid, 0); + } catch (e) { + // On Windows, checking a process you don't own might throw EPERM. + // We only want to kill if the error is ESRCH (No Such Process). + if (e.code === 'ESRCH') { + cleanup(); + process.exit(0); + } + // If e.code is EPERM, the parent is alive but we don't have permission to signal it. + // Do nothing. + } +}, 5000); -const lsp = spawn(command, args, { shell: process.platform === "win32" }); const proxy = createLspProxy({ server: lsp, proxy: process }); proxy.on("client", (data, passthrough) => { From 33ee24e2f8c58025d481a85a48c137e7520829d6 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:03:13 +0100 Subject: [PATCH 56/93] Add CI workflow for pull requests to main (#142) Adopt CI workflow as defined by Zed. Each PR will now ensure: - optimal format is used - clippy lint is applied --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8eda26d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,11 @@ +name: zed-tests +on: + pull_request: + branches: + - main +jobs: + run-extension-tests: + uses: zed-industries/zed/.github/workflows/extension_tests.yml@main + with: + # No tests implemented yet + run_tests: false From 010280f201e8b9c64fa7ebb0899a8dd3ba79965f Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:32:54 +0100 Subject: [PATCH 57/93] fix(util): improve remove_all_files_except function (#141) - Address inconsistent behavior of remove_all_files function by saving partial results in a vector before removing them - EXTRA: clippy --fix applied to project --- src/config.rs | 8 ++++---- src/debugger.rs | 6 ++---- src/jdtls.rs | 4 ++-- src/lsp.rs | 2 +- src/util.rs | 41 +++++++++++++++++++++++++---------------- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/config.rs b/src/config.rs index a2f45b6..9aa99e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,7 +21,7 @@ pub fn get_java_home(configuration: &Option, worktree: &Worktree) -> Opti match expand_home_path(worktree, java_home.to_string()) { Ok(home_path) => return Some(home_path), Err(err) => { - println!("{}", err); + println!("{err}"); } }; } @@ -86,7 +86,7 @@ pub fn get_jdtls_launcher(configuration: &Option, worktree: &Worktree) -> match expand_home_path(worktree, launcher_path.to_string()) { Ok(path) => return Some(path), Err(err) => { - println!("{}", err); + println!("{err}"); } } } @@ -103,7 +103,7 @@ pub fn get_lombok_jar(configuration: &Option, worktree: &Worktree) -> Opt match expand_home_path(worktree, jar_path.to_string()) { Ok(path) => return Some(path), Err(err) => { - println!("{}", err); + println!("{err}"); } } } @@ -120,7 +120,7 @@ pub fn get_java_debug_jar(configuration: &Option, worktree: &Worktree) -> match expand_home_path(worktree, jar_path.to_string()) { Ok(path) => return Some(path), Err(err) => { - println!("{}", err); + println!("{err}"); } } } diff --git a/src/debugger.rs b/src/debugger.rs index aac5135..d3af412 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -157,8 +157,7 @@ impl Debugger { ) .map_err(|err| { format!( - "Failed to download java-debug fork from {}: {err}", - JAVA_DEBUG_PLUGIN_FORK_URL + "Failed to download java-debug fork from {JAVA_DEBUG_PLUGIN_FORK_URL}: {err}" ) })?; @@ -198,8 +197,7 @@ impl Debugger { } println!( - "Could not fetch debugger: {}\nFalling back to local version.", - err + "Could not fetch debugger: {err}\nFalling back to local version." ); let exists = fs::read_dir(prefix) diff --git a/src/jdtls.rs b/src/jdtls.rs index 0971935..cfcd60a 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -283,7 +283,7 @@ fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result // else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar' let entries = - read_dir(&plugins_dir).map_err(|e| format!("Failed to read plugins directory: {}", e))?; + read_dir(&plugins_dir).map_err(|e| format!("Failed to read plugins directory: {e}"))?; entries .filter_map(Result::ok) @@ -325,7 +325,7 @@ fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { let cache_key = worktree.root_path(); let hex_digest = get_sha1_hex(&cache_key); - let unique_dir_name = format!("jdtls-{}", hex_digest); + let unique_dir_name = format!("jdtls-{hex_digest}"); Ok(base_cachedir.join(unique_dir_name)) } diff --git a/src/lsp.rs b/src/lsp.rs index 73a78da..de1fcbd 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -118,7 +118,7 @@ impl LspClient { fn string_to_hex(s: &str) -> String { let mut hex_string = String::new(); for byte in s.as_bytes() { - hex_string.push_str(&format!("{:02x}", byte)); + hex_string.push_str(&format!("{byte:02x}")); } hex_string } diff --git a/src/util.rs b/src/util.rs index bb81883..16178ec 100644 --- a/src/util.rs +++ b/src/util.rs @@ -20,7 +20,8 @@ const EXPAND_ERROR: &str = "Failed to expand ~"; const CURR_DIR_ERROR: &str = "Could not get current dir"; const DIR_ENTRY_LOAD_ERROR: &str = "Failed to load directory entry"; const DIR_ENTRY_RM_ERROR: &str = "Failed to remove directory entry"; -const DIR_ENTRY_LS_ERROR: &str = "Failed to list prefix directory"; +const ENTRY_TYPE_ERROR: &str = "Could not determine entry type"; +const FILE_ENTRY_RM_ERROR: &str = "Failed to remove file entry"; const PATH_TO_STR_ERROR: &str = "Failed to convert path to string"; const JAVA_EXEC_ERROR: &str = "Failed to convert Java executable path to string"; const JAVA_VERSION_ERROR: &str = "Failed to determine Java major version"; @@ -49,7 +50,7 @@ pub fn create_path_if_not_exists>(path: P) -> zed::Result<()> { if metadata.is_dir() { Ok(()) } else { - Err(format!("{PATH_IS_NOT_DIR}: {:?}", path_ref)) + Err(format!("{PATH_IS_NOT_DIR}: {path_ref:?}")) } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { @@ -280,22 +281,31 @@ pub fn path_to_string>(path: P) -> zed::Result { /// /// Returns `Ok(())` on success, even if some removals fail (errors are printed to stdout). pub fn remove_all_files_except>(prefix: P, filename: &str) -> zed::Result<()> { - match fs::read_dir(prefix) { - Ok(entries) => { - for entry in entries { - match entry { - Ok(entry) => { - if entry.file_name().to_str() != Some(filename) - && let Err(err) = fs::remove_dir_all(entry.path()) - { - println!("{msg}: {err}", msg = DIR_ENTRY_RM_ERROR, err = err); - } + let entries: Vec<_> = match fs::read_dir(prefix) { + Ok(entries) => entries.filter_map(|e| e.ok()).collect(), + Err(err) => { + println!("{DIR_ENTRY_LOAD_ERROR}: {err}"); + return Err(format!("{DIR_ENTRY_LOAD_ERROR}: {err}")); + } + }; + + for entry in entries { + if entry.file_name().to_str() != Some(filename) { + match entry.file_type() { + Ok(t) => { + if t.is_dir() + && let Err(err) = fs::remove_dir_all(entry.path()) + { + println!("{DIR_ENTRY_RM_ERROR}: {err}"); + } else if t.is_file() + && let Err(err) = fs::remove_file(entry.path()) + { + println!("{FILE_ENTRY_RM_ERROR}: {err}"); } - Err(err) => println!("{msg}: {err}", msg = DIR_ENTRY_LOAD_ERROR, err = err), } + Err(type_err) => println!("{ENTRY_TYPE_ERROR}: {type_err}"), } } - Err(err) => println!("{msg}: {err}", msg = DIR_ENTRY_LS_ERROR, err = err), } Ok(()) @@ -328,8 +338,7 @@ pub fn should_use_local_or_download( CheckUpdates::Never => match local { Some(path) => Ok(Some(path)), None => Err(format!( - "{} for {}", - NO_LOCAL_INSTALL_NEVER_ERROR, component_name + "{NO_LOCAL_INSTALL_NEVER_ERROR} for {component_name}" )), }, CheckUpdates::Once => Ok(local), From 7d23a439c7de7f160793ebc8210cfac301ee975c Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:41:17 +0100 Subject: [PATCH 58/93] Bump version to 6.8.0 (#143) --- Cargo.toml | 2 +- extension.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2e8706f..616d742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.7.1" +version = "6.8.0" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 88ea5ba..007f248 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.7.1" +version = "6.8.0" schema_version = 1 authors = [ "Valentine Briese ", From d390b5b679f90dc4c73d65e091a9d3962457d920 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 4 Dec 2025 21:32:57 +0100 Subject: [PATCH 59/93] Update to latest extension workflows (#144) Hey you two! This would be the first version for the new workflows that I just rolled out to the organization. Feedback is always welcome, feel free to take a look and give feedback! Brief overview: - tests on all PRs and main - automatic bump PR will be created if you merge something non-workflow related into main - on merge of that PR, the bot will open a PR against zed-industries/extensions Thanks! --- .github/workflows/bump_version.yml | 52 +++++++++++++++++++++++++++ .github/workflows/ci.yml | 11 ------ .github/workflows/release_version.yml | 13 +++++++ .github/workflows/run_tests.yml | 16 +++++++++ 4 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/bump_version.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release_version.yml create mode 100644 .github/workflows/run_tests.yml diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml new file mode 100644 index 0000000..7f4318d --- /dev/null +++ b/.github/workflows/bump_version.yml @@ -0,0 +1,52 @@ +# Generated from xtask::workflows::extensions::bump_version within the Zed repository. +# Rebuild with `cargo xtask workflows`. +name: extensions::bump_version +on: + pull_request: + types: + - labeled + push: + branches: + - main + paths-ignore: + - .github/** + workflow_dispatch: {} +jobs: + determine_bump_type: + runs-on: namespace-profile-16x32-ubuntu-2204 + steps: + - id: get-bump-type + name: extensions::bump_version::get_bump_type + run: | + if [ "$HAS_MAJOR_LABEL" = "true" ]; then + bump_type="major" + elif [ "$HAS_MINOR_LABEL" = "true" ]; then + bump_type="minor" + else + bump_type="patch" + fi + echo "bump_type=$bump_type" >> $GITHUB_OUTPUT + shell: bash -euxo pipefail {0} + env: + HAS_MAJOR_LABEL: |- + ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || + (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }} + HAS_MINOR_LABEL: |- + ${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || + (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }} + outputs: + bump_type: ${{ steps.get-bump-type.outputs.bump_type }} + call_bump_version: + needs: + - determine_bump_type + if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch' + uses: zed-industries/zed/.github/workflows/extension_bump.yml@main + secrets: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} + with: + bump-type: ${{ needs.determine_bump_type.outputs.bump_type }} + force-bump: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels + cancel-in-progress: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8eda26d..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: zed-tests -on: - pull_request: - branches: - - main -jobs: - run-extension-tests: - uses: zed-industries/zed/.github/workflows/extension_tests.yml@main - with: - # No tests implemented yet - run_tests: false diff --git a/.github/workflows/release_version.yml b/.github/workflows/release_version.yml new file mode 100644 index 0000000..f752931 --- /dev/null +++ b/.github/workflows/release_version.yml @@ -0,0 +1,13 @@ +# Generated from xtask::workflows::extensions::release_version within the Zed repository. +# Rebuild with `cargo xtask workflows`. +name: extensions::release_version +on: + push: + tags: + - v** +jobs: + call_release_version: + uses: zed-industries/zed/.github/workflows/extension_release.yml@main + secrets: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 0000000..81ba76c --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,16 @@ +# Generated from xtask::workflows::extensions::run_tests within the Zed repository. +# Rebuild with `cargo xtask workflows`. +name: extensions::run_tests +on: + pull_request: + branches: + - '**' + push: + branches: + - main +jobs: + call_extension_tests: + uses: zed-industries/zed/.github/workflows/extension_tests.yml@main +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}pr + cancel-in-progress: true From 13de31f60ebe110b3012c0711dfa9736820b9295 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 15 Dec 2025 23:23:15 +0100 Subject: [PATCH 60/93] ci: Remove `extensions::run_tests` workflow (#149) This PR removes the `run_tests.yml` workflow file, as it is no longer needed after moving the organization into the Zed GitHub enterprise. We now run the tests on pull requests with the workflow defined at https://github.com/zed-extensions/workflows and invoke this using an organization-wide policy for `zed-extensions`. Aside from the duplicate testing, having both files causes issues with concurrency, hence the removal of that file here. --- .github/workflows/run_tests.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/run_tests.yml diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml deleted file mode 100644 index 81ba76c..0000000 --- a/.github/workflows/run_tests.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Generated from xtask::workflows::extensions::run_tests within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::run_tests -on: - pull_request: - branches: - - '**' - push: - branches: - - main -jobs: - call_extension_tests: - uses: zed-industries/zed/.github/workflows/extension_tests.yml@main -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}pr - cancel-in-progress: true From 325407cc3f3aff67bd647542eb4724ba638224c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Dec 2025 16:29:06 -0800 Subject: [PATCH 61/93] Fix panic in label for completion (#152) Closes https://github.com/zed-industries/zed/issues/39328 --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- src/debugger.rs | 8 ++------ src/java.rs | 11 ++++++++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f30accc..043d4a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.7.1" +version = "6.8.1" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 616d742..a26c52a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.0" +version = "6.8.1" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 007f248..083d432 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.0" +version = "6.8.1" schema_version = 1 authors = [ "Valentine Briese ", diff --git a/src/debugger.rs b/src/debugger.rs index d3af412..e536aa3 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -156,9 +156,7 @@ impl Debugger { DownloadedFileType::Uncompressed, ) .map_err(|err| { - format!( - "Failed to download java-debug fork from {JAVA_DEBUG_PLUGIN_FORK_URL}: {err}" - ) + format!("Failed to download java-debug fork from {JAVA_DEBUG_PLUGIN_FORK_URL}: {err}") })?; self.plugin_path = Some(jar_path.clone()); @@ -196,9 +194,7 @@ impl Debugger { return Err(err.to_owned()); } - println!( - "Could not fetch debugger: {err}\nFalling back to local version." - ); + println!("Could not fetch debugger: {err}\nFalling back to local version."); let exists = fs::read_dir(prefix) .ok() diff --git a/src/java.rs b/src/java.rs index ff43cbe..5adc09d 100644 --- a/src/java.rs +++ b/src/java.rs @@ -448,9 +448,14 @@ impl Extension for Java { }; let braces = " {}"; let code = format!("{keyword}{}{braces}", completion.label); - let namespace = completion - .detail - .map(|detail| detail[..detail.len() - completion.label.len() - 1].to_string()); + let namespace = completion.detail.and_then(|detail| { + if detail.len() > completion.label.len() { + let prefix_len = detail.len() - completion.label.len() - 1; + Some(detail[..prefix_len].to_string()) + } else { + None + } + }); let mut spans = vec![CodeLabelSpan::code_range( keyword.len()..code.len() - braces.len(), )]; From a540253dff02ac311c4b84ed2d128ceff4c0b226 Mon Sep 17 00:00:00 2001 From: Uriel Curiel Date: Sat, 20 Dec 2025 04:43:31 -0600 Subject: [PATCH 62/93] fix(windows): quote paths in JDTLS and Lombok arguments (#151) --- src/debugger.rs | 9 ++++++--- src/java.rs | 7 ++++--- src/jdk.rs | 4 ++-- src/jdtls.rs | 16 ++++++++-------- src/util.rs | 37 ++++++++++++++++++++++++++++++++----- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index e536aa3..67371df 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -12,7 +12,10 @@ use zed_extension_api::{ use crate::{ config::get_java_debug_jar, lsp::LspWrapper, - util::{create_path_if_not_exists, get_curr_dir, path_to_string, should_use_local_or_download}, + util::{ + create_path_if_not_exists, get_curr_dir, path_to_quoted_string, + should_use_local_or_download, + }, }; #[derive(Serialize, Deserialize, Debug)] @@ -152,7 +155,7 @@ impl Debugger { download_file( JAVA_DEBUG_PLUGIN_FORK_URL, - &path_to_string(jar_path.clone())?, + &path_to_quoted_string(jar_path.clone())?, DownloadedFileType::Uncompressed, ) .map_err(|err| { @@ -256,7 +259,7 @@ impl Debugger { download_file( url.as_str(), - &path_to_string(&jar_path)?, + &path_to_quoted_string(&jar_path)?, DownloadedFileType::Uncompressed, ) .map_err(|err| format!("Failed to download {url} {err}"))?; diff --git a/src/java.rs b/src/java.rs index 5adc09d..4a22ee8 100644 --- a/src/java.rs +++ b/src/java.rs @@ -32,7 +32,7 @@ use crate::{ try_to_fetch_and_install_latest_lombok, }, lsp::LspWrapper, - util::path_to_string, + util::path_to_quoted_string, }; const PROXY_FILE: &str = include_str!("proxy.mjs"); @@ -279,14 +279,15 @@ impl Extension for Java { "--input-type=module".to_string(), "-e".to_string(), PROXY_FILE.to_string(), - path_to_string(current_dir.clone())?, + path_to_quoted_string(current_dir.clone())?, ]; // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true let lombok_jvm_arg = if is_lombok_enabled(&configuration) { let lombok_jar_path = self.lombok_jar_path(language_server_id, &configuration, worktree)?; - let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; + let canonical_lombok_jar_path = + path_to_quoted_string(current_dir.join(lombok_jar_path))?; Some(format!("-javaagent:{canonical_lombok_jar_path}")) } else { diff --git a/src/jdk.rs b/src/jdk.rs index 0e128c3..aab5f47 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -6,7 +6,7 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::util::{get_curr_dir, path_to_string, remove_all_files_except}; +use crate::util::{get_curr_dir, path_to_quoted_string, remove_all_files_except}; // Errors const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory"; @@ -79,7 +79,7 @@ pub fn try_to_fetch_and_install_latest_jdk( download_file( build_corretto_url(&version, &platform, &arch).as_str(), - path_to_string(install_path.clone())?.as_str(), + path_to_quoted_string(install_path.clone())?.as_str(), match zed::current_platform().0 { Os::Windows => DownloadedFileType::Zip, _ => DownloadedFileType::GzipTar, diff --git a/src/jdtls.rs b/src/jdtls.rs index cfcd60a..da915fb 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -19,7 +19,7 @@ use crate::{ jdk::try_to_fetch_and_install_latest_jdk, util::{ create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable, - get_java_major_version, get_latest_versions_from_tag, path_to_string, + get_java_major_version, get_latest_versions_from_tag, path_to_quoted_string, remove_all_files_except, should_use_local_or_download, }, }; @@ -65,14 +65,14 @@ pub fn build_jdtls_launch_args( let jdtls_data_path = get_jdtls_data_path(worktree)?; let mut args = vec![ - path_to_string(java_executable)?, + path_to_quoted_string(java_executable)?, "-Declipse.application=org.eclipse.jdt.ls.core.id1".to_string(), "-Dosgi.bundles.defaultStartLevel=4".to_string(), "-Declipse.product=org.eclipse.jdt.ls.core.product".to_string(), "-Dosgi.checkConfiguration=true".to_string(), format!( "-Dosgi.sharedConfiguration.area={}", - path_to_string(shared_config_path)? + path_to_quoted_string(shared_config_path)? ), "-Dosgi.sharedConfiguration.area.readOnly=true".to_string(), "-Dosgi.configuration.cascaded=true".to_string(), @@ -86,9 +86,9 @@ pub fn build_jdtls_launch_args( args.extend(jvm_args); args.extend(vec![ "-jar".to_string(), - path_to_string(jar_path)?, + path_to_quoted_string(jar_path)?, "-data".to_string(), - path_to_string(jdtls_data_path)?, + path_to_quoted_string(jdtls_data_path)?, ]); if java_major_version >= 24 { args.push("-Djdk.xml.maxGeneralEntitySizeLimit=0".to_string()); @@ -195,10 +195,10 @@ pub fn try_to_fetch_and_install_latest_jdtls( &format!( "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" ), - path_to_string(build_path.clone())?.as_str(), + path_to_quoted_string(build_path.clone())?.as_str(), DownloadedFileType::GzipTar, )?; - make_file_executable(path_to_string(binary_path)?.as_str())?; + make_file_executable(path_to_quoted_string(binary_path)?.as_str())?; // ...and delete other versions let _ = remove_all_files_except(prefix, build_directory.as_str()); @@ -241,7 +241,7 @@ pub fn try_to_fetch_and_install_latest_lombok( create_path_if_not_exists(prefix)?; download_file( &format!("https://projectlombok.org/downloads/{jar_name}"), - path_to_string(jar_path.clone())?.as_str(), + path_to_quoted_string(jar_path.clone())?.as_str(), DownloadedFileType::Uncompressed, )?; diff --git a/src/util.rs b/src/util.rs index 16178ec..9e1b92b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -176,7 +176,8 @@ pub fn get_java_exec_name() -> String { /// * [`java_executable`] can't be converted into a String /// * No major version can be determined pub fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { - let program = path_to_string(java_executable).map_err(|_| JAVA_EXEC_ERROR.to_string())?; + let program = + path_to_quoted_string(java_executable).map_err(|_| JAVA_EXEC_ERROR.to_string())?; let output_bytes = Command::new(program).arg("-version").output()?.stderr; let output = String::from_utf8(output_bytes).map_err(|e| e.to_string())?; @@ -246,24 +247,28 @@ fn get_tag_at(github_tags: &Value, index: usize) -> Option<&str> { }) } -/// Convert [`path`] into [`String`] +/// Converts a [`Path`] into a double-quoted [`String`]. +/// +/// This function performs two steps in one: it converts the path to a string +/// and wraps the result in double quotes (e.g., `"path/to/file"`). /// /// # Arguments /// -/// * [`path`] the path of type [`AsRef`] to convert +/// * `path` - The path of type `AsRef` to be converted and quoted. /// /// # Returns /// -/// Returns a String representing [`path`] +/// Returns a `String` representing the `path` enclosed in double quotes. /// /// # Errors /// /// This function will return an error when the string conversion fails -pub fn path_to_string>(path: P) -> zed::Result { +pub fn path_to_quoted_string>(path: P) -> zed::Result { path.as_ref() .to_path_buf() .into_os_string() .into_string() + .map(|s| format!("\"{}\"", s)) .map_err(|_| PATH_TO_STR_ERROR.to_string()) } @@ -345,3 +350,25 @@ pub fn should_use_local_or_download( CheckUpdates::Always => Ok(None), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_to_quoted_string_windows() { + let path = PathBuf::from("C:\\Users\\User Name\\Projects\\zed-extension-java"); + let escaped = path_to_quoted_string(&path).unwrap(); + assert_eq!( + escaped, + "\"C:\\Users\\User Name\\Projects\\zed-extension-java\"" + ); + } + + #[test] + fn test_path_to_quoted_string_unix() { + let path = PathBuf::from("/home/username/Projects/zed extension java"); + let escaped = path_to_quoted_string(&path).unwrap(); + assert_eq!(escaped, "\"/home/username/Projects/zed extension java\""); + } +} From c8f8292f9289972ebfe0ed50bbedb235e95f6776 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:06:03 +0100 Subject: [PATCH 63/93] Bump version to 6.8.2 (#153) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 043d4a9..02d0b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.1" +version = "6.8.2" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index a26c52a..1eeaaf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.1" +version = "6.8.2" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 083d432..a285bdf 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.1" +version = "6.8.2" schema_version = 1 authors = [ "Valentine Briese ", From a89b5d8297a817ce64122e6a02852c41f3ff52b7 Mon Sep 17 00:00:00 2001 From: Jackson Kringel Date: Sun, 21 Dec 2025 10:09:16 -0500 Subject: [PATCH 64/93] fix: only quote paths on Windows to prevent macOS/Linux spawn() errors (#156) ## Summary Fixes a regression introduced in #151 where path quoting broke the Java extension on macOS and Linux. Closes #155 ## Problem PR #151 added double-quote wrapping to paths to fix spaces-in-path issues on Windows. However, this broke macOS and Linux because: - On Windows, Zed's proxy uses shell mode, where quotes are interpreted and stripped - On macOS/Linux, the proxy uses `spawn()` with `shell: false`, which treats quotes as **literal filename characters** This caused "No such file or directory (os error 2)" errors on macOS/Linux because the system was looking for files like `"/path/to/java"` (with literal quotes in the filename). ## Solution - Only apply path quoting on Windows via runtime `current_platform()` check - Extract `format_path_for_os()` helper function for testability - Add unit tests covering Windows, Mac, and Linux behavior --- src/util.rs | 55 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/util.rs b/src/util.rs index 9e1b92b..ffb573c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -247,29 +247,45 @@ fn get_tag_at(github_tags: &Value, index: usize) -> Option<&str> { }) } -/// Converts a [`Path`] into a double-quoted [`String`]. +/// Formats a path string with platform-specific quoting. /// -/// This function performs two steps in one: it converts the path to a string -/// and wraps the result in double quotes (e.g., `"path/to/file"`). +/// On Windows, wraps the path in double quotes for shell mode compatibility. +/// On Unix, returns the path unquoted since spawn() treats quotes as literals. +fn format_path_for_os(path_str: String, os: Os) -> String { + if os == Os::Windows { + format!("\"{}\"", path_str) + } else { + path_str + } +} + +/// Converts a [`Path`] into a [`String`], with platform-specific quoting. +/// +/// On Windows, the path is wrapped in double quotes (e.g., `"C:\path\to\file"`) +/// for compatibility with shell mode. On Unix-like systems, the path is returned +/// unquoted, as the proxy uses `spawn()` with `shell: false` which treats quotes +/// as literal filename characters, causing "No such file or directory" errors. /// /// # Arguments /// -/// * `path` - The path of type `AsRef` to be converted and quoted. +/// * `path` - The path of type `AsRef` to be converted. /// /// # Returns /// -/// Returns a `String` representing the `path` enclosed in double quotes. +/// Returns a `String` representing the path, quoted on Windows, unquoted on Unix. /// /// # Errors /// /// This function will return an error when the string conversion fails pub fn path_to_quoted_string>(path: P) -> zed::Result { - path.as_ref() + let path_str = path + .as_ref() .to_path_buf() .into_os_string() .into_string() - .map(|s| format!("\"{}\"", s)) - .map_err(|_| PATH_TO_STR_ERROR.to_string()) + .map_err(|_| PATH_TO_STR_ERROR.to_string())?; + + Ok(format_path_for_os(path_str, current_platform().0)) } /// Remove all files or directories that aren't equal to [`filename`]. @@ -356,19 +372,26 @@ mod tests { use super::*; #[test] - fn test_path_to_quoted_string_windows() { - let path = PathBuf::from("C:\\Users\\User Name\\Projects\\zed-extension-java"); - let escaped = path_to_quoted_string(&path).unwrap(); + fn test_format_path_for_os_windows() { + let path = "C:\\Users\\User Name\\Projects\\zed-extension-java".to_string(); + let result = format_path_for_os(path, Os::Windows); assert_eq!( - escaped, + result, "\"C:\\Users\\User Name\\Projects\\zed-extension-java\"" ); } #[test] - fn test_path_to_quoted_string_unix() { - let path = PathBuf::from("/home/username/Projects/zed extension java"); - let escaped = path_to_quoted_string(&path).unwrap(); - assert_eq!(escaped, "\"/home/username/Projects/zed extension java\""); + fn test_format_path_for_os_unix() { + let path = "/home/username/Projects/zed extension java".to_string(); + let result = format_path_for_os(path, Os::Mac); + assert_eq!(result, "/home/username/Projects/zed extension java"); + } + + #[test] + fn test_format_path_for_os_linux() { + let path = "/home/username/Projects/zed extension java".to_string(); + let result = format_path_for_os(path, Os::Linux); + assert_eq!(result, "/home/username/Projects/zed extension java"); } } From 9929ca374dad327b24da558cc51d74e8fdf7dbc7 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:20:43 +0100 Subject: [PATCH 65/93] Bump version to 6.8.3 (#157) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02d0b8e..06f913a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.2" +version = "6.8.3" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 1eeaaf5..023518b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.2" +version = "6.8.3" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index a285bdf..a3f8248 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.2" +version = "6.8.3" schema_version = 1 authors = [ "Valentine Briese ", From 11d843a433c99e5c68b998077c6b7d3410283691 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:57:16 +0100 Subject: [PATCH 66/93] Revert quoting all paths on Windows (#161) - Remove Windows specific path quoting - Spawn JDTLS in shell mode only when a .bat file is provided, improving handling of paths with spaces - Paths with spaces are still a problem when users provide their own .bat file as the python instance invoked by the .bat is not able to understand how to treat the space included in the arg. --- src/debugger.rs | 9 +++---- src/java.rs | 9 ++++--- src/jdk.rs | 4 ++-- src/jdtls.rs | 16 ++++++------- src/proxy.mjs | 17 +++++++------ src/util.rs | 64 ++++++------------------------------------------- 6 files changed, 32 insertions(+), 87 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 67371df..e536aa3 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -12,10 +12,7 @@ use zed_extension_api::{ use crate::{ config::get_java_debug_jar, lsp::LspWrapper, - util::{ - create_path_if_not_exists, get_curr_dir, path_to_quoted_string, - should_use_local_or_download, - }, + util::{create_path_if_not_exists, get_curr_dir, path_to_string, should_use_local_or_download}, }; #[derive(Serialize, Deserialize, Debug)] @@ -155,7 +152,7 @@ impl Debugger { download_file( JAVA_DEBUG_PLUGIN_FORK_URL, - &path_to_quoted_string(jar_path.clone())?, + &path_to_string(jar_path.clone())?, DownloadedFileType::Uncompressed, ) .map_err(|err| { @@ -259,7 +256,7 @@ impl Debugger { download_file( url.as_str(), - &path_to_quoted_string(&jar_path)?, + &path_to_string(&jar_path)?, DownloadedFileType::Uncompressed, ) .map_err(|err| format!("Failed to download {url} {err}"))?; diff --git a/src/java.rs b/src/java.rs index 4a22ee8..2beb223 100644 --- a/src/java.rs +++ b/src/java.rs @@ -32,7 +32,7 @@ use crate::{ try_to_fetch_and_install_latest_lombok, }, lsp::LspWrapper, - util::path_to_quoted_string, + util::path_to_string, }; const PROXY_FILE: &str = include_str!("proxy.mjs"); @@ -279,17 +279,16 @@ impl Extension for Java { "--input-type=module".to_string(), "-e".to_string(), PROXY_FILE.to_string(), - path_to_quoted_string(current_dir.clone())?, + path_to_string(current_dir.clone())?, ]; // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true let lombok_jvm_arg = if is_lombok_enabled(&configuration) { let lombok_jar_path = self.lombok_jar_path(language_server_id, &configuration, worktree)?; - let canonical_lombok_jar_path = - path_to_quoted_string(current_dir.join(lombok_jar_path))?; + let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; - Some(format!("-javaagent:{canonical_lombok_jar_path}")) + Some(format!("-javaagent:{}", canonical_lombok_jar_path)) } else { None }; diff --git a/src/jdk.rs b/src/jdk.rs index aab5f47..0e128c3 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -6,7 +6,7 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::util::{get_curr_dir, path_to_quoted_string, remove_all_files_except}; +use crate::util::{get_curr_dir, path_to_string, remove_all_files_except}; // Errors const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory"; @@ -79,7 +79,7 @@ pub fn try_to_fetch_and_install_latest_jdk( download_file( build_corretto_url(&version, &platform, &arch).as_str(), - path_to_quoted_string(install_path.clone())?.as_str(), + path_to_string(install_path.clone())?.as_str(), match zed::current_platform().0 { Os::Windows => DownloadedFileType::Zip, _ => DownloadedFileType::GzipTar, diff --git a/src/jdtls.rs b/src/jdtls.rs index da915fb..cfcd60a 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -19,7 +19,7 @@ use crate::{ jdk::try_to_fetch_and_install_latest_jdk, util::{ create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable, - get_java_major_version, get_latest_versions_from_tag, path_to_quoted_string, + get_java_major_version, get_latest_versions_from_tag, path_to_string, remove_all_files_except, should_use_local_or_download, }, }; @@ -65,14 +65,14 @@ pub fn build_jdtls_launch_args( let jdtls_data_path = get_jdtls_data_path(worktree)?; let mut args = vec![ - path_to_quoted_string(java_executable)?, + path_to_string(java_executable)?, "-Declipse.application=org.eclipse.jdt.ls.core.id1".to_string(), "-Dosgi.bundles.defaultStartLevel=4".to_string(), "-Declipse.product=org.eclipse.jdt.ls.core.product".to_string(), "-Dosgi.checkConfiguration=true".to_string(), format!( "-Dosgi.sharedConfiguration.area={}", - path_to_quoted_string(shared_config_path)? + path_to_string(shared_config_path)? ), "-Dosgi.sharedConfiguration.area.readOnly=true".to_string(), "-Dosgi.configuration.cascaded=true".to_string(), @@ -86,9 +86,9 @@ pub fn build_jdtls_launch_args( args.extend(jvm_args); args.extend(vec![ "-jar".to_string(), - path_to_quoted_string(jar_path)?, + path_to_string(jar_path)?, "-data".to_string(), - path_to_quoted_string(jdtls_data_path)?, + path_to_string(jdtls_data_path)?, ]); if java_major_version >= 24 { args.push("-Djdk.xml.maxGeneralEntitySizeLimit=0".to_string()); @@ -195,10 +195,10 @@ pub fn try_to_fetch_and_install_latest_jdtls( &format!( "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" ), - path_to_quoted_string(build_path.clone())?.as_str(), + path_to_string(build_path.clone())?.as_str(), DownloadedFileType::GzipTar, )?; - make_file_executable(path_to_quoted_string(binary_path)?.as_str())?; + make_file_executable(path_to_string(binary_path)?.as_str())?; // ...and delete other versions let _ = remove_all_files_except(prefix, build_directory.as_str()); @@ -241,7 +241,7 @@ pub fn try_to_fetch_and_install_latest_lombok( create_path_if_not_exists(prefix)?; download_file( &format!("https://projectlombok.org/downloads/{jar_name}"), - path_to_quoted_string(jar_path.clone())?.as_str(), + path_to_string(jar_path.clone())?.as_str(), DownloadedFileType::Uncompressed, )?; diff --git a/src/proxy.mjs b/src/proxy.mjs index 55680cd..9c6333a 100644 --- a/src/proxy.mjs +++ b/src/proxy.mjs @@ -28,11 +28,11 @@ const args = process.argv.slice(3); const PROXY_ID = Buffer.from(process.cwd().replace(/\/+$/, "")).toString("hex"); const PROXY_HTTP_PORT_FILE = join(workdir, "proxy", PROXY_ID); const isWindows = process.platform === "win32"; -const command = isWindows ? `"${bin}"` : bin; +const command = (isWindows && bin.endsWith(".bat")) ? `"${bin}"` : bin; const lsp = spawn(command, args, { - shell: isWindows, - detached: false + shell: (isWindows && bin.endsWith(".bat")), + detached: false, }); function cleanup() { @@ -44,19 +44,18 @@ function cleanup() { // Windows: Use taskkill to kill the process tree (cmd.exe + the child) // /T = Tree kill (child processes), /F = Force exec(`taskkill /pid ${lsp.pid} /T /F`); - } - else { - lsp.kill('SIGTERM'); + } else { + lsp.kill("SIGTERM"); setTimeout(() => { if (!lsp.killed && lsp.exitCode === null) { - lsp.kill('SIGKILL'); + lsp.kill("SIGKILL"); } }, 1000); } } // Handle graceful IDE shutdown via stdin close -process.stdin.on('end', () => { +process.stdin.on("end", () => { cleanup(); process.exit(0); }); @@ -71,7 +70,7 @@ setInterval(() => { } catch (e) { // On Windows, checking a process you don't own might throw EPERM. // We only want to kill if the error is ESRCH (No Such Process). - if (e.code === 'ESRCH') { + if (e.code === "ESRCH") { cleanup(); process.exit(0); } diff --git a/src/util.rs b/src/util.rs index ffb573c..17795e9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -176,8 +176,7 @@ pub fn get_java_exec_name() -> String { /// * [`java_executable`] can't be converted into a String /// * No major version can be determined pub fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { - let program = - path_to_quoted_string(java_executable).map_err(|_| JAVA_EXEC_ERROR.to_string())?; + let program = path_to_string(java_executable).map_err(|_| JAVA_EXEC_ERROR.to_string())?; let output_bytes = Command::new(program).arg("-version").output()?.stderr; let output = String::from_utf8(output_bytes).map_err(|e| e.to_string())?; @@ -247,24 +246,7 @@ fn get_tag_at(github_tags: &Value, index: usize) -> Option<&str> { }) } -/// Formats a path string with platform-specific quoting. -/// -/// On Windows, wraps the path in double quotes for shell mode compatibility. -/// On Unix, returns the path unquoted since spawn() treats quotes as literals. -fn format_path_for_os(path_str: String, os: Os) -> String { - if os == Os::Windows { - format!("\"{}\"", path_str) - } else { - path_str - } -} - -/// Converts a [`Path`] into a [`String`], with platform-specific quoting. -/// -/// On Windows, the path is wrapped in double quotes (e.g., `"C:\path\to\file"`) -/// for compatibility with shell mode. On Unix-like systems, the path is returned -/// unquoted, as the proxy uses `spawn()` with `shell: false` which treats quotes -/// as literal filename characters, causing "No such file or directory" errors. +/// Converts a [`Path`] into a [`String`]. /// /// # Arguments /// @@ -272,20 +254,17 @@ fn format_path_for_os(path_str: String, os: Os) -> String { /// /// # Returns /// -/// Returns a `String` representing the path, quoted on Windows, unquoted on Unix. +/// Returns a `String` representing the path. /// /// # Errors /// -/// This function will return an error when the string conversion fails -pub fn path_to_quoted_string>(path: P) -> zed::Result { - let path_str = path - .as_ref() +/// This function will return an error when the string conversion fails. +pub fn path_to_string>(path: P) -> zed::Result { + path.as_ref() .to_path_buf() .into_os_string() .into_string() - .map_err(|_| PATH_TO_STR_ERROR.to_string())?; - - Ok(format_path_for_os(path_str, current_platform().0)) + .map_err(|_| PATH_TO_STR_ERROR.to_string()) } /// Remove all files or directories that aren't equal to [`filename`]. @@ -366,32 +345,3 @@ pub fn should_use_local_or_download( CheckUpdates::Always => Ok(None), } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_path_for_os_windows() { - let path = "C:\\Users\\User Name\\Projects\\zed-extension-java".to_string(); - let result = format_path_for_os(path, Os::Windows); - assert_eq!( - result, - "\"C:\\Users\\User Name\\Projects\\zed-extension-java\"" - ); - } - - #[test] - fn test_format_path_for_os_unix() { - let path = "/home/username/Projects/zed extension java".to_string(); - let result = format_path_for_os(path, Os::Mac); - assert_eq!(result, "/home/username/Projects/zed extension java"); - } - - #[test] - fn test_format_path_for_os_linux() { - let path = "/home/username/Projects/zed extension java".to_string(); - let result = format_path_for_os(path, Os::Linux); - assert_eq!(result, "/home/username/Projects/zed extension java"); - } -} From 19aea3afb37cf2e007ed6825af91634d4ca2d122 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sat, 27 Dec 2025 12:39:29 +0100 Subject: [PATCH 67/93] fix(update): Implement proper "once" behavior for check_updates setting (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When check_updates is set to "once", the extension now correctly checks for updates only once per component, preventing repeated GitHub API calls on every Zed restart. Previously, the "once" mode would check GitHub API on every restart when no local installation existed, making it functionally identical to "always" mode until a download completed. Changes: - Add persistence mechanism using .update_checked marker files - Store downloaded version in marker files for future reference - Implement has_checked_once() and mark_checked_once() helpers - Update should_use_local_or_download() to check marker before allowing download - Apply fix to all components: JDTLS, Lombok, Debugger, and JDK Behavior after fix: - First run: GitHub API called → Download → Create marker with version - Subsequent runs: No GitHub API call (uses local or returns error) Marker files created: - jdtls/.update_checked - lombok/.update_checked - debugger/.update_checked - jdk/.update_checked --- src/debugger.rs | 19 ++++++++++++---- src/jdk.rs | 60 +++++++++++++++++++++++++++++++++++++++---------- src/jdtls.rs | 26 ++++++++++++++++----- src/util.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 141 insertions(+), 24 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index e536aa3..4b46492 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -12,7 +12,10 @@ use zed_extension_api::{ use crate::{ config::get_java_debug_jar, lsp::LspWrapper, - util::{create_path_if_not_exists, get_curr_dir, path_to_string, should_use_local_or_download}, + util::{ + create_path_if_not_exists, get_curr_dir, mark_checked_once, path_to_string, + should_use_local_or_download, + }, }; #[derive(Serialize, Deserialize, Debug)] @@ -121,9 +124,11 @@ impl Debugger { } // Use local installation if update mode requires it - if let Some(path) = - should_use_local_or_download(configuration, find_latest_local_debugger(), "debugger")? - { + if let Some(path) = should_use_local_or_download( + configuration, + find_latest_local_debugger(), + DEBUGGER_INSTALL_PATH, + )? { self.plugin_path = Some(path.clone()); return Ok(path); } @@ -159,6 +164,9 @@ impl Debugger { format!("Failed to download java-debug fork from {JAVA_DEBUG_PLUGIN_FORK_URL}: {err}") })?; + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(DEBUGGER_INSTALL_PATH, latest_version); + self.plugin_path = Some(jar_path.clone()); Ok(jar_path) } @@ -260,6 +268,9 @@ impl Debugger { DownloadedFileType::Uncompressed, ) .map_err(|err| format!("Failed to download {url} {err}"))?; + + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(DEBUGGER_INSTALL_PATH, latest_version); } self.plugin_path = Some(jar_path.clone()); diff --git a/src/jdk.rs b/src/jdk.rs index 0e128c3..bb0dbb4 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -2,11 +2,14 @@ use std::path::{Path, PathBuf}; use zed_extension_api::{ self as zed, Architecture, DownloadedFileType, LanguageServerId, - LanguageServerInstallationStatus, Os, current_platform, download_file, + LanguageServerInstallationStatus, Os, current_platform, download_file, serde_json::Value, set_language_server_installation_status, }; -use crate::util::{get_curr_dir, path_to_string, remove_all_files_except}; +use crate::util::{ + get_curr_dir, mark_checked_once, path_to_string, remove_all_files_except, + should_use_local_or_download, +}; // Errors const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory"; @@ -15,6 +18,7 @@ const NO_JDK_DIR_ERROR: &str = "No match for jdk or corretto in the extracted di const CORRETTO_REPO: &str = "corretto/corretto-25"; const CORRETTO_UNIX_URL_TEMPLATE: &str = "https://corretto.aws/downloads/resources/{version}/amazon-corretto-{version}-{platform}-{arch}.tar.gz"; const CORRETTO_WINDOWS_URL_TEMPLATE: &str = "https://corretto.aws/downloads/resources/{version}/amazon-corretto-{version}-{platform}-{arch}-jdk.zip"; +const JDK_INSTALL_PATH: &str = "jdk"; fn build_corretto_url(version: &str, platform: &str, arch: &str) -> String { let template = match zed::current_platform().0 { @@ -46,9 +50,42 @@ fn get_platform() -> zed::Result { } } +fn find_latest_local_jdk() -> Option { + let jdk_path = get_curr_dir().ok()?.join(JDK_INSTALL_PATH); + std::fs::read_dir(&jdk_path) + .ok()? + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .filter_map(|path| { + let created_time = std::fs::metadata(&path) + .and_then(|meta| meta.created()) + .ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + .map(|(path, _)| path) +} + pub fn try_to_fetch_and_install_latest_jdk( language_server_id: &LanguageServerId, + configuration: &Option, ) -> zed::Result { + let jdk_path = get_curr_dir()?.join(JDK_INSTALL_PATH); + + // Check if we should use local installation based on update mode + if let Some(path) = + should_use_local_or_download(configuration, find_latest_local_jdk(), JDK_INSTALL_PATH)? + { + return get_jdk_bin_path(&path); + } + + // Check for updates, if same version is already downloaded skip download + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + let version = zed::latest_github_release( CORRETTO_REPO, zed_extension_api::GithubReleaseOptions { @@ -58,16 +95,8 @@ pub fn try_to_fetch_and_install_latest_jdk( )? .version; - let jdk_path = get_curr_dir()?.join("jdk"); let install_path = jdk_path.join(&version); - // Check for updates, if same version is already downloaded skip download - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - if !install_path.exists() { set_language_server_installation_status( language_server_id, @@ -87,12 +116,19 @@ pub fn try_to_fetch_and_install_latest_jdk( )?; // Remove older versions - let _ = remove_all_files_except(jdk_path, version.as_str()); + let _ = remove_all_files_except(&jdk_path, version.as_str()); + + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(JDK_INSTALL_PATH, &version); } + get_jdk_bin_path(&install_path) +} + +fn get_jdk_bin_path(install_path: &Path) -> zed::Result { // Depending on the platform the name of the extracted dir might differ // Rather than hard coding, extract it dynamically - let extracted_dir = get_extracted_dir(&install_path)?; + let extracted_dir = get_extracted_dir(install_path)?; Ok(install_path .join(extracted_dir) diff --git a/src/jdtls.rs b/src/jdtls.rs index cfcd60a..2c77c5f 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -19,7 +19,7 @@ use crate::{ jdk::try_to_fetch_and_install_latest_jdk, util::{ create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable, - get_java_major_version, get_latest_versions_from_tag, path_to_string, + get_java_major_version, get_latest_versions_from_tag, mark_checked_once, path_to_string, remove_all_files_except, should_use_local_or_download, }, }; @@ -50,7 +50,8 @@ pub fn build_jdtls_launch_args( if java_major_version < 21 { if is_java_autodownload(configuration) { java_executable = - try_to_fetch_and_install_latest_jdk(language_server_id)?.join(get_java_exec_name()); + try_to_fetch_and_install_latest_jdk(language_server_id, configuration)? + .join(get_java_exec_name()); } else { return Err(JAVA_VERSION_ERROR.to_string()); } @@ -157,12 +158,17 @@ pub fn try_to_fetch_and_install_latest_jdtls( ) -> zed::Result { // Use local installation if update mode requires it if let Some(path) = - should_use_local_or_download(configuration, find_latest_local_jdtls(), "jdtls")? + should_use_local_or_download(configuration, find_latest_local_jdtls(), JDTLS_INSTALL_PATH)? { return Ok(path); } // Download latest version + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) @@ -202,6 +208,9 @@ pub fn try_to_fetch_and_install_latest_jdtls( // ...and delete other versions let _ = remove_all_files_except(prefix, build_directory.as_str()); + + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); } // return jdtls base path @@ -213,9 +222,11 @@ pub fn try_to_fetch_and_install_latest_lombok( configuration: &Option, ) -> zed::Result { // Use local installation if update mode requires it - if let Some(path) = - should_use_local_or_download(configuration, find_latest_local_lombok(), "lombok")? - { + if let Some(path) = should_use_local_or_download( + configuration, + find_latest_local_lombok(), + LOMBOK_INSTALL_PATH, + )? { return Ok(path); } @@ -248,6 +259,9 @@ pub fn try_to_fetch_and_install_latest_lombok( // ...and delete other versions let _ = remove_all_files_except(prefix, jar_name.as_str()); + + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version); } // else use it diff --git a/src/util.rs b/src/util.rs index 17795e9..152de2e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -32,6 +32,10 @@ const TAG_UNEXPECTED_FORMAT_ERROR: &str = "Malformed GitHub tags response"; const PATH_IS_NOT_DIR: &str = "File exists but is not a path"; const NO_LOCAL_INSTALL_NEVER_ERROR: &str = "Update checks disabled (never) and no local installation found"; +const NO_LOCAL_INSTALL_ONCE_ERROR: &str = + "Update check already performed once and no local installation found"; + +const ONCE_CHECK_MARKER: &str = ".update_checked"; /// Create a Path if it does not exist /// @@ -60,6 +64,41 @@ pub fn create_path_if_not_exists>(path: P) -> zed::Result<()> { } } +/// Check if update check has been performed once for a component +/// +/// # Arguments +/// +/// * [`component_name`] - The component directory name (e.g., "jdtls", "lombok") +/// +/// # Returns +/// +/// Returns true if the marker file exists, indicating a check was already performed +pub fn has_checked_once(component_name: &str) -> bool { + PathBuf::from(component_name) + .join(ONCE_CHECK_MARKER) + .exists() +} + +/// Mark that an update check has been performed for a component +/// +/// # Arguments +/// +/// * [`component_name`] - The component directory name (e.g., "jdtls", "lombok") +/// * [`version`] - The version that was downloaded +/// +/// # Returns +/// +/// Returns Ok(()) if the marker was created successfully +/// +/// # Errors +/// +/// Returns an error if the directory or marker file could not be created +pub fn mark_checked_once(component_name: &str, version: &str) -> zed::Result<()> { + let marker_path = PathBuf::from(component_name).join(ONCE_CHECK_MARKER); + create_path_if_not_exists(PathBuf::from(component_name))?; + fs::write(marker_path, version).map_err(|e| e.to_string()) +} + /// Expand ~ on Unix-like systems /// /// # Arguments @@ -140,7 +179,8 @@ pub fn get_java_executable( // If the user has set the option, retrieve the latest version of Corretto (OpenJDK) if is_java_autodownload(configuration) { return Ok( - try_to_fetch_and_install_latest_jdk(language_server_id)?.join(java_executable_filename) + try_to_fetch_and_install_latest_jdk(language_server_id, configuration)? + .join(java_executable_filename), ); } @@ -329,6 +369,7 @@ pub fn remove_all_files_except>(prefix: P, filename: &str) -> zed /// /// # Errors /// - Update mode is Never but no local installation found +/// - Update mode is Once and already checked but no local installation found pub fn should_use_local_or_download( configuration: &Option, local: Option, @@ -341,7 +382,22 @@ pub fn should_use_local_or_download( "{NO_LOCAL_INSTALL_NEVER_ERROR} for {component_name}" )), }, - CheckUpdates::Once => Ok(local), + CheckUpdates::Once => { + // If we have a local installation, use it + if let Some(path) = local { + return Ok(Some(path)); + } + + // If we've already checked once, don't check again + if has_checked_once(component_name) { + return Err(format!( + "{NO_LOCAL_INSTALL_ONCE_ERROR} for {component_name}" + )); + } + + // First time checking - allow download + Ok(None) + } CheckUpdates::Always => Ok(None), } } From ad56d5650ce3d5a6af2dc52ac038ec3a7ba5d092 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 27 Dec 2025 12:51:36 +0100 Subject: [PATCH 68/93] Remove `bug` label from issue templates (#163) Remove the `bug` label from the issue templates in favor of the `Bug` issue type --- .github/ISSUE_TEMPLATE/1-grammar-bug.yml | 2 +- .github/ISSUE_TEMPLATE/2-language-server-bug.yml | 2 +- .github/ISSUE_TEMPLATE/3-other-bug.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1-grammar-bug.yml b/.github/ISSUE_TEMPLATE/1-grammar-bug.yml index cf7d7eb..a8fed9b 100644 --- a/.github/ISSUE_TEMPLATE/1-grammar-bug.yml +++ b/.github/ISSUE_TEMPLATE/1-grammar-bug.yml @@ -1,6 +1,6 @@ name: Grammar Bug description: A bug related to the Tree-Sitter grammar (e.g. syntax highlighting, auto-indents, outline, bracket-closing). -labels: ["bug", "grammar"] +labels: ["grammar"] type: "Bug" body: - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/2-language-server-bug.yml b/.github/ISSUE_TEMPLATE/2-language-server-bug.yml index 1e14071..b9c53ed 100644 --- a/.github/ISSUE_TEMPLATE/2-language-server-bug.yml +++ b/.github/ISSUE_TEMPLATE/2-language-server-bug.yml @@ -1,6 +1,6 @@ name: Language Server Bug description: A bug related to the language server (e.g. autocomplete, diagnostics, hover-docs, go to symbol, initialization options). -labels: ["bug", "language-server"] +labels: ["language-server"] type: "Bug" body: - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/3-other-bug.yml b/.github/ISSUE_TEMPLATE/3-other-bug.yml index 86b8a6e..a10d5bc 100644 --- a/.github/ISSUE_TEMPLATE/3-other-bug.yml +++ b/.github/ISSUE_TEMPLATE/3-other-bug.yml @@ -1,6 +1,6 @@ name: Other Bug description: A bug related to something else! -labels: ["bug"] +labels: [] type: "Bug" body: - type: textarea From c1a63976428c3fcf4e7cd972455fbbb0fb42ed39 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:04:16 +0100 Subject: [PATCH 69/93] Bump version to 6.8.4 (#164) This PR bumps the version of this extension to v6.8.4 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06f913a..363c7f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.3" +version = "6.8.4" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 023518b..785c8e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.3" +version = "6.8.4" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index a3f8248..a6aa077 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.3" +version = "6.8.4" schema_version = 1 authors = [ "Valentine Briese ", From b68357e9b1aebc713917d2e2f03848946ae98fcb Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 17:27:00 +0000 Subject: [PATCH 70/93] Update CI workflows to zed@fc89e19 (#166) This PR updates the CI workflow files from the main Zed repository based on the commit zed-industries/zed@fc89e19098a9161c46d034617beab0c0d8bb6f9c Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- .github/workflows/bump_version.yml | 9 ++++++++- .github/workflows/release_version.yml | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 7f4318d..49aca67 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -13,7 +13,9 @@ on: workflow_dispatch: {} jobs: determine_bump_type: - runs-on: namespace-profile-16x32-ubuntu-2204 + if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') + runs-on: namespace-profile-2x4-ubuntu-2404 + permissions: {} steps: - id: get-bump-type name: extensions::bump_version::get_bump_type @@ -40,6 +42,11 @@ jobs: needs: - determine_bump_type if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch' + permissions: + actions: write + contents: write + issues: write + pull-requests: write uses: zed-industries/zed/.github/workflows/extension_bump.yml@main secrets: app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} diff --git a/.github/workflows/release_version.yml b/.github/workflows/release_version.yml index f752931..623ec04 100644 --- a/.github/workflows/release_version.yml +++ b/.github/workflows/release_version.yml @@ -7,6 +7,9 @@ on: - v** jobs: call_release_version: + permissions: + contents: write + pull-requests: write uses: zed-industries/zed/.github/workflows/extension_release.yml@main secrets: app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} From 0b98d6d37f4822a7f8937762ee4678a8c0571c81 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Wed, 31 Dec 2025 09:12:13 +0100 Subject: [PATCH 71/93] Respect XDG_CACHE_HOME on Linux and Mac (#168) Address #167 When the environmental variable XDG_CACHE_HOME is set, it takes precedence over the cache path rooted at HOME --- src/java.rs | 2 +- src/jdtls.rs | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/java.rs b/src/java.rs index 2beb223..5adc09d 100644 --- a/src/java.rs +++ b/src/java.rs @@ -288,7 +288,7 @@ impl Extension for Java { self.lombok_jar_path(language_server_id, &configuration, worktree)?; let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; - Some(format!("-javaagent:{}", canonical_lombok_jar_path)) + Some(format!("-javaagent:{canonical_lombok_jar_path}")) } else { None }; diff --git a/src/jdtls.rs b/src/jdtls.rs index 2c77c5f..91a983f 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -317,15 +317,28 @@ fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { // we fall back to the the extension's workdir, which may never get cleaned up. // In future we may want to deliberately manage caches to be able to force-clean them. - let mut env_iter = worktree.shell_env().into_iter(); + let env = worktree.shell_env(); let base_cachedir = match current_platform().0 { - Os::Mac => env_iter - .find(|(k, _)| k == "HOME") - .map(|(_, v)| PathBuf::from(v).join("Library").join("Caches")), - Os::Linux => env_iter - .find(|(k, _)| k == "HOME") - .map(|(_, v)| PathBuf::from(v).join(".cache")), - Os::Windows => env_iter + Os::Mac => env + .iter() + .find(|(k, _)| k == "XDG_CACHE_HOME") + .map(|(_, v)| PathBuf::from(v)) + .or_else(|| { + env.iter() + .find(|(k, _)| k == "HOME") + .map(|(_, v)| PathBuf::from(v).join("Library").join("Caches")) + }), + Os::Linux => env + .iter() + .find(|(k, _)| k == "XDG_CACHE_HOME") + .map(|(_, v)| PathBuf::from(v)) + .or_else(|| { + env.iter() + .find(|(k, _)| k == "HOME") + .map(|(_, v)| PathBuf::from(v).join(".cache")) + }), + Os::Windows => env + .iter() .find(|(k, _)| k == "APPDATA") .map(|(_, v)| PathBuf::from(v)), } From 18bf70d09301d60ea23ca87a5dd1c39b3ab718ae Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:12:08 +0100 Subject: [PATCH 72/93] Update authors in extension.toml (#170) Remove individual authors and replace with a collective name --- extension.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/extension.toml b/extension.toml index a6aa077..43032b1 100644 --- a/extension.toml +++ b/extension.toml @@ -3,9 +3,7 @@ name = "Java" version = "6.8.4" schema_version = 1 authors = [ - "Valentine Briese ", - "Samuser107 L.Longheval ", - "Yury Abykhodau ", + "Java Extension Contributors", ] description = "Java support." repository = "https://github.com/zed-extensions/java" From f5d1c7fdad18ebdf6f048ff51e6e38bccde146d8 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:15:36 +0100 Subject: [PATCH 73/93] Bump version to 6.8.5 (#169) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 363c7f9..8113815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.4" +version = "6.8.5" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 785c8e9..0a80c12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.4" +version = "6.8.5" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 43032b1..204259f 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.4" +version = "6.8.5" schema_version = 1 authors = [ "Java Extension Contributors", From 3a1eac725dea72a7edf1837da598addcb4ef0a95 Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:27:36 +0100 Subject: [PATCH 74/93] Add support for @Nested test classes in runnables (#172) Relates to #171 This adds support for runnables in tests with one level of nesting in test classes for Mac and Linux. For Windows there would still be a custom `tasks.json` required. --- languages/java/runnables.scm | 65 ++++++++++++++++++++++++++++++++++++ languages/java/tasks.json | 34 +++++++++---------- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/languages/java/runnables.scm b/languages/java/runnables.scm index 75f786e..a8a2b2e 100644 --- a/languages/java/runnables.scm +++ b/languages/java/runnables.scm @@ -64,6 +64,39 @@ (#set! tag java-test-method) ) +; Run nested test function +( + (package_declaration + (scoped_identifier) @java_package_name + ) + (class_declaration + name: (identifier) @java_outer_class_name + body: (class_body + (class_declaration + (modifiers + (marker_annotation + name: (identifier) @nested_annotation + ) + ) + name: (identifier) @java_class_name + body: (class_body + (method_declaration + (modifiers + (marker_annotation + name: (identifier) @annotation_name + ) + ) + name: (identifier) @run @java_method_name + (#eq? @annotation_name "Test") + ) + ) + (#eq? @nested_annotation "Nested") + ) @_ + ) + ) + (#set! tag java-test-method-nested) +) + ; Run test class ( (package_declaration @@ -84,3 +117,35 @@ ) @_ (#set! tag java-test-class) ) + +; Run nested test class +( + (package_declaration + (scoped_identifier) @java_package_name + ) + (class_declaration + name: (identifier) @java_outer_class_name + body: (class_body + (class_declaration + (modifiers + (marker_annotation + name: (identifier) @nested_annotation + ) + ) + name: (identifier) @run @java_class_name + body: (class_body + (method_declaration + (modifiers + (marker_annotation + name: (identifier) @annotation_name + ) + ) + (#eq? @annotation_name "Test") + ) + ) + (#eq? @nested_annotation "Nested") + ) @_ + ) + ) + (#set! tag java-test-class-nested) +) diff --git a/languages/java/tasks.json b/languages/java/tasks.json index 280bfc7..4816928 100644 --- a/languages/java/tasks.json +++ b/languages/java/tasks.json @@ -1,23 +1,23 @@ [ - { - "label": "Run $ZED_CUSTOM_java_class_name", - "command": "pkg=\"${ZED_CUSTOM_java_package_name:}\"; cls=\"$ZED_CUSTOM_java_class_name\"; if [ -n \"$pkg\" ]; then c=\"$pkg.$cls\"; else c=\"$cls\"; fi; if [ -f pom.xml ]; then ./mvnw clean compile exec:java -Dexec.mainClass=\"$c\"; elif [ -f gradlew ]; then ./gradlew run -PmainClass=\"$c\"; else find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java -cp bin \"$c\"; fi;", - "use_new_terminal": false, - "reveal": "always", - "tags": ["java-main"], - "shell": { - "with_arguments": { - "program": "/bin/sh", - "args": ["-c"] - } + { + "label": "Run $ZED_CUSTOM_java_class_name", + "command": "pkg=\"${ZED_CUSTOM_java_package_name:}\"; cls=\"$ZED_CUSTOM_java_class_name\"; if [ -n \"$pkg\" ]; then c=\"$pkg.$cls\"; else c=\"$cls\"; fi; if [ -f pom.xml ]; then ./mvnw clean compile exec:java -Dexec.mainClass=\"$c\"; elif [ -f gradlew ]; then ./gradlew run -PmainClass=\"$c\"; else find . -name '*.java' -not -path './bin/*' -not -path './target/*' -not -path './build/*' -print0 | xargs -0 javac -d bin && java -cp bin \"$c\"; fi;", + "use_new_terminal": false, + "reveal": "always", + "tags": ["java-main"], + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] } - }, + } + }, { - "label": "Test $ZED_CUSTOM_java_class_name.$ZED_CUSTOM_java_method_name", - "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; m=\"$ZED_CUSTOM_java_method_name\"; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$c#$m\"; elif [ -f gradlew ]; then ./gradlew test --tests $c.$m; else >&2 echo 'No build system found'; exit 1; fi;", + "label": "$ZED_CUSTOM_java_class_name.${ZED_CUSTOM_java_outer_class_name:}.$ZED_CUSTOM_java_method_name", + "command": "package=\"$ZED_CUSTOM_java_package_name\"; outer=\"${ZED_CUSTOM_java_outer_class_name:}\"; inner=\"$ZED_CUSTOM_java_class_name\"; method=\"$ZED_CUSTOM_java_method_name\"; sep=\"$\"; if [ -n \"$outer\" ]; then c=\"$outer$sep$inner\"; else c=\"$inner\"; fi; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$package.$c#$method\"; elif [ -f gradlew ]; then ./gradlew test --tests \"$package.$c.$method\"; else >&2 echo 'No build system found'; exit 1; fi;", "use_new_terminal": false, "reveal": "always", - "tags": ["java-test-method"], + "tags": ["java-test-method", "java-test-method-nested"], "shell": { "with_arguments": { "program": "/bin/sh", @@ -27,10 +27,10 @@ }, { "label": "Test class $ZED_CUSTOM_java_class_name", - "command": "c=\"$ZED_CUSTOM_java_package_name.$ZED_CUSTOM_java_class_name\"; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$c\"; elif [ -f gradlew ]; then ./gradlew test --tests $c; else >&2 echo 'No build system found'; exit 1; fi;", + "command": "package=\"$ZED_CUSTOM_java_package_name\"; outer=\"${ZED_CUSTOM_java_outer_class_name:}\"; inner=\"$ZED_CUSTOM_java_class_name\"; sep=\"$\"; if [ -n \"$outer\" ]; then c=\"$outer$sep$inner\"; else c=\"$inner\"; fi; if [ -f pom.xml ]; then ./mvnw clean test -Dtest=\"$package.$c\"; elif [ -f gradlew ]; then ./gradlew test --tests \"$package.$c\"; else >&2 echo 'No build system found'; exit 1; fi;", "use_new_terminal": false, "reveal": "always", - "tags": ["java-test-class"], + "tags": ["java-test-class", "java-test-class-nested"], "shell": { "with_arguments": { "program": "/bin/sh", From 563178e45fa39fe06ec76af2e5dda52847f723a1 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:40:19 +0100 Subject: [PATCH 75/93] Bump version to 6.8.6 (#173) This PR bumps the version of this extension to v6.8.6 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8113815..17ab8fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.5" +version = "6.8.6" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 0a80c12..a90d3cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.5" +version = "6.8.6" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 204259f..d5e0092 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.5" +version = "6.8.6" schema_version = 1 authors = [ "Java Extension Contributors", From 8173dd734f99a45f1555950af99467339b214deb Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:47:48 +0100 Subject: [PATCH 76/93] Prevent JDTLS from generating metadata files at project root (#179) Address #178 Add -Djava.import.generatesMetadataFilesAtProjectRoot=false system property to stop .project, .classpath, and .settings/ files from being created in the project directory. --- src/jdtls.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/jdtls.rs b/src/jdtls.rs index 91a983f..5a429af 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -77,6 +77,7 @@ pub fn build_jdtls_launch_args( ), "-Dosgi.sharedConfiguration.area.readOnly=true".to_string(), "-Dosgi.configuration.cascaded=true".to_string(), + "-Djava.import.generatesMetadataFilesAtProjectRoot=false".to_string(), "-Xms1G".to_string(), "--add-modules=ALL-SYSTEM".to_string(), "--add-opens".to_string(), From c2ee7599386cc6c94f01e09f176f325aa7fe003e Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:42:31 +0100 Subject: [PATCH 77/93] Add task to clear JDTLS cache in Java configuration (#181) --- languages/java/tasks.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/languages/java/tasks.json b/languages/java/tasks.json index 4816928..ee37f93 100644 --- a/languages/java/tasks.json +++ b/languages/java/tasks.json @@ -49,5 +49,17 @@ "args": ["-c"] } } + }, + { + "label": "Clear JDTLS cache", + "command": "cache_dir=\"\"; if [ -n \"$XDG_CACHE_HOME\" ]; then cache_dir=\"$XDG_CACHE_HOME\"; elif [ \"$(uname)\" = \"Darwin\" ]; then cache_dir=\"$HOME/Library/Caches\"; else cache_dir=\"$HOME/.cache\"; fi; found=$(find \"$cache_dir\" -maxdepth 1 -type d -name 'jdtls-*' 2>/dev/null); if [ -n \"$found\" ]; then echo \"$found\" | xargs rm -rf && echo 'JDTLS cache cleared. Restart the language server'; else echo 'No JDTLS cache found'; fi", + "use_new_terminal": false, + "reveal": "always", + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } } ] From ec245acde227ba00262a31b869e78281678efbe2 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:21:10 +0100 Subject: [PATCH 78/93] Bump version to 6.8.7 (#182) This PR bumps the version of this extension to v6.8.7 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17ab8fe..598cb5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.6" +version = "6.8.7" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index a90d3cc..fd791b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.6" +version = "6.8.7" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index d5e0092..afce10b 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.6" +version = "6.8.7" schema_version = 1 authors = [ "Java Extension Contributors", From 6ac8dcaca20bff360ff06772e3eb2389e26ce47e Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:21:28 +0100 Subject: [PATCH 79/93] Add completion sorting by parameter count (#184) Address #183 Sort method completions by parameter count (fewer parameters first). Previously, JDTLS returned the same sortText value (e.g., 999999179) for all overloads of a method like List.of(), causing Zed to display them in arbitrary order. This change prefixes sortText with a zero-padded parameter count (e.g., 00999999179, 01999999179, 02999999179), ensuring overloads are sorted from fewest to most parameters. --- src/proxy.mjs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/proxy.mjs b/src/proxy.mjs index 9c6333a..41bb04e 100644 --- a/src/proxy.mjs +++ b/src/proxy.mjs @@ -88,6 +88,30 @@ proxy.on("server", (data, passthrough) => { passthrough(); }); +function countParams(detail) { + if (!detail || detail === "()") return 0; + const inner = detail.slice(1, -1).trim(); + if (inner === "") return 0; + let count = 1, depth = 0; + for (const ch of inner) { + if (ch === "<") depth++; + else if (ch === ">") depth--; + else if (ch === "," && depth === 0) count++; + } + return count; +} + +function sortCompletionsByParamCount(result) { + const items = Array.isArray(result) ? result : result?.items; + if (!Array.isArray(items)) return; + for (const item of items) { + if (item.kind === 2 || item.kind === 3) { // Method or Function + const paramCount = countParams(item.labelDetails?.detail); + item.sortText = String(paramCount).padStart(2, "0") + (item.sortText || ""); + } + } +} + const server = createServer(async (req, res) => { if (req.method !== "POST") { res.status = 405; @@ -137,6 +161,13 @@ export function createLspProxy({ return; } + // Modify completion responses to sort by param count + if (message?.result?.items || Array.isArray(message?.result)) { + sortCompletionsByParamCount(message.result); + proxyStdout.write(stringify(message)); + return; + } + events.emit("server", message, () => proxyStdout.write(data)); }); From 9b8e5acbd4ec8aa1aa78991cf96913f340a7d8ef Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:26:39 +0100 Subject: [PATCH 80/93] Bump version to 6.8.8 (#185) This PR bumps the version of this extension to v6.8.8 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 598cb5d..b2bf646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.7" +version = "6.8.8" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index fd791b4..3e322a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.7" +version = "6.8.8" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index afce10b..b797f3e 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.7" +version = "6.8.8" schema_version = 1 authors = [ "Java Extension Contributors", From 2defa5febc046022131ca595c6eda477aae2475f Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:41:59 +0100 Subject: [PATCH 81/93] Add debug context to error messages across codebase (#186) Improve error handling by adding contextual information to error messages instead of forwarding raw errors. This helps with debugging by providing details about what operation failed and relevant parameters like file paths, URLs, and component names. All error messages now follow a consistent pattern: - Start with "Failed to..." - Use colon separator before error details - Use "err" as the error variable name --- src/debugger.rs | 55 ++++++++++++++++++++---------- src/java.rs | 89 ++++++++++++++++++++++++++++++++++--------------- src/jdk.rs | 29 +++++++++++----- src/jdtls.rs | 74 ++++++++++++++++++++++++++-------------- src/lsp.rs | 12 +++---- src/util.rs | 22 ++++++++---- 6 files changed, 190 insertions(+), 91 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 4b46492..566889c 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -128,7 +128,9 @@ impl Debugger { configuration, find_latest_local_debugger(), DEBUGGER_INSTALL_PATH, - )? { + ) + .map_err(|err| format!("Failed to resolve debugger installation: {err}"))? + { self.plugin_path = Some(path.clone()); return Ok(path); } @@ -153,11 +155,13 @@ impl Debugger { return Ok(path.clone()); } - create_path_if_not_exists(prefix)?; + create_path_if_not_exists(prefix) + .map_err(|err| format!("Failed to create debugger directory '{prefix}': {err}"))?; download_file( JAVA_DEBUG_PLUGIN_FORK_URL, - &path_to_string(jar_path.clone())?, + &path_to_string(jar_path.clone()) + .map_err(|err| format!("Invalid debugger jar path {jar_path:?}: {err}"))?, DownloadedFileType::Uncompressed, ) .map_err(|err| { @@ -230,7 +234,7 @@ impl Debugger { } let xml = String::from_utf8(res?.body).map_err(|err| { - format!("could not get string from maven metadata response body: {err}") + format!("Failed to get string from Maven metadata response body: {err}") })?; let start_tag = ""; @@ -240,7 +244,9 @@ impl Debugger { .split_once(start_tag) .and_then(|(_, rest)| rest.split_once(end_tag)) .map(|(content, _)| content.trim()) - .ok_or(format!("Failed to parse maven-metadata.xml response {xml}"))?; + .ok_or(format!( + "Failed to parse maven-metadata.xml response: {xml}" + ))?; let artifact = "com.microsoft.java.debug.plugin"; @@ -267,7 +273,7 @@ impl Debugger { &path_to_string(&jar_path)?, DownloadedFileType::Uncompressed, ) - .map_err(|err| format!("Failed to download {url} {err}"))?; + .map_err(|err| format!("Failed to download {url}: {err}"))?; // Mark the downloaded version for "Once" mode tracking let _ = mark_checked_once(DEBUGGER_INSTALL_PATH, latest_version); @@ -278,10 +284,15 @@ impl Debugger { } pub fn start_session(&self) -> zed::Result { - let port = self.lsp.get()?.request::( - "workspace/executeCommand", - json!({ "command": "vscode.java.startDebugSession" }), - )?; + let port = self + .lsp + .get() + .map_err(|err| format!("Failed to acquire LSP client lock: {err}"))? + .request::( + "workspace/executeCommand", + json!({ "command": "vscode.java.startDebugSession" }), + ) + .map_err(|err| format!("Failed to start debug session via LSP: {err}"))?; Ok(TcpArgumentsTemplate { host: None, @@ -292,7 +303,7 @@ impl Debugger { pub fn inject_config(&self, worktree: &Worktree, config_string: String) -> zed::Result { let config: Value = serde_json::from_str(&config_string) - .map_err(|err| format!("Failed to parse debug config {err}"))?; + .map_err(|err| format!("Failed to parse debug config: {err}"))?; if config .get("request") @@ -303,7 +314,7 @@ impl Debugger { } let mut config = serde_json::from_value::(config) - .map_err(|err| format!("Failed to parse java debug config {err}"))?; + .map_err(|err| format!("Failed to parse Java debug config: {err}"))?; let workspace_folder = worktree.root_path(); @@ -316,8 +327,10 @@ impl Debugger { let entries = self .lsp - .get()? - .resolve_main_class(arguments)? + .get() + .map_err(|err| format!("Failed to acquire LSP client lock: {err}"))? + .resolve_main_class(arguments) + .map_err(|err| format!("Failed to resolve main class: {err}"))? .into_iter() .filter(|entry| { config @@ -369,7 +382,12 @@ impl Debugger { let arguments = vec![main_class.clone(), project_name.clone(), scope.clone()]; - let result = self.lsp.get()?.resolve_class_path(arguments)?; + let result = self + .lsp + .get() + .map_err(|err| format!("Failed to acquire LSP client lock: {err}"))? + .resolve_class_path(arguments) + .map_err(|err| format!("Failed to resolve classpath: {err}"))?; for resolved in result { classpaths.extend(resolved); @@ -387,7 +405,7 @@ impl Debugger { config.cwd = config.cwd.or(Some(workspace_folder.to_string())); let config = serde_json::to_string(&config) - .map_err(|err| format!("Failed to stringify debug config {err}"))? + .map_err(|err| format!("Failed to stringify debug config: {err}"))? .replace("${workspaceFolder}", &workspace_folder); Ok(config) @@ -397,14 +415,15 @@ impl Debugger { &self, initialization_options: Option, ) -> zed::Result { - let current_dir = get_curr_dir()?; + let current_dir = get_curr_dir() + .map_err(|err| format!("Failed to get current directory for debugger plugin: {err}"))?; let canonical_path = Value::String( current_dir .join( self.plugin_path .as_ref() - .ok_or("Debugger is not loaded yet")?, + .ok_or("Debugger plugin path not set")?, ) .to_string_lossy() .to_string(), diff --git a/src/java.rs b/src/java.rs index 5adc09d..76dc8e2 100644 --- a/src/java.rs +++ b/src/java.rs @@ -173,7 +173,11 @@ impl Extension for Java { } if self.integrations.is_some() { - self.lsp()?.switch_workspace(worktree.root_path())?; + self.lsp()? + .switch_workspace(worktree.root_path()) + .map_err(|err| { + format!("Failed to switch LSP workspace for debug adapter: {err}") + })?; } Ok(DebugAdapterBinary { @@ -182,15 +186,22 @@ impl Extension for Java { cwd: Some(worktree.root_path()), envs: vec![], request_args: StartDebuggingRequestArguments { - request: self.dap_request_kind( - adapter_name, - Value::from_str(config.config.as_str()) - .map_err(|e| format!("Invalid JSON configuration: {e}"))?, - )?, - configuration: self.debugger()?.inject_config(worktree, config.config)?, + request: self + .dap_request_kind( + adapter_name, + Value::from_str(config.config.as_str()) + .map_err(|err| format!("Invalid JSON configuration: {err}"))?, + ) + .map_err(|err| format!("Failed to determine debug request kind: {err}"))?, + configuration: self + .debugger()? + .inject_config(worktree, config.config) + .map_err(|err| format!("Failed to inject debug configuration: {err}"))?, }, connection: Some(zed::resolve_tcp_template( - self.debugger()?.start_session()?, + self.debugger()? + .start_session() + .map_err(|err| format!("Failed to start debug session: {err}"))?, )?), }) } @@ -245,7 +256,11 @@ impl Extension for Java { Ok(zed::DebugScenario { adapter: config.adapter, build: None, - tcp_connection: Some(self.debugger()?.start_session()?), + tcp_connection: Some( + self.debugger()? + .start_session() + .map_err(|err| format!("Failed to start debug session: {err}"))?, + ), label: "Attach to Java process".to_string(), config: debug_config.to_string(), }) @@ -263,7 +278,7 @@ impl Extension for Java { worktree: &Worktree, ) -> zed::Result { let current_dir = - env::current_dir().map_err(|err| format!("could not get current dir: {err}"))?; + env::current_dir().map_err(|err| format!("Failed to get current directory: {err}"))?; let configuration = self.language_server_workspace_configuration(language_server_id, worktree)?; @@ -279,14 +294,17 @@ impl Extension for Java { "--input-type=module".to_string(), "-e".to_string(), PROXY_FILE.to_string(), - path_to_string(current_dir.clone())?, + path_to_string(current_dir.clone()) + .map_err(|err| format!("Failed to convert current directory to string: {err}"))?, ]; // Add lombok as javaagent if settings.java.jdt.ls.lombokSupport.enabled is true let lombok_jvm_arg = if is_lombok_enabled(&configuration) { - let lombok_jar_path = - self.lombok_jar_path(language_server_id, &configuration, worktree)?; - let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; + let lombok_jar_path = self + .lombok_jar_path(language_server_id, &configuration, worktree) + .map_err(|err| format!("Failed to get Lombok jar path: {err}"))?; + let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path)) + .map_err(|err| format!("Failed to convert Lombok jar path to string: {err}"))?; Some(format!("-javaagent:{canonical_lombok_jar_path}")) } else { @@ -309,13 +327,18 @@ impl Extension for Java { } } else { // otherwise we launch ourselves - args.extend(build_jdtls_launch_args( - &self.language_server_binary_path(language_server_id, &configuration)?, - &configuration, - worktree, - lombok_jvm_arg.into_iter().collect(), - language_server_id, - )?); + args.extend( + build_jdtls_launch_args( + &self + .language_server_binary_path(language_server_id, &configuration) + .map_err(|err| format!("Failed to get JDTLS binary path: {err}"))?, + &configuration, + worktree, + lombok_jvm_arg.into_iter().collect(), + language_server_id, + ) + .map_err(|err| format!("Failed to build JDTLS launch arguments: {err}"))?, + ); } // download debugger if not exists @@ -326,10 +349,13 @@ impl Extension for Java { println!("Failed to download debugger: {err}"); }; - self.lsp()?.switch_workspace(worktree.root_path())?; + self.lsp()? + .switch_workspace(worktree.root_path()) + .map_err(|err| format!("Failed to switch LSP workspace: {err}"))?; Ok(zed::Command { - command: zed::node_binary_path()?, + command: zed::node_binary_path() + .map_err(|err| format!("Failed to get Node.js binary path: {err}"))?, args, env, }) @@ -341,14 +367,25 @@ impl Extension for Java { worktree: &Worktree, ) -> zed::Result> { if self.integrations.is_some() { - self.lsp()?.switch_workspace(worktree.root_path())?; + self.lsp()? + .switch_workspace(worktree.root_path()) + .map_err(|err| { + format!("Failed to switch LSP workspace for initialization: {err}") + })?; } let options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.initialization_options)?; + .map(|lsp_settings| lsp_settings.initialization_options) + .map_err(|err| format!("Failed to get LSP settings for worktree: {err}"))?; if self.debugger().is_ok_and(|v| v.loaded()) { - return Ok(Some(self.debugger()?.inject_plugin_into_options(options)?)); + return Ok(Some( + self.debugger()? + .inject_plugin_into_options(options) + .map_err(|err| { + format!("Failed to inject debugger plugin into options: {err}") + })?, + )); } Ok(options) diff --git a/src/jdk.rs b/src/jdk.rs index bb0dbb4..a844c9a 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -71,11 +71,14 @@ pub fn try_to_fetch_and_install_latest_jdk( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let jdk_path = get_curr_dir()?.join(JDK_INSTALL_PATH); + let jdk_path = get_curr_dir() + .map_err(|err| format!("Failed to get current directory for JDK installation: {err}"))? + .join(JDK_INSTALL_PATH); // Check if we should use local installation based on update mode if let Some(path) = - should_use_local_or_download(configuration, find_latest_local_jdk(), JDK_INSTALL_PATH)? + should_use_local_or_download(configuration, find_latest_local_jdk(), JDK_INSTALL_PATH) + .map_err(|err| format!("Failed to resolve JDK installation: {err}"))? { return get_jdk_bin_path(&path); } @@ -92,7 +95,8 @@ pub fn try_to_fetch_and_install_latest_jdk( require_assets: false, pre_release: false, }, - )? + ) + .map_err(|err| format!("Failed to fetch latest Corretto release from {CORRETTO_REPO}: {err}"))? .version; let install_path = jdk_path.join(&version); @@ -103,17 +107,23 @@ pub fn try_to_fetch_and_install_latest_jdk( &LanguageServerInstallationStatus::Downloading, ); - let platform = get_platform()?; - let arch = get_architecture()?; + let platform = get_platform() + .map_err(|err| format!("Failed to detect platform for JDK download: {err}"))?; + let arch = get_architecture() + .map_err(|err| format!("Failed to detect architecture for JDK download: {err}"))?; + let download_url = build_corretto_url(&version, &platform, &arch); download_file( - build_corretto_url(&version, &platform, &arch).as_str(), - path_to_string(install_path.clone())?.as_str(), + download_url.as_str(), + path_to_string(install_path.clone()) + .map_err(|err| format!("Invalid JDK install path {install_path:?}: {err}"))? + .as_str(), match zed::current_platform().0 { Os::Windows => DownloadedFileType::Zip, _ => DownloadedFileType::GzipTar, }, - )?; + ) + .map_err(|err| format!("Failed to download Corretto JDK from {download_url}: {err}"))?; // Remove older versions let _ = remove_all_files_except(&jdk_path, version.as_str()); @@ -128,7 +138,8 @@ pub fn try_to_fetch_and_install_latest_jdk( fn get_jdk_bin_path(install_path: &Path) -> zed::Result { // Depending on the platform the name of the extracted dir might differ // Rather than hard coding, extract it dynamically - let extracted_dir = get_extracted_dir(install_path)?; + let extracted_dir = get_extracted_dir(install_path) + .map_err(|err| format!("Failed to find JDK directory in {install_path:?}: {err}"))?; Ok(install_path .join(extracted_dir) diff --git a/src/jdtls.rs b/src/jdtls.rs index 5a429af..18972a1 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -45,25 +45,32 @@ pub fn build_jdtls_launch_args( return Ok(vec![jdtls_launcher]); } - let mut java_executable = get_java_executable(configuration, worktree, language_server_id)?; - let java_major_version = get_java_major_version(&java_executable)?; + let mut java_executable = get_java_executable(configuration, worktree, language_server_id) + .map_err(|err| format!("Failed to locate Java executable for JDTLS: {err}"))?; + let java_major_version = get_java_major_version(&java_executable) + .map_err(|err| format!("Failed to determine Java version: {err}"))?; if java_major_version < 21 { if is_java_autodownload(configuration) { java_executable = - try_to_fetch_and_install_latest_jdk(language_server_id, configuration)? + try_to_fetch_and_install_latest_jdk(language_server_id, configuration) + .map_err(|err| format!("Failed to auto-download JDK for JDTLS: {err}"))? .join(get_java_exec_name()); } else { return Err(JAVA_VERSION_ERROR.to_string()); } } - let extension_workdir = get_curr_dir()?; + let extension_workdir = get_curr_dir() + .map_err(|err| format!("Failed to get extension working directory: {err}"))?; let jdtls_base_path = extension_workdir.join(jdtls_path); let shared_config_path = get_shared_config_path(&jdtls_base_path); - let jar_path = find_equinox_launcher(&jdtls_base_path)?; - let jdtls_data_path = get_jdtls_data_path(worktree)?; + let jar_path = find_equinox_launcher(&jdtls_base_path).map_err(|err| { + format!("Failed to find JDTLS equinox launcher in {jdtls_base_path:?}: {err}") + })?; + let jdtls_data_path = get_jdtls_data_path(worktree) + .map_err(|err| format!("Failed to determine JDTLS data path: {err}"))?; let mut args = vec![ path_to_string(java_executable)?, @@ -159,7 +166,8 @@ pub fn try_to_fetch_and_install_latest_jdtls( ) -> zed::Result { // Use local installation if update mode requires it if let Some(path) = - should_use_local_or_download(configuration, find_latest_local_jdtls(), JDTLS_INSTALL_PATH)? + should_use_local_or_download(configuration, find_latest_local_jdtls(), JDTLS_INSTALL_PATH) + .map_err(|err| format!("Failed to resolve JDTLS installation: {err}"))? { return Ok(path); } @@ -170,7 +178,8 @@ pub fn try_to_fetch_and_install_latest_jdtls( &LanguageServerInstallationStatus::CheckingForUpdate, ); - let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; + let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO) + .map_err(|err| format!("Failed to fetch JDTLS versions from {JDTLS_REPO}: {err}"))?; let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) .map_or_else( @@ -198,14 +207,23 @@ pub fn try_to_fetch_and_install_latest_jdtls( language_server_id, &LanguageServerInstallationStatus::Downloading, ); + let download_url = format!( + "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" + ); download_file( - &format!( - "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" - ), - path_to_string(build_path.clone())?.as_str(), + &download_url, + path_to_string(build_path.clone()) + .map_err(|err| format!("Invalid JDTLS build path {build_path:?}: {err}"))? + .as_str(), DownloadedFileType::GzipTar, - )?; - make_file_executable(path_to_string(binary_path)?.as_str())?; + ) + .map_err(|err| format!("Failed to download JDTLS from {download_url}: {err}"))?; + make_file_executable( + path_to_string(&binary_path) + .map_err(|err| format!("Invalid JDTLS binary path {binary_path:?}: {err}"))? + .as_str(), + ) + .map_err(|err| format!("Failed to make JDTLS executable at {binary_path:?}: {err}"))?; // ...and delete other versions let _ = remove_all_files_except(prefix, build_directory.as_str()); @@ -227,7 +245,9 @@ pub fn try_to_fetch_and_install_latest_lombok( configuration, find_latest_local_lombok(), LOMBOK_INSTALL_PATH, - )? { + ) + .map_err(|err| format!("Failed to resolve Lombok installation: {err}"))? + { return Ok(path); } @@ -237,7 +257,8 @@ pub fn try_to_fetch_and_install_latest_lombok( &LanguageServerInstallationStatus::CheckingForUpdate, ); - let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO)?; + let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO) + .map_err(|err| format!("Failed to fetch Lombok versions from {LOMBOK_REPO}: {err}"))?; let prefix = LOMBOK_INSTALL_PATH; let jar_name = format!("lombok-{latest_version}.jar"); let jar_path = Path::new(prefix).join(&jar_name); @@ -250,12 +271,17 @@ pub fn try_to_fetch_and_install_latest_lombok( language_server_id, &LanguageServerInstallationStatus::Downloading, ); - create_path_if_not_exists(prefix)?; + create_path_if_not_exists(prefix) + .map_err(|err| format!("Failed to create Lombok directory '{prefix}': {err}"))?; + let download_url = format!("https://projectlombok.org/downloads/{jar_name}"); download_file( - &format!("https://projectlombok.org/downloads/{jar_name}"), - path_to_string(jar_path.clone())?.as_str(), + &download_url, + path_to_string(jar_path.clone()) + .map_err(|err| format!("Invalid Lombok jar path {jar_path:?}: {err}"))? + .as_str(), DownloadedFileType::Uncompressed, - )?; + ) + .map_err(|err| format!("Failed to download Lombok from {download_url}: {err}"))?; // ...and delete other versions @@ -279,12 +305,10 @@ fn download_jdtls_milestone(version: &str) -> zed::Result { )) .build()?, ) - .map_err(|err| format!("failed to get latest version's build: {err}"))? + .map_err(|err| format!("Failed to get latest version's build: {err}"))? .body, ) - .map_err(|err| { - format!("attempt to get latest version's build resulted in a malformed response: {err}") - }) + .map_err(|err| format!("Failed to get latest version's build (malformed response): {err}")) } fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result { @@ -298,7 +322,7 @@ fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result // else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar' let entries = - read_dir(&plugins_dir).map_err(|e| format!("Failed to read plugins directory: {e}"))?; + read_dir(&plugins_dir).map_err(|err| format!("Failed to read plugins directory: {err}"))?; entries .filter_map(Result::ok) diff --git a/src/lsp.rs b/src/lsp.rs index de1fcbd..5c1b804 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -35,14 +35,14 @@ impl LspWrapper { pub fn get(&self) -> zed::Result> { self.0 .read() - .map_err(|err| format!("LspClient RwLock poisoned during read {err}")) + .map_err(|err| format!("LspClient RwLock poisoned during read: {err}")) } pub fn switch_workspace(&self, workspace: String) -> zed::Result<()> { let mut lock = self .0 .write() - .map_err(|err| format!("LspClient RwLock poisoned during read {err}"))?; + .map_err(|err| format!("LspClient RwLock poisoned during read: {err}"))?; lock.workspace = workspace; @@ -86,9 +86,9 @@ impl LspClient { } fs::read_to_string(port_path) - .map_err(|e| format!("Failed to read a lsp proxy port from file {e}"))? + .map_err(|err| format!("Failed to read LSP proxy port from file: {err}"))? .parse::() - .map_err(|e| format!("Failed to read a lsp proxy port, file corrupted {e}"))? + .map_err(|err| format!("Failed to parse LSP proxy port (file corrupted): {err}"))? }; let mut body = Map::new(); @@ -102,10 +102,10 @@ impl LspClient { .body(Value::Object(body).to_string()) .build()?, ) - .map_err(|e| format!("Failed to send request to lsp proxy {e}"))?; + .map_err(|err| format!("Failed to send request to LSP proxy: {err}"))?; let data: LspResponse = serde_json::from_slice(&res.body) - .map_err(|e| format!("Failed to parse response from lsp proxy {e}"))?; + .map_err(|err| format!("Failed to parse response from LSP proxy: {err}"))?; match data { LspResponse::Success { result } => Ok(result), diff --git a/src/util.rs b/src/util.rs index 152de2e..06fa4a3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -95,8 +95,10 @@ pub fn has_checked_once(component_name: &str) -> bool { /// Returns an error if the directory or marker file could not be created pub fn mark_checked_once(component_name: &str, version: &str) -> zed::Result<()> { let marker_path = PathBuf::from(component_name).join(ONCE_CHECK_MARKER); - create_path_if_not_exists(PathBuf::from(component_name))?; - fs::write(marker_path, version).map_err(|e| e.to_string()) + create_path_if_not_exists(PathBuf::from(component_name)) + .map_err(|err| format!("Failed to create directory for {component_name}: {err}"))?; + fs::write(&marker_path, version) + .map_err(|err| format!("Failed to write marker file {marker_path:?}: {err}")) } /// Expand ~ on Unix-like systems @@ -216,12 +218,18 @@ pub fn get_java_exec_name() -> String { /// * [`java_executable`] can't be converted into a String /// * No major version can be determined pub fn get_java_major_version(java_executable: &PathBuf) -> zed::Result { - let program = path_to_string(java_executable).map_err(|_| JAVA_EXEC_ERROR.to_string())?; - let output_bytes = Command::new(program).arg("-version").output()?.stderr; - let output = String::from_utf8(output_bytes).map_err(|e| e.to_string())?; + let program = path_to_string(java_executable) + .map_err(|err| format!("{JAVA_EXEC_ERROR} '{java_executable:?}': {err}"))?; + let output_bytes = Command::new(&program) + .arg("-version") + .output() + .map_err(|err| format!("Failed to execute '{program} -version': {err}"))? + .stderr; + let output = String::from_utf8(output_bytes) + .map_err(|err| format!("Invalid UTF-8 in java version output: {err}"))?; - let major_version_regex = - Regex::new(r#"version\s"(?P\d+)(\.\d+\.\d+(_\d+)?)?"#).map_err(|e| e.to_string())?; + let major_version_regex = Regex::new(r#"version\s"(?P\d+)(\.\d+\.\d+(_\d+)?)?"#) + .map_err(|err| format!("Invalid regex for Java version parsing: {err}"))?; let major_version = major_version_regex .captures_iter(&output) .find_map(|c| c.name("major").and_then(|m| m.as_str().parse::().ok())); From 5bea6f190e3738dbaeecac733e2558888a7dbde1 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:47:04 +0100 Subject: [PATCH 82/93] Bump version to 6.8.9 (#187) This PR bumps the version of this extension to v6.8.9 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2bf646..77354cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.8" +version = "6.8.9" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 3e322a2..e714ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.8" +version = "6.8.9" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index b797f3e..f99a84f 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.8" +version = "6.8.9" schema_version = 1 authors = [ "Java Extension Contributors", From e82bc49d9ff25c991e089dcfb579066aa7602b60 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:22:25 +0000 Subject: [PATCH 83/93] Update CI workflows to `zed@82dedcbc6c` (#189) This PR updates the CI workflow files from the main Zed repository based on the commit zed-industries/zed@82dedcbc6c1914c2f46164db50befaa40ffbad01 Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- .github/workflows/bump_version.yml | 4 +++- .github/workflows/release_version.yml | 16 ---------------- 2 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 .github/workflows/release_version.yml diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 49aca67..bbf7e9b 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -28,7 +28,6 @@ jobs: bump_type="patch" fi echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - shell: bash -euxo pipefail {0} env: HAS_MAJOR_LABEL: |- ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || @@ -57,3 +56,6 @@ jobs: concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels cancel-in-progress: true +defaults: + run: + shell: bash -euxo pipefail {0} diff --git a/.github/workflows/release_version.yml b/.github/workflows/release_version.yml deleted file mode 100644 index 623ec04..0000000 --- a/.github/workflows/release_version.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Generated from xtask::workflows::extensions::release_version within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::release_version -on: - push: - tags: - - v** -jobs: - call_release_version: - permissions: - contents: write - pull-requests: write - uses: zed-industries/zed/.github/workflows/extension_release.yml@main - secrets: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} From 82ee084d834850e06d9b772d1325a3ddb844b05d Mon Sep 17 00:00:00 2001 From: Karl-Erik Enkelmann <110300169+playdohface@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:27:29 +0100 Subject: [PATCH 84/93] Accept both simple string and list of strings in args fields for debug config (#191) Fixes #190 Adds a new type for leniently deserialized argument-lists. Users can supply either a single string (with whitespace-separated args) or a list of strings (commonly this would be an element for each argument, but mix and match will work as well). Since we only pass these on we simply join them by space upon serializing them back to a string. --- debug_adapter_schemas/Java.json | 28 ++++++++++++++++++++++++---- src/debugger.rs | 8 ++++---- src/util.rs | 24 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/debug_adapter_schemas/Java.json b/debug_adapter_schemas/Java.json index 896a3b9..e36b5bf 100644 --- a/debug_adapter_schemas/Java.json +++ b/debug_adapter_schemas/Java.json @@ -19,12 +19,32 @@ "description": "The fully qualified name of the class containing the main method. If not specified, the debugger automatically resolves the possible main class from the current project." }, "args": { - "type": "string", - "description": "The command line arguments passed to the program." + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "The command line arguments passed to the program. Can be a single string or an array of strings." }, "vmArgs": { - "type": "string", - "description": "The extra options and system properties for the JVM (e.g., -Xms -Xmx -D=)." + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "The extra options and system properties for the JVM (e.g., -Xms -Xmx -D=). Can be a single string or an array of strings." }, "encoding": { "type": "string", diff --git a/src/debugger.rs b/src/debugger.rs index 566889c..4507a02 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -13,8 +13,8 @@ use crate::{ config::get_java_debug_jar, lsp::LspWrapper, util::{ - create_path_if_not_exists, get_curr_dir, mark_checked_once, path_to_string, - should_use_local_or_download, + ArgsStringOrList, create_path_if_not_exists, get_curr_dir, mark_checked_once, + path_to_string, should_use_local_or_download, }, }; @@ -27,9 +27,9 @@ struct JavaDebugLaunchConfig { #[serde(skip_serializing_if = "Option::is_none")] main_class: Option, #[serde(skip_serializing_if = "Option::is_none")] - args: Option, + args: Option, #[serde(skip_serializing_if = "Option::is_none")] - vm_args: Option, + vm_args: Option, #[serde(skip_serializing_if = "Option::is_none")] encoding: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/util.rs b/src/util.rs index 06fa4a3..8a7d558 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,5 @@ use regex::Regex; +use serde::{Deserialize, Serialize, Serializer}; use std::{ env::current_dir, fs, @@ -409,3 +410,26 @@ pub fn should_use_local_or_download( CheckUpdates::Always => Ok(None), } } + +/// A type that can be deserialized from either a single string or a list of strings. +/// +/// When serialized, it always produces a single string. If it was a list, +/// the elements are joined with a space. +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum ArgsStringOrList { + String(String), + List(Vec), +} + +impl Serialize for ArgsStringOrList { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ArgsStringOrList::String(s) => serializer.serialize_str(s), + ArgsStringOrList::List(l) => serializer.serialize_str(&l.join(" ")), + } + } +} From 3392cb7db0d48d08a1522e4c0716aefd05a42f3e Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:36:01 +0100 Subject: [PATCH 85/93] Bump version to 6.8.10 (#192) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77354cd..c97a379 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.9" +version = "6.8.10" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index e714ae6..45e80c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.9" +version = "6.8.10" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index f99a84f..0035c1b 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.9" +version = "6.8.10" schema_version = 1 authors = [ "Java Extension Contributors", From baf7e3873053f7fb16276382a9fc62d0b97f965a Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sat, 21 Feb 2026 23:38:43 +0100 Subject: [PATCH 86/93] Quote args containing spaces in ArgsStringOrList (#195) --- src/util.rs | 71 ++++++++++++++++++- testdata/args_empty_list.json | 3 + testdata/args_list_no_spaces.json | 3 + testdata/args_single_element_with_spaces.json | 3 + testdata/args_single_string.json | 3 + testdata/args_with_spaces.json | 3 + 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 testdata/args_empty_list.json create mode 100644 testdata/args_list_no_spaces.json create mode 100644 testdata/args_single_element_with_spaces.json create mode 100644 testdata/args_single_string.json create mode 100644 testdata/args_with_spaces.json diff --git a/src/util.rs b/src/util.rs index 8a7d558..2f4984b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -414,7 +414,7 @@ pub fn should_use_local_or_download( /// A type that can be deserialized from either a single string or a list of strings. /// /// When serialized, it always produces a single string. If it was a list, -/// the elements are joined with a space. +/// the elements are joined with a space, quoting elements that contain spaces. #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] pub enum ArgsStringOrList { @@ -429,7 +429,74 @@ impl Serialize for ArgsStringOrList { { match self { ArgsStringOrList::String(s) => serializer.serialize_str(s), - ArgsStringOrList::List(l) => serializer.serialize_str(&l.join(" ")), + ArgsStringOrList::List(l) => { + let quoted: Vec = l + .iter() + .map(|s| { + if s.contains(' ') { + format!("\"{}\"", s) + } else { + s.clone() + } + }) + .collect(); + serializer.serialize_str("ed.join(" ")) + } } } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[derive(Deserialize, Serialize)] + struct ArgsWrapper { + args: ArgsStringOrList, + } + + #[test] + fn test_args_list_with_spaces_quotes_elements() { + let json = std::fs::read_to_string("testdata/args_with_spaces.json").unwrap(); + let wrapper: ArgsWrapper = serde_json::from_str(&json).unwrap(); + let serialized = serde_json::to_value(&wrapper).unwrap(); + assert_eq!( + serialized["args"], + r#""C:\path with spaces\some file.txt" arg2"# + ); + } + + #[test] + fn test_args_single_string_preserved_as_is() { + let json = std::fs::read_to_string("testdata/args_single_string.json").unwrap(); + let wrapper: ArgsWrapper = serde_json::from_str(&json).unwrap(); + let serialized = serde_json::to_value(&wrapper).unwrap(); + assert_eq!(serialized["args"], r#"C:\path with spaces\some file.txt"#); + } + + #[test] + fn test_args_list_no_spaces_not_quoted() { + let json = std::fs::read_to_string("testdata/args_list_no_spaces.json").unwrap(); + let wrapper: ArgsWrapper = serde_json::from_str(&json).unwrap(); + let serialized = serde_json::to_value(&wrapper).unwrap(); + assert_eq!(serialized["args"], "arg1 arg2"); + } + + #[test] + fn test_args_single_element_with_spaces_quoted() { + let json = + std::fs::read_to_string("testdata/args_single_element_with_spaces.json").unwrap(); + let wrapper: ArgsWrapper = serde_json::from_str(&json).unwrap(); + let serialized = serde_json::to_value(&wrapper).unwrap(); + assert_eq!(serialized["args"], r#""path with spaces""#); + } + + #[test] + fn test_args_empty_list() { + let json = std::fs::read_to_string("testdata/args_empty_list.json").unwrap(); + let wrapper: ArgsWrapper = serde_json::from_str(&json).unwrap(); + let serialized = serde_json::to_value(&wrapper).unwrap(); + assert_eq!(serialized["args"], ""); + } +} diff --git a/testdata/args_empty_list.json b/testdata/args_empty_list.json new file mode 100644 index 0000000..21a41b3 --- /dev/null +++ b/testdata/args_empty_list.json @@ -0,0 +1,3 @@ +{ + "args": [] +} diff --git a/testdata/args_list_no_spaces.json b/testdata/args_list_no_spaces.json new file mode 100644 index 0000000..917d7b8 --- /dev/null +++ b/testdata/args_list_no_spaces.json @@ -0,0 +1,3 @@ +{ + "args": ["arg1", "arg2"] +} diff --git a/testdata/args_single_element_with_spaces.json b/testdata/args_single_element_with_spaces.json new file mode 100644 index 0000000..54bd527 --- /dev/null +++ b/testdata/args_single_element_with_spaces.json @@ -0,0 +1,3 @@ +{ + "args": ["path with spaces"] +} diff --git a/testdata/args_single_string.json b/testdata/args_single_string.json new file mode 100644 index 0000000..6e344aa --- /dev/null +++ b/testdata/args_single_string.json @@ -0,0 +1,3 @@ +{ + "args": "C:\\path with spaces\\some file.txt" +} diff --git a/testdata/args_with_spaces.json b/testdata/args_with_spaces.json new file mode 100644 index 0000000..c53f037 --- /dev/null +++ b/testdata/args_with_spaces.json @@ -0,0 +1,3 @@ +{ + "args": ["C:\\path with spaces\\some file.txt", "arg2"] +} From a26f84bab67ab8cc3d7ce330fc71e5703ea74a9f Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:48:43 +0100 Subject: [PATCH 87/93] Bump version to 6.8.11 (#196) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c97a379..9db3a76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.10" +version = "6.8.11" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 45e80c1..08cb800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.10" +version = "6.8.11" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 0035c1b..82a1b44 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.10" +version = "6.8.11" schema_version = 1 authors = [ "Java Extension Contributors", From 6cda290ac28a83b8948587a379ab086f9bc1f796 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:01:49 +0100 Subject: [PATCH 88/93] Support any decorator ending in Test in runnables (#200) Address #198 --- languages/java/runnables.scm | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/languages/java/runnables.scm b/languages/java/runnables.scm index a8a2b2e..7ad84dc 100644 --- a/languages/java/runnables.scm +++ b/languages/java/runnables.scm @@ -42,7 +42,7 @@ (#set! tag java-main) ) -; Run test function +; Run test function (marker annotation, e.g. @Test) ( (package_declaration (scoped_identifier) @java_package_name @@ -52,12 +52,15 @@ body: (class_body (method_declaration (modifiers - (marker_annotation + [(marker_annotation name: (identifier) @annotation_name ) + (annotation + name: (identifier) @annotation_name + )] ) name: (identifier) @run @java_method_name - (#eq? @annotation_name "Test") + (#match? @annotation_name "Test$") ) ) ) @_ @@ -82,12 +85,15 @@ body: (class_body (method_declaration (modifiers - (marker_annotation + [(marker_annotation name: (identifier) @annotation_name ) + (annotation + name: (identifier) @annotation_name + )] ) name: (identifier) @run @java_method_name - (#eq? @annotation_name "Test") + (#match? @annotation_name "Test$") ) ) (#eq? @nested_annotation "Nested") @@ -107,11 +113,14 @@ body: (class_body (method_declaration (modifiers - (marker_annotation + [(marker_annotation name: (identifier) @annotation_name ) + (annotation + name: (identifier) @annotation_name + )] ) - (#eq? @annotation_name "Test") + (#match? @annotation_name "Test$") ) ) ) @_ @@ -136,11 +145,14 @@ body: (class_body (method_declaration (modifiers - (marker_annotation + [(marker_annotation name: (identifier) @annotation_name ) + (annotation + name: (identifier) @annotation_name + )] ) - (#eq? @annotation_name "Test") + (#match? @annotation_name "Test$") ) ) (#eq? @nested_annotation "Nested") From 5e372db21acc7685c48c3dfc9bffd19649ec0611 Mon Sep 17 00:00:00 2001 From: Soumyadwip Chanda <81933624+da-r-k@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:23:59 +0530 Subject: [PATCH 89/93] Add syntax highlighting for project symbol search (#188) Implements `label_for_symbol` to provide syntax-highlighted labels in the project symbol picker (`cmd-t`) Co-authored-by: Claude Opus 4.6 --- README.md | 6 +++++ src/java.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 62d7d06..139691e 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ Here is a common `settings.json` including the above mentioned configurations: } ``` +## Project Symbol Search + +The extension supports project-wide symbol search with syntax-highlighted results. This feature is powered by JDTLS and can be accessed via Zed's symbol search. + +JDTLS uses **CamelCase fuzzy matching** for symbol queries. For example, searching for `EmpMe` would match `EmptyMedia`. The pattern works like `Emp*Me*`, matching the capital letters of CamelCase names. + ## Debugger Debug support is enabled via our [Fork of Java Debug](https://github.com/zed-industries/java-debug), which the extension will automatically download and start for you. Please refer to the [Zed Documentation](https://zed.dev/docs/debugger#getting-started) for general information about how debugging works in Zed. diff --git a/src/java.rs b/src/java.rs index 76dc8e2..05f7438 100644 --- a/src/java.rs +++ b/src/java.rs @@ -16,7 +16,7 @@ use zed_extension_api::{ self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, Extension, LanguageServerId, LanguageServerInstallationStatus, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, Worktree, - lsp::{Completion, CompletionKind}, + lsp::{Completion, CompletionKind, Symbol, SymbolKind}, register_extension, serde_json::{Value, json}, set_language_server_installation_status, @@ -535,6 +535,71 @@ impl Extension for Java { _ => None, }) } + + fn label_for_symbol( + &self, + _language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + let name = &symbol.name; + + match symbol.kind { + SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum => { + let keyword = match symbol.kind { + SymbolKind::Class => "class ", + SymbolKind::Interface => "interface ", + SymbolKind::Enum => "enum ", + _ => unreachable!(), + }; + let code = format!("{keyword}{name} {{}}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + SymbolKind::Method | SymbolKind::Function => { + // jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)" + // display: "ReturnType methodName(Type, Type)" (Java declaration order) + let method_name = name.split('(').next().unwrap_or(name); + let after_name = &name[method_name.len()..]; + + let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") { + (p, Some(r)) + } else { + (after_name, None) + }; + + let ret = return_type.unwrap_or("void"); + let class_open = "class _ { "; + let code = format!("{class_open}{ret} {method_name}() {{}} }}"); + + let ret_start = class_open.len(); + let name_start = ret_start + ret.len() + 1; + + // Display: "void methodName(String, int)" + let mut spans = vec![ + CodeLabelSpan::code_range(ret_start..ret_start + ret.len()), + CodeLabelSpan::literal(" ".to_string(), None), + CodeLabelSpan::code_range(name_start..name_start + method_name.len()), + ]; + if !params.is_empty() { + spans.push(CodeLabelSpan::literal(params.to_string(), None)); + } + + // filter on "methodName(params)" portion of displayed text + let type_prefix_len = ret.len() + 1; // "void " + let filter_end = type_prefix_len + method_name.len() + params.len(); + Some(CodeLabel { + spans, + filter_range: (type_prefix_len..filter_end).into(), + code, + }) + } + _ => None, + } + } } register_extension!(Java); From a63aeb21bc8e6e8fec14ee186f6099454979e22e Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:44:48 +0100 Subject: [PATCH 90/93] Bump version to 6.8.12 (#201) --- Cargo.lock | 2 +- Cargo.toml | 2 +- extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9db3a76..a53688f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "zed_java" -version = "6.8.11" +version = "6.8.12" dependencies = [ "hex", "regex", diff --git a/Cargo.toml b/Cargo.toml index 08cb800..04a1ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zed_java" -version = "6.8.11" +version = "6.8.12" edition = "2024" publish = false license = "Apache-2.0" diff --git a/extension.toml b/extension.toml index 82a1b44..91f82a9 100644 --- a/extension.toml +++ b/extension.toml @@ -1,6 +1,6 @@ id = "java" name = "Java" -version = "6.8.11" +version = "6.8.12" schema_version = 1 authors = [ "Java Extension Contributors", From 5b5829bc35eeab2953d307b29749b81be1d77ed5 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:27:48 +0000 Subject: [PATCH 91/93] Update CI workflows to `e439c295c5` (#202) This PR updates the CI workflow files from the main Zed repository based on the commit zed-industries/zed@e439c295c5669252151e283cd78d9f95fe1fd4ce Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- .github/workflows/bump_version.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index bbf7e9b..dbe92a4 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -52,7 +52,7 @@ jobs: app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} with: bump-type: ${{ needs.determine_bump_type.outputs.bump_type }} - force-bump: true + force-bump: ${{ github.event_name != 'push' }} concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels cancel-in-progress: true From 38633a94cd1765886a50d66467e3239801b5dd74 Mon Sep 17 00:00:00 2001 From: Riccardo Strina <85676009+tartarughina@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:05:51 +0100 Subject: [PATCH 92/93] Update the README (#205) --- README.md | 235 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 170 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 139691e..ff01859 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,32 @@ To get started with Java, click the `edit debug.json` button in the Debug menu, You should then be able to start a new Debug Session with the "Launch Debugger" scenario from the debug menu. +### Single-File Debugging + +If you're working a lot with single file debugging, you can use the following `debug.json` config instead: +```jsonc +[ + { + "label": "Debug $ZED_STEM", + "adapter": "Java", + "request": "launch", + "mainClass": "$ZED_STEM", + "build": { + "command": "javac -d . $ZED_FILE", + "shell": { + "with_arguments": { + "program": "/bin/sh", + "args": ["-c"] + } + } + } + } +] +``` +This will compile and launch the debugger using the currently selected file as the entry point. +Ideally, we would implement a run/debug option directly in the runnables (similar to how the Rust extension does it), which would allow you to easily start a debugging session without explicitly updating the entry point. +Note that integrating the debugger with runnables is currently limited to core languages in Zed, so this is the best workaround for now. + ## Launch Scripts (aka Tasks) in Windows This extension provides tasks for running your application and tests from within Zed via little play buttons next to tests/entry points. However, due to current limitiations of Zed's extension interface, we can not provide scripts that will work across Maven and Gradle on both Windows and Unix-compatible systems, so out of the box the launch scripts only work on Mac and Linux. @@ -82,78 +108,132 @@ This extension provides tasks for running your application and tests from within There is a fairly straightforward fix that you can apply to make it work on Windows by supplying your own task scripts. Please see [this Issue](https://github.com/zed-extensions/java/issues/94) for information on how to do that and read the [Tasks section in Zeds documentation](https://zed.dev/docs/tasks) for more information. ## Advanced Configuration/JDTLS initialization Options -JDTLS provides many configuration options that can be passed via the `initialize` LSP-request. The extension will pass the JSON-object from `lsp.jdtls.settings.initialization_options` in your settings on to JDTLS. Please refer to the [JDTLS Configuration Wiki Page](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request) for the available options and values. Below is an example `settings.json` that would pass on the example configuration from the above wiki page to JDTLS: +JDTLS provides many configuration options that can be passed via the `initialize` LSP-request. The extension will pass the JSON-object from `lsp.jdtls.initialization_options` in your settings on to JDTLS. Please refer to the [JDTLS Configuration Wiki Page](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request) for the available options and values. Below is an opinionated example configuration for JDTLS with most options enabled: ```jsonc -{ - "lsp": { - "jdtls": { +"lsp": { + "jdtls": { + "initialization_options": { + "bundles": [], + "workspaceFolders": [ + "file:///home/snjeza/Project" + ], "settings": { - // this will be sent to JDTLS as initializationOptions: - "initialization_options": { - "bundles": [], - // use this if your zed project root folder is not the same as the java project root: - "workspaceFolders": ["file:///home/snjeza/Project"], - "settings": { - "java": { - "home": "/usr/local/jdk-9.0.1", - "errors": { - "incompleteClasspath": { - "severity": "warning" - } - }, - "configuration": { - "updateBuildConfiguration": "interactive", - "maven": { - "userSettings": null - } - }, - "import": { - "gradle": { - "enabled": true - }, - "maven": { - "enabled": true - }, - "exclusions": [ - "**/node_modules/**", - "**/.metadata/**", - "**/archetype-resources/**", - "**/META-INF/maven/**", - "/**/test/**" - ] - }, - "referencesCodeLens": { - "enabled": false - }, - "signatureHelp": { - "enabled": false - }, - "implementationCodeLens": "all", - "format": { + "java": { + "configuration": { + "updateBuildConfiguration": "automatic", + "runtimes": [] + }, + "saveActions": { + "organizeImports": true + }, + "compile": { + "nullAnalysis": { + "mode": "automatic" + } + }, + "references": { + "includeAccessors": true, + "includeDecompiledSources": true + }, + "jdt": { + "ls": { + "protobufSupport": { "enabled": true }, - "saveActions": { - "organizeImports": false - }, - "contentProvider": { - "preferred": null - }, - "autobuild": { - "enabled": false - }, - "completion": { - "favoriteStaticMembers": [ - "org.junit.Assert.*", - "org.junit.Assume.*", - "org.junit.jupiter.api.Assertions.*", - "org.junit.jupiter.api.Assumptions.*", - "org.junit.jupiter.api.DynamicContainer.*", - "org.junit.jupiter.api.DynamicTest.*" - ], - "importOrder": ["java", "javax", "com", "org"] + "groovySupport": { + "enabled": true + } + } + }, + "eclipse": { + "downloadSources": true + }, + "maven": { + "downloadSources": true, + "updateSnapshots": true + }, + "autobuild": { + "enabled": true + }, + "maxConcurrentBuilds": 1, + "inlayHints": { + "parameterNames": { + "enabled": "all" + } + }, + "signatureHelp": { + "enabled": true, + "description": { + "enabled": true + } + }, + "format": { + "enabled": true, + "settings": { + // The formatter config to use + "url": "~/.config/jdtls/palantir_java_jdtls.xml" + }, + "onType": { + "enabled": true + } + }, + "contentProvider": { + "preferred": null + }, + "import": { + "gradle": { + "enabled": true, + "wrapper": { + "enabled": true } + }, + "maven": { + "enabled": true + }, + "exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**", + "/**/test/**" + ] + }, + "completion": { + "enabled": true, + "favoriteStaticMembers": [ + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*", + "org.mockito.Mockito.*", + "org.mockito.ArgumentMatchers.*" + ], + "importOrder": [ + "java", + "javax", + "com", + "org" + ], + "postfix": { + "enabled": true + }, + "chain": { + "enabled": true + }, + "guessMethodArguments": "insertParameterNames", + "overwrite": true + }, + "errors": { + "incompleteClasspath": { + "severity": "warning" } + }, + "implementationCodeLens": "all", + "referencesCodeLens": { + "enabled": true } } } @@ -161,3 +241,28 @@ JDTLS provides many configuration options that can be passed via the `initialize } } ``` + +If you're working without a Gradle or Maven project, and the following error `The declared package "Example" does not match the expected package ""` pops up, consider adding these settings under + +``` +MyProject/ + ├── .zed/ + │ └── settings.json + ``` + +```jsonc +"lsp": { + "jdtls": { + "initialization_options": { + "project": { + "sourcePaths": [ + ".", + "src" + ] + }, + } + } +} +``` + +If changes are not picked up, clean JDTLS' cache (from a java file run the task `Clear JDTLS cache`) and restart the language server From 6b1e3264385b138d5f7e2a20db64914344f8d450 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:38:10 +0000 Subject: [PATCH 93/93] Update CI workflows to `0c49aaa` (#209) This PR updates the CI workflow files from the main Zed repository based on the commit zed-industries/zed@0c49aaae3743e349dc18452c90877dbdee59bee1 This changes the workflows to instead use a pinned version of the workflows from the main repository. Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- .github/workflows/bump_version.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index dbe92a4..7d9033b 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -46,7 +46,7 @@ jobs: contents: write issues: write pull-requests: write - uses: zed-industries/zed/.github/workflows/extension_bump.yml@main + uses: zed-industries/zed/.github/workflows/extension_bump.yml@0c49aaae3743e349dc18452c90877dbdee59bee1 secrets: app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}