From ecfb30cc43b4391511119738f790a6c7729f13be Mon Sep 17 00:00:00 2001 From: himicoswilson Date: Tue, 11 Nov 2025 17:52:04 +0800 Subject: [PATCH 1/6] Add configuration options for update checks and custom download URLs --- README.md | 6 ++- src/config.rs | 38 +++++++++++++++ src/debugger.rs | 67 ++++++++++++++++++++++---- src/java.rs | 26 +++++++--- src/jdtls.rs | 124 ++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 235 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 4eb028a..c3640c2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,11 @@ 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, + "check_updates_on_startup": true, + "jdtls_download_url": "https://download.eclipse.org/jdtls/milestones/{version}/jdt-language-server-{version}.tar.gz", + "lombok_download_url": "https://projectlombok.org/downloads/lombok-{version}.jar", + "debugger_download_url": "https://github.com/zed-industries/java-debug/releases/download/{version}/com.microsoft.java.debug.plugin-{version}.jar" } } } diff --git a/src/config.rs b/src/config.rs index 3dc9fe3..3fe2c16 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,3 +51,41 @@ pub fn is_lombok_enabled(configuration: &Option) -> bool { }) .unwrap_or(true) } + +pub fn is_check_updates_enabled(configuration: &Option) -> bool { + configuration + .as_ref() + .and_then(|configuration| { + configuration + .pointer("/check_updates_on_startup") + .and_then(|enabled| enabled.as_bool()) + }) + .unwrap_or(true) +} + +pub fn get_jdtls_download_url(configuration: &Option) -> Option { + configuration.as_ref().and_then(|configuration| { + configuration + .pointer("/jdtls_download_url") + .and_then(|url| url.as_str()) + .map(|s| s.to_string()) + }) +} + +pub fn get_lombok_download_url(configuration: &Option) -> Option { + configuration.as_ref().and_then(|configuration| { + configuration + .pointer("/lombok_download_url") + .and_then(|url| url.as_str()) + .map(|s| s.to_string()) + }) +} + +pub fn get_debugger_download_url(configuration: &Option) -> Option { + configuration.as_ref().and_then(|configuration| { + configuration + .pointer("/debugger_download_url") + .and_then(|url| url.as_str()) + .map(|s| s.to_string()) + }) +} diff --git a/src/debugger.rs b/src/debugger.rs index c6b98ca..a29acae 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -10,6 +10,7 @@ use zed_extension_api::{ }; use crate::{ + config::{get_debugger_download_url, is_check_updates_enabled}, lsp::LspWrapper, util::{create_path_if_not_exists, get_curr_dir, path_to_string}, }; @@ -80,16 +81,47 @@ impl Debugger { pub fn get_or_download( &mut self, language_server_id: &LanguageServerId, + configuration: &Option, ) -> zed::Result { + // If update checks are disabled, prefer local installation only. + if !is_check_updates_enabled(configuration) { + if let Some(path) = &self.plugin_path + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + { + return Ok(path.clone()); + } + + // Try to find any existing debugger jar in the directory + let prefix = "debugger"; + if let Ok(entries) = fs::read_dir(prefix) { + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + if path.is_file() + && path.extension().and_then(|ext| ext.to_str()) == Some("jar") + && path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("com.microsoft.java.debug.plugin")) + { + self.plugin_path = Some(path.clone()); + return Ok(path); + } + } + } + + return Err("Update checks are disabled and no local debugger installation found. Please enable check_updates_on_startup or manually install the debugger.".to_string()); + } + // 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) + self.get_or_download_fork(language_server_id, configuration) } fn get_or_download_fork( &mut self, - _language_server_id: &LanguageServerId, + language_server_id: &LanguageServerId, + configuration: &Option, ) -> zed::Result { let prefix = "debugger"; let artifact = "com.microsoft.java.debug.plugin"; @@ -97,26 +129,41 @@ impl Debugger { let jar_name = format!("{artifact}-{latest_version}.jar"); let jar_path = PathBuf::from(prefix).join(&jar_name); + // Report checking-for-update status + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + if let Some(path) = &self.plugin_path && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - && path.ends_with(jar_name) + && path.ends_with(&jar_name) { return Ok(path.clone()); } + // Report downloading status + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + create_path_if_not_exists(prefix)?; + let download_url = if let Some(custom_url) = get_debugger_download_url(configuration) { + // Use custom URL directly (no placeholders since we use fixed version) + custom_url + } else { + // Use default fork URL + JAVA_DEBUG_PLUGIN_FORK_URL.to_string() + }; + download_file( - JAVA_DEBUG_PLUGIN_FORK_URL, + &download_url, &path_to_string(jar_path.clone())?, DownloadedFileType::Uncompressed, ) - .map_err(|err| { - format!( - "Failed to download java-debug fork from {}: {err}", - JAVA_DEBUG_PLUGIN_FORK_URL - ) - })?; + .map_err(|err| format!("Failed to download java-debug from {}: {err}", download_url))?; self.plugin_path = Some(jar_path.clone()); Ok(jar_path) diff --git a/src/java.rs b/src/java.rs index 95e9fcd..683fdbe 100644 --- a/src/java.rs +++ b/src/java.rs @@ -74,6 +74,7 @@ impl Java { fn language_server_binary_path( &mut self, language_server_id: &LanguageServerId, + worktree: &Worktree, ) -> zed::Result { // Use cached path if exists @@ -83,13 +84,16 @@ impl Java { return Ok(path.clone()); } + let configuration = + self.language_server_workspace_configuration(language_server_id, worktree)?; + // Check for latest version set_language_server_installation_status( language_server_id, &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 +109,21 @@ impl Java { } } - fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result { + fn lombok_jar_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> 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) { + let configuration = + self.language_server_workspace_configuration(language_server_id, worktree)?; + + 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 +281,7 @@ 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, worktree)?; let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; Some(format!("-javaagent:{canonical_lombok_jar_path}")) @@ -289,7 +300,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, worktree)?, &configuration, worktree, lombok_jvm_arg.into_iter().collect(), @@ -298,7 +309,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) + { println!("Failed to download debugger: {err}"); }; diff --git a/src/jdtls.rs b/src/jdtls.rs index e48f223..ea67fa8 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,7 +15,10 @@ use zed_extension_api::{ }; use crate::{ - config::is_java_autodownload, + config::{ + get_jdtls_download_url, get_lombok_download_url, is_check_updates_enabled, + is_java_autodownload, + }, jdk::try_to_fetch_and_install_latest_jdk, util::{ create_path_if_not_exists, get_curr_dir, get_java_exec_name, get_java_executable, @@ -153,9 +156,66 @@ 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 { - let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + + // If update checks are disabled, prefer local installation only. + if !is_check_updates_enabled(configuration) { + if let Some(local_version) = find_latest_local_jdtls() { + return Ok(local_version); + } + return Err("Update checks are disabled and no local JDTLS installation found. Please enable check_updates_on_startup or manually install JDTLS.".to_string()); + } + + // Report checking‐for‐update status + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + // Check for a custom download URL in configuration. If present, use it directly. + if let Some(custom_url) = get_jdtls_download_url(configuration) { + // derive a directory name from the filename of the URL + let filename = custom_url + .split('/') + .last() + .unwrap_or("jdtls.tar.gz") + .to_string(); + let build_directory_name = filename.trim_end_matches(".tar.gz"); + let build_path = prefix.join(build_directory_name); + let binary_path = build_path.join("bin").join(get_binary_name()); + + if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { + // mark status as downloading + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + // ensure install directory exists + create_path_if_not_exists(&prefix)?; + + // download the archive from the custom URL + download_file( + &custom_url, + path_to_string(build_path.clone())?.as_str(), + DownloadedFileType::GzipTar, + )?; + + // make the binary executable (on Unix‐style platforms) + make_file_executable(path_to_string(binary_path)?.as_str())?; + + // remove older versions, keep only this directory + let _ = remove_all_files_except(prefix, build_directory_name); + } + return Ok(build_path); + } + + // No custom URL. + // fetch the list of latest versions from repository tags + let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; + // attempt to download the “latest” milestone let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) .map_or_else( |_| { @@ -168,8 +228,6 @@ pub fn try_to_fetch_and_install_latest_jdtls( |milestone| Ok((last, milestone.trim_end().to_string())), )?; - let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - 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()); @@ -182,10 +240,13 @@ pub fn try_to_fetch_and_install_latest_jdtls( language_server_id, &LanguageServerInstallationStatus::Downloading, ); + + let download_url = format!( + "https://download.eclipse.org/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}" - ), + &download_url, path_to_string(build_path.clone())?.as_str(), DownloadedFileType::GzipTar, )?; @@ -201,14 +262,56 @@ 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 { + // If update checks are disabled, try to use local cache first + if !is_check_updates_enabled(configuration) { + if let Some(local_version) = find_latest_local_lombok() { + return Ok(local_version); + } + return Err("Update checks are disabled and no local Lombok installation found. Please enable check_updates_on_startup or manually install Lombok.".to_string()); + } + + let prefix = LOMBOK_INSTALL_PATH; + + // Check if custom download URL is configured + if let Some(custom_url) = get_lombok_download_url(configuration) { + // Use custom URL directly without version checking + // Extract filename from URL + let jar_name = custom_url + .split('/') + .last() + .unwrap_or("lombok.jar") + .to_string(); + let jar_path = Path::new(prefix).join(&jar_name); + + // If not installed, download it + if !metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + create_path_if_not_exists(prefix)?; + + download_file( + &custom_url, + path_to_string(jar_path.clone())?.as_str(), + DownloadedFileType::Uncompressed, + )?; + + let _ = remove_all_files_except(prefix, jar_name.as_str()); + } + + return Ok(jar_path); + } + + // Default behavior: get version from GitHub tags set_language_server_installation_status( language_server_id, &LanguageServerInstallationStatus::CheckingForUpdate, ); 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); @@ -221,8 +324,11 @@ pub fn try_to_fetch_and_install_latest_lombok( &LanguageServerInstallationStatus::Downloading, ); create_path_if_not_exists(prefix)?; + + let download_url = format!("https://projectlombok.org/downloads/{jar_name}"); + download_file( - &format!("https://projectlombok.org/downloads/{jar_name}"), + &download_url, path_to_string(jar_path.clone())?.as_str(), DownloadedFileType::Uncompressed, )?; From a0792f1b5866371318e86a460323c117f48aca30 Mon Sep 17 00:00:00 2001 From: himicoswilson Date: Wed, 12 Nov 2025 14:49:59 +0800 Subject: [PATCH 2/6] Refactor update check logic with unified mode and resolver Replaces per-component update check flags and custom URLs with a unified `update_check_mode` configuration supporting 'always', 'once', and 'never' modes. Introduces a shared resolver utility for component installation logic, simplifying and unifying how JDTLS, Lombok, and Debugger are managed. Updates documentation and cleans up related code paths. --- README.md | 16 ++- src/config.rs | 55 +++++----- src/debugger.rs | 126 ++++++++++++----------- src/java.rs | 9 +- src/jdtls.rs | 265 +++++++++++++++++------------------------------- src/util.rs | 64 +++++++++++- 6 files changed, 260 insertions(+), 275 deletions(-) diff --git a/README.md b/README.md index c3640c2..471bb47 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,18 @@ Here is a common `settings.json` including the above mentioned configurations: "java_home": "/path/to/your/JDK21+", "lombok_support": true, "jdk_auto_download": false, - "check_updates_on_startup": true, - "jdtls_download_url": "https://download.eclipse.org/jdtls/milestones/{version}/jdt-language-server-{version}.tar.gz", - "lombok_download_url": "https://projectlombok.org/downloads/lombok-{version}.jar", - "debugger_download_url": "https://github.com/zed-industries/java-debug/releases/download/{version}/com.microsoft.java.debug.plugin-{version}.jar" + + // Component update mode (default: "always") + // Controls how JDTLS, Lombok, and Debugger are managed: + // + // "always" + // - Every startup: Checks for and downloads latest versions + // "once" + // - First startup: Downloads the latest versions + // "never" + // - Only uses existing local installations + // - Will fail if components are not already installed + "update_check_mode": "always" } } } diff --git a/src/config.rs b/src/config.rs index 3fe2c16..67f8c3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -52,40 +52,31 @@ pub fn is_lombok_enabled(configuration: &Option) -> bool { .unwrap_or(true) } -pub fn is_check_updates_enabled(configuration: &Option) -> bool { - configuration - .as_ref() - .and_then(|configuration| { - configuration - .pointer("/check_updates_on_startup") - .and_then(|enabled| enabled.as_bool()) - }) - .unwrap_or(true) +#[derive(Debug, Clone, PartialEq)] +pub enum UpdateCheckMode { + Always, + Once, + Never, } -pub fn get_jdtls_download_url(configuration: &Option) -> Option { - configuration.as_ref().and_then(|configuration| { - configuration - .pointer("/jdtls_download_url") - .and_then(|url| url.as_str()) - .map(|s| s.to_string()) - }) -} - -pub fn get_lombok_download_url(configuration: &Option) -> Option { - configuration.as_ref().and_then(|configuration| { - configuration - .pointer("/lombok_download_url") - .and_then(|url| url.as_str()) - .map(|s| s.to_string()) - }) +impl Default for UpdateCheckMode { + fn default() -> Self { + UpdateCheckMode::Always + } } -pub fn get_debugger_download_url(configuration: &Option) -> Option { - configuration.as_ref().and_then(|configuration| { - configuration - .pointer("/debugger_download_url") - .and_then(|url| url.as_str()) - .map(|s| s.to_string()) - }) +pub fn get_update_check_mode(configuration: &Option) -> UpdateCheckMode { + if let Some(configuration) = configuration + && let Some(mode_str) = configuration + .pointer("/update_check_mode") + .and_then(|x| x.as_str()) + { + return match mode_str.to_lowercase().as_str() { + "once" => UpdateCheckMode::Once, + "never" => UpdateCheckMode::Never, + "always" => UpdateCheckMode::Always, + _ => UpdateCheckMode::default(), + }; + } + UpdateCheckMode::default() } diff --git a/src/debugger.rs b/src/debugger.rs index a29acae..c0cf973 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,4 +1,8 @@ -use std::{collections::HashMap, fs, path::PathBuf}; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, +}; use serde::{Deserialize, Serialize}; use zed_extension_api::{ @@ -10,9 +14,11 @@ use zed_extension_api::{ }; use crate::{ - config::{get_debugger_download_url, is_check_updates_enabled}, lsp::LspWrapper, - util::{create_path_if_not_exists, get_curr_dir, path_to_string}, + util::{ + ComponentPathResolution, ComponentResolver, create_path_if_not_exists, get_curr_dir, + path_to_string, should_use_local_or_download, + }, }; #[derive(Serialize, Deserialize, Debug)] @@ -61,6 +67,45 @@ const JAVA_DEBUG_PLUGIN_FORK_URL: &str = "https://github.com/zed-industries/java const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/java/com.microsoft.java.debug.plugin/maven-metadata.xml"; +fn is_valid_debug_plugin_jar(path: &Path) -> bool { + if !path.is_file() { + return false; + } + + let has_jar_extension = path + .extension() + .and_then(|ext| ext.to_str()) + .map_or(false, |ext| ext == "jar"); + + let has_correct_name = path + .file_name() + .and_then(|name| name.to_str()) + .map_or(false, |name| { + name.starts_with("com.microsoft.java.debug.plugin") + }); + + has_jar_extension && has_correct_name +} + +fn find_local_debugger(cached_path: &Option) -> Option { + if let Some(path) = cached_path { + if fs::metadata(path).is_ok() { + return Some(path.clone()); + } + } + + if let Ok(entries) = fs::read_dir("debugger") { + for entry in entries.filter_map(Result::ok) { + let path = entry.path(); + if is_valid_debug_plugin_jar(&path) { + return Some(path); + } + } + } + + None +} + pub struct Debugger { lsp: LspWrapper, plugin_path: Option, @@ -83,45 +128,25 @@ impl Debugger { language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - // If update checks are disabled, prefer local installation only. - if !is_check_updates_enabled(configuration) { - if let Some(path) = &self.plugin_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } + let resolver = ComponentResolver { + find_local: &|| find_local_debugger(&self.plugin_path), + component_name: "debugger", + }; - // Try to find any existing debugger jar in the directory - let prefix = "debugger"; - if let Ok(entries) = fs::read_dir(prefix) { - for entry in entries.filter_map(Result::ok) { - let path = entry.path(); - if path.is_file() - && path.extension().and_then(|ext| ext.to_str()) == Some("jar") - && path - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.starts_with("com.microsoft.java.debug.plugin")) - { - self.plugin_path = Some(path.clone()); - return Ok(path); - } - } + match should_use_local_or_download(configuration, &resolver)? { + ComponentPathResolution::LocalPath(path) => { + self.plugin_path = Some(path.clone()); + Ok(path) + } + ComponentPathResolution::ShouldDownload => { + self.get_or_download_fork(language_server_id) } - - return Err("Update checks are disabled and no local debugger installation found. Please enable check_updates_on_startup or manually install the debugger.".to_string()); } - - // 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, configuration) } fn get_or_download_fork( &mut self, - language_server_id: &LanguageServerId, - configuration: &Option, + _language_server_id: &LanguageServerId, ) -> zed::Result { let prefix = "debugger"; let artifact = "com.microsoft.java.debug.plugin"; @@ -129,41 +154,26 @@ impl Debugger { let jar_name = format!("{artifact}-{latest_version}.jar"); let jar_path = PathBuf::from(prefix).join(&jar_name); - // Report checking-for-update status - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - if let Some(path) = &self.plugin_path && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - && path.ends_with(&jar_name) + && path.ends_with(jar_name) { return Ok(path.clone()); } - // Report downloading status - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - create_path_if_not_exists(prefix)?; - let download_url = if let Some(custom_url) = get_debugger_download_url(configuration) { - // Use custom URL directly (no placeholders since we use fixed version) - custom_url - } else { - // Use default fork URL - JAVA_DEBUG_PLUGIN_FORK_URL.to_string() - }; - download_file( - &download_url, + JAVA_DEBUG_PLUGIN_FORK_URL, &path_to_string(jar_path.clone())?, DownloadedFileType::Uncompressed, ) - .map_err(|err| format!("Failed to download java-debug from {}: {err}", download_url))?; + .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) diff --git a/src/java.rs b/src/java.rs index 683fdbe..bb42d17 100644 --- a/src/java.rs +++ b/src/java.rs @@ -74,19 +74,14 @@ impl Java { fn language_server_binary_path( &mut self, language_server_id: &LanguageServerId, - worktree: &Worktree, + configuration: &Option, ) -> zed::Result { - // Use cached path if exists - if let Some(path) = &self.cached_binary_path && metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } - let configuration = - self.language_server_workspace_configuration(language_server_id, worktree)?; - // Check for latest version set_language_server_installation_status( language_server_id, @@ -300,7 +295,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)?, &configuration, worktree, lombok_jvm_arg.into_iter().collect(), diff --git a/src/jdtls.rs b/src/jdtls.rs index ea67fa8..9f0fb7b 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,15 +15,13 @@ use zed_extension_api::{ }; use crate::{ - config::{ - get_jdtls_download_url, get_lombok_download_url, is_check_updates_enabled, - is_java_autodownload, - }, + config::is_java_autodownload, 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, - remove_all_files_except, + ComponentPathResolution, ComponentResolver, 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, + should_use_local_or_download, }, }; @@ -158,188 +156,109 @@ pub fn try_to_fetch_and_install_latest_jdtls( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - - // If update checks are disabled, prefer local installation only. - if !is_check_updates_enabled(configuration) { - if let Some(local_version) = find_latest_local_jdtls() { - return Ok(local_version); - } - return Err("Update checks are disabled and no local JDTLS installation found. Please enable check_updates_on_startup or manually install JDTLS.".to_string()); - } - - // Report checking‐for‐update status - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - - // Check for a custom download URL in configuration. If present, use it directly. - if let Some(custom_url) = get_jdtls_download_url(configuration) { - // derive a directory name from the filename of the URL - let filename = custom_url - .split('/') - .last() - .unwrap_or("jdtls.tar.gz") - .to_string(); - let build_directory_name = filename.trim_end_matches(".tar.gz"); - let build_path = prefix.join(build_directory_name); - let binary_path = build_path.join("bin").join(get_binary_name()); - - if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - // mark status as downloading - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - // ensure install directory exists - create_path_if_not_exists(&prefix)?; - - // download the archive from the custom URL - download_file( - &custom_url, - path_to_string(build_path.clone())?.as_str(), - DownloadedFileType::GzipTar, - )?; - - // make the binary executable (on Unix‐style platforms) - make_file_executable(path_to_string(binary_path)?.as_str())?; + let resolver = ComponentResolver { + find_local: &find_latest_local_jdtls, + component_name: "jdtls", + }; - // remove older versions, keep only this directory - let _ = remove_all_files_except(prefix, build_directory_name); + match should_use_local_or_download(configuration, &resolver)? { + ComponentPathResolution::LocalPath(path) => Ok(path), + ComponentPathResolution::ShouldDownload => { + 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 prefix = PathBuf::from(JDTLS_INSTALL_PATH); + + 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, + 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.as_str()); + } + + // return jdtls base path + Ok(build_path) } - - return Ok(build_path); - } - - // No custom URL. - // fetch the list of latest versions from repository tags - let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; - // attempt to download the “latest” milestone - 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 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, - if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - - let download_url = format!( - "https://download.eclipse.org/jdtls/milestones/{latest_version}/{latest_version_build}" - ); - - download_file( - &download_url, - 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.as_str()); } - - // return jdtls base path - Ok(build_path) } pub fn try_to_fetch_and_install_latest_lombok( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - // If update checks are disabled, try to use local cache first - if !is_check_updates_enabled(configuration) { - if let Some(local_version) = find_latest_local_lombok() { - return Ok(local_version); - } - return Err("Update checks are disabled and no local Lombok installation found. Please enable check_updates_on_startup or manually install Lombok.".to_string()); - } + let resolver = ComponentResolver { + find_local: &find_latest_local_lombok, + component_name: "lombok", + }; - let prefix = LOMBOK_INSTALL_PATH; - - // Check if custom download URL is configured - if let Some(custom_url) = get_lombok_download_url(configuration) { - // Use custom URL directly without version checking - // Extract filename from URL - let jar_name = custom_url - .split('/') - .last() - .unwrap_or("lombok.jar") - .to_string(); - let jar_path = Path::new(prefix).join(&jar_name); - - // If not installed, download it - if !metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { + match should_use_local_or_download(configuration, &resolver)? { + ComponentPathResolution::LocalPath(path) => Ok(path), + ComponentPathResolution::ShouldDownload => { set_language_server_installation_status( language_server_id, - &LanguageServerInstallationStatus::Downloading, + &LanguageServerInstallationStatus::CheckingForUpdate, ); - create_path_if_not_exists(prefix)?; - - download_file( - &custom_url, - path_to_string(jar_path.clone())?.as_str(), - DownloadedFileType::Uncompressed, - )?; - let _ = remove_all_files_except(prefix, jar_name.as_str()); + 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); + + // 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_path_if_not_exists(prefix)?; + 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) } - - return Ok(jar_path); } - - // Default behavior: get version from GitHub tags - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - - let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO)?; - 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_path_if_not_exists(prefix)?; - - let download_url = format!("https://projectlombok.org/downloads/{jar_name}"); - - download_file( - &download_url, - 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 download_jdtls_milestone(version: &str) -> zed::Result { diff --git a/src/util.rs b/src/util.rs index b01bb2c..e85d81d 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::{UpdateCheckMode, get_java_home, get_update_check_mode, is_java_autodownload}, jdk::try_to_fetch_and_install_latest_jdk, }; @@ -29,6 +29,24 @@ 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"; + +// Result of component path resolution +pub enum ComponentPathResolution { + /// Local installation found + LocalPath(PathBuf), + /// No local installation, should download + ShouldDownload, +} + +// Configuration for component path resolution +pub struct ComponentResolver<'a> { + /// Function to find latest local installation + pub find_local: &'a dyn Fn() -> Option, + /// Component name for error messages (e.g., "jdtls", "lombok") + pub component_name: &'static str, +} /// Create a Path if it does not exist /// @@ -298,3 +316,47 @@ 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 +/// * `resolver` - Component-specific configuration +/// +/// # Returns +/// * `Ok(ComponentPathResolution)` - Path resolution result +/// * `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, + resolver: &ComponentResolver, +) -> Result { + let update_mode = get_update_check_mode(configuration); + + match update_mode { + UpdateCheckMode::Never => { + if let Some(local) = (resolver.find_local)() { + return Ok(ComponentPathResolution::LocalPath(local)); + } + return Err(format!( + "{} for {}", + NO_LOCAL_INSTALL_NEVER_ERROR, resolver.component_name + )); + } + UpdateCheckMode::Once => { + if let Some(local) = (resolver.find_local)() { + return Ok(ComponentPathResolution::LocalPath(local)); + } + return Ok(ComponentPathResolution::ShouldDownload); + } + UpdateCheckMode::Always => { + return Ok(ComponentPathResolution::ShouldDownload); + } + } +} From 634e6c509b397c8815de8da9bc322e3de5eabe3b Mon Sep 17 00:00:00 2001 From: himicoswilson Date: Thu, 13 Nov 2025 10:07:10 +0800 Subject: [PATCH 3/6] refactor: simplify component and improve documentation --- README.md | 17 +++++----- src/config.rs | 38 ++++++++++------------- src/debugger.rs | 82 ++++++++++++++++++------------------------------- src/java.rs | 2 +- src/jdtls.rs | 29 +++++++---------- src/util.rs | 58 ++++++++++------------------------ 6 files changed, 83 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 471bb47..3c92d26 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,14 @@ Here is a common `settings.json` including the above mentioned configurations: "lombok_support": true, "jdk_auto_download": false, - // Component update mode (default: "always") - // Controls how JDTLS, Lombok, and Debugger are managed: + // Controls when to check for updates for JDTLS, Lombok, and Debugger: // - // "always" - // - Every startup: Checks for and downloads latest versions - // "once" - // - First startup: Downloads the latest versions - // "never" - // - Only uses existing local installations - // - Will fail if components are not already installed - "update_check_mode": "always" + // - "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" + "check_updates": "always" } } } diff --git a/src/config.rs b/src/config.rs index 67f8c3a..528ddda 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 @@ -52,31 +60,19 @@ pub fn is_lombok_enabled(configuration: &Option) -> bool { .unwrap_or(true) } -#[derive(Debug, Clone, PartialEq)] -pub enum UpdateCheckMode { - Always, - Once, - Never, -} - -impl Default for UpdateCheckMode { - fn default() -> Self { - UpdateCheckMode::Always - } -} - -pub fn get_update_check_mode(configuration: &Option) -> UpdateCheckMode { +pub fn get_update_check_mode(configuration: &Option) -> CheckUpdates { if let Some(configuration) = configuration && let Some(mode_str) = configuration - .pointer("/update_check_mode") + .pointer("/check_updates") .and_then(|x| x.as_str()) + .map(|s| s.to_lowercase()) { - return match mode_str.to_lowercase().as_str() { - "once" => UpdateCheckMode::Once, - "never" => UpdateCheckMode::Never, - "always" => UpdateCheckMode::Always, - _ => UpdateCheckMode::default(), + return match mode_str.as_str() { + "once" => CheckUpdates::Once, + "never" => CheckUpdates::Never, + "always" => CheckUpdates::Always, + _ => CheckUpdates::default(), }; } - UpdateCheckMode::default() + CheckUpdates::default() } diff --git a/src/debugger.rs b/src/debugger.rs index c0cf973..46ae7d8 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, - fs, - path::{Path, PathBuf}, + fs::{self, metadata, read_dir}, + path::PathBuf, }; use serde::{Deserialize, Serialize}; @@ -15,10 +15,7 @@ use zed_extension_api::{ use crate::{ lsp::LspWrapper, - util::{ - ComponentPathResolution, ComponentResolver, 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_string, should_use_local_or_download}, }; #[derive(Serialize, Deserialize, Debug)] @@ -63,47 +60,33 @@ 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"; -fn is_valid_debug_plugin_jar(path: &Path) -> bool { - if !path.is_file() { - return false; - } - - let has_jar_extension = path - .extension() - .and_then(|ext| ext.to_str()) - .map_or(false, |ext| ext == "jar"); - - let has_correct_name = path - .file_name() - .and_then(|name| name.to_str()) - .map_or(false, |name| { - name.starts_with("com.microsoft.java.debug.plugin") - }); - - has_jar_extension && has_correct_name -} - -fn find_local_debugger(cached_path: &Option) -> Option { - if let Some(path) = cached_path { - if fs::metadata(path).is_ok() { - return Some(path.clone()); - } - } - - if let Ok(entries) = fs::read_dir("debugger") { - for entry in entries.filter_map(Result::ok) { - let path = entry.path(); - if is_valid_debug_plugin_jar(&path) { - return Some(path); - } - } - } - - None +pub fn find_latest_local_debugger() -> Option { + let prefix = PathBuf::from(DEBUGGER_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 struct Debugger { @@ -128,19 +111,14 @@ impl Debugger { language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let resolver = ComponentResolver { - find_local: &|| find_local_debugger(&self.plugin_path), - component_name: "debugger", - }; + let local = find_latest_local_debugger(); - match should_use_local_or_download(configuration, &resolver)? { - ComponentPathResolution::LocalPath(path) => { + match should_use_local_or_download(configuration, local, "debugger")? { + Some(path) => { self.plugin_path = Some(path.clone()); Ok(path) } - ComponentPathResolution::ShouldDownload => { - self.get_or_download_fork(language_server_id) - } + None => self.get_or_download_fork(language_server_id), } } diff --git a/src/java.rs b/src/java.rs index bb42d17..769d23a 100644 --- a/src/java.rs +++ b/src/java.rs @@ -88,7 +88,7 @@ impl Java { &LanguageServerInstallationStatus::CheckingForUpdate, ); - match try_to_fetch_and_install_latest_jdtls(language_server_id, &configuration) { + match try_to_fetch_and_install_latest_jdtls(language_server_id, configuration) { Ok(path) => { self.cached_binary_path = Some(path.clone()); Ok(path) diff --git a/src/jdtls.rs b/src/jdtls.rs index 9f0fb7b..c7441f9 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -18,10 +18,9 @@ use crate::{ config::is_java_autodownload, jdk::try_to_fetch_and_install_latest_jdk, util::{ - ComponentPathResolution, ComponentResolver, 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, - should_use_local_or_download, + 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, should_use_local_or_download, }, }; @@ -156,14 +155,11 @@ pub fn try_to_fetch_and_install_latest_jdtls( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let resolver = ComponentResolver { - find_local: &find_latest_local_jdtls, - component_name: "jdtls", - }; + let local = find_latest_local_jdtls(); - match should_use_local_or_download(configuration, &resolver)? { - ComponentPathResolution::LocalPath(path) => Ok(path), - ComponentPathResolution::ShouldDownload => { + match should_use_local_or_download(configuration, local, "jdtls")? { + Some(path) => Ok(path), + None => { let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO)?; let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) @@ -217,14 +213,11 @@ pub fn try_to_fetch_and_install_latest_lombok( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let resolver = ComponentResolver { - find_local: &find_latest_local_lombok, - component_name: "lombok", - }; + let local = find_latest_local_lombok(); - match should_use_local_or_download(configuration, &resolver)? { - ComponentPathResolution::LocalPath(path) => Ok(path), - ComponentPathResolution::ShouldDownload => { + match should_use_local_or_download(configuration, local, "lombok")? { + Some(path) => Ok(path), + None => { set_language_server_installation_status( language_server_id, &LanguageServerInstallationStatus::CheckingForUpdate, diff --git a/src/util.rs b/src/util.rs index e85d81d..ddbe8b6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -11,7 +11,7 @@ use zed_extension_api::{ }; use crate::{ - config::{UpdateCheckMode, get_java_home, get_update_check_mode, is_java_autodownload}, + config::{CheckUpdates, get_java_home, get_update_check_mode, is_java_autodownload}, jdk::try_to_fetch_and_install_latest_jdk, }; @@ -32,22 +32,6 @@ 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"; -// Result of component path resolution -pub enum ComponentPathResolution { - /// Local installation found - LocalPath(PathBuf), - /// No local installation, should download - ShouldDownload, -} - -// Configuration for component path resolution -pub struct ComponentResolver<'a> { - /// Function to find latest local installation - pub find_local: &'a dyn Fn() -> Option, - /// Component name for error messages (e.g., "jdtls", "lombok") - pub component_name: &'static str, -} - /// 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 @@ -325,38 +309,30 @@ pub fn remove_all_files_except>(prefix: P, filename: &str) -> zed /// /// # Arguments /// * `configuration` - User configuration JSON -/// * `resolver` - Component-specific configuration +/// * `local` - Optional path to local installation +/// * `component_name` - Component name for error messages (e.g., "jdtls", "lombok", "debugger") /// /// # Returns -/// * `Ok(ComponentPathResolution)` - Path resolution result +/// * `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, - resolver: &ComponentResolver, -) -> Result { - let update_mode = get_update_check_mode(configuration); - - match update_mode { - UpdateCheckMode::Never => { - if let Some(local) = (resolver.find_local)() { - return Ok(ComponentPathResolution::LocalPath(local)); - } - return Err(format!( + local: Option, + component_name: &str, +) -> Result, String> { + match get_update_check_mode(configuration) { + CheckUpdates::Never => match local { + Some(path) => Ok(Some(path)), + None => Err(format!( "{} for {}", - NO_LOCAL_INSTALL_NEVER_ERROR, resolver.component_name - )); - } - UpdateCheckMode::Once => { - if let Some(local) = (resolver.find_local)() { - return Ok(ComponentPathResolution::LocalPath(local)); - } - return Ok(ComponentPathResolution::ShouldDownload); - } - UpdateCheckMode::Always => { - return Ok(ComponentPathResolution::ShouldDownload); - } + NO_LOCAL_INSTALL_NEVER_ERROR, component_name + )), + }, + CheckUpdates::Once => Ok(local), + CheckUpdates::Always => Ok(None), } } From d4b5c2f5bfb7c0ca9c690ba229e72366caf16e2b Mon Sep 17 00:00:00 2001 From: himicoswilson Date: Thu, 13 Nov 2025 11:03:43 +0800 Subject: [PATCH 4/6] Refactor: simplify update check logic with early returns --- src/config.rs | 2 +- src/debugger.rs | 28 ++++---- src/java.rs | 11 ++- src/jdtls.rs | 178 ++++++++++++++++++++++++------------------------ src/util.rs | 4 +- 5 files changed, 111 insertions(+), 112 deletions(-) diff --git a/src/config.rs b/src/config.rs index 528ddda..7ba15a8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -60,7 +60,7 @@ pub fn is_lombok_enabled(configuration: &Option) -> bool { .unwrap_or(true) } -pub fn get_update_check_mode(configuration: &Option) -> CheckUpdates { +pub fn get_check_updates(configuration: &Option) -> CheckUpdates { if let Some(configuration) = configuration && let Some(mode_str) = configuration .pointer("/check_updates") diff --git a/src/debugger.rs b/src/debugger.rs index 46ae7d8..15cf654 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - fs::{self, metadata, read_dir}, - path::PathBuf, -}; +use std::{collections::HashMap, fs, path::PathBuf}; use serde::{Deserialize, Serialize}; use zed_extension_api::{ @@ -69,7 +65,7 @@ const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/j pub fn find_latest_local_debugger() -> Option { let prefix = PathBuf::from(DEBUGGER_INSTALL_PATH); // walk the dir where we install lombok - read_dir(&prefix) + fs::read_dir(&prefix) .map(|entries| { entries .filter_map(Result::ok) @@ -79,7 +75,7 @@ pub fn find_latest_local_debugger() -> Option { 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()?; + let created_time = fs::metadata(&path).and_then(|meta| meta.created()).ok()?; Some((path, created_time)) }) .max_by_key(|&(_, time)| time) @@ -111,15 +107,19 @@ impl Debugger { language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let local = find_latest_local_debugger(); + // 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); - match should_use_local_or_download(configuration, local, "debugger")? { - Some(path) => { - self.plugin_path = Some(path.clone()); - Ok(path) - } - None => self.get_or_download_fork(language_server_id), + // 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) } fn get_or_download_fork( diff --git a/src/java.rs b/src/java.rs index 769d23a..ad92845 100644 --- a/src/java.rs +++ b/src/java.rs @@ -76,6 +76,8 @@ impl Java { language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { + // Use cached path if exists + if let Some(path) = &self.cached_binary_path && metadata(path).is_ok_and(|stat| stat.is_file()) { @@ -107,7 +109,7 @@ impl Java { fn lombok_jar_path( &mut self, language_server_id: &LanguageServerId, - worktree: &Worktree, + configuration: &Option, ) -> zed::Result { if let Some(path) = &self.cached_lombok_path && fs::metadata(path).is_ok_and(|stat| stat.is_file()) @@ -115,10 +117,7 @@ impl Java { return Ok(path.clone()); } - let configuration = - self.language_server_workspace_configuration(language_server_id, worktree)?; - - match try_to_fetch_and_install_latest_lombok(language_server_id, &configuration) { + match try_to_fetch_and_install_latest_lombok(language_server_id, configuration) { Ok(path) => { self.cached_lombok_path = Some(path.clone()); Ok(path) @@ -276,7 +275,7 @@ 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, worktree)?; + let lombok_jar_path = self.lombok_jar_path(language_server_id, &configuration)?; let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path))?; Some(format!("-javaagent:{canonical_lombok_jar_path}")) diff --git a/src/jdtls.rs b/src/jdtls.rs index c7441f9..0971935 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -155,103 +155,103 @@ pub fn try_to_fetch_and_install_latest_jdtls( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let local = find_latest_local_jdtls(); - - match should_use_local_or_download(configuration, local, "jdtls")? { - Some(path) => Ok(path), - 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 prefix = PathBuf::from(JDTLS_INSTALL_PATH); - - 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, - 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.as_str()); - } - - // return jdtls base path - Ok(build_path) - } + // 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()) + .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 prefix = PathBuf::from(JDTLS_INSTALL_PATH); + + 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, + 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.as_str()); } + + // return jdtls base path + Ok(build_path) } pub fn try_to_fetch_and_install_latest_lombok( language_server_id: &LanguageServerId, configuration: &Option, ) -> zed::Result { - let local = find_latest_local_lombok(); - - match should_use_local_or_download(configuration, local, "lombok")? { - Some(path) => Ok(path), - None => { - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - - 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); - - // 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_path_if_not_exists(prefix)?; - 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) - } + // 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, + ); + + 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); + + // 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_path_if_not_exists(prefix)?; + 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 download_jdtls_milestone(version: &str) -> zed::Result { diff --git a/src/util.rs b/src/util.rs index ddbe8b6..f82e125 100644 --- a/src/util.rs +++ b/src/util.rs @@ -11,7 +11,7 @@ use zed_extension_api::{ }; use crate::{ - config::{CheckUpdates, get_java_home, get_update_check_mode, is_java_autodownload}, + config::{CheckUpdates, get_check_updates, get_java_home, is_java_autodownload}, jdk::try_to_fetch_and_install_latest_jdk, }; @@ -324,7 +324,7 @@ pub fn should_use_local_or_download( local: Option, component_name: &str, ) -> Result, String> { - match get_update_check_mode(configuration) { + match get_check_updates(configuration) { CheckUpdates::Never => match local { Some(path) => Ok(Some(path)), None => Err(format!( From 333106b2c176128e47ccbd5dc4c42616751b43d5 Mon Sep 17 00:00:00 2001 From: himicoswilson Date: Thu, 13 Nov 2025 16:21:05 +0800 Subject: [PATCH 5/6] Refactor: update stale comments and unify return types to zed::Result --- src/debugger.rs | 2 +- src/util.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 15cf654..37bea3e 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -64,7 +64,7 @@ const MAVEN_METADATA_URL: &str = "https://repo1.maven.org/maven2/com/microsoft/j pub fn find_latest_local_debugger() -> Option { let prefix = PathBuf::from(DEBUGGER_INSTALL_PATH); - // walk the dir where we install lombok + // walk the dir where we install debugger fs::read_dir(&prefix) .map(|entries| { entries diff --git a/src/util.rs b/src/util.rs index f82e125..bb81883 100644 --- a/src/util.rs +++ b/src/util.rs @@ -323,7 +323,7 @@ pub fn should_use_local_or_download( configuration: &Option, local: Option, component_name: &str, -) -> Result, String> { +) -> zed::Result> { match get_check_updates(configuration) { CheckUpdates::Never => match local { Some(path) => Ok(Some(path)), From c3b4d81949c91c2f8499d7d131e61a16d254c793 Mon Sep 17 00:00:00 2001 From: himicoswilson Date: Thu, 13 Nov 2025 23:28:42 +0800 Subject: [PATCH 6/6] feat: add support for custom component paths and improve documentation Add configuration options to allow users to specify custom paths for JDTLS, Lombok, and Java Debug components instead of relying on automatic downloads. Changes: - Add get_jdtls_launcher(), get_lombok_jar(), and get_java_debug_jar() config helpers - Support jdtls_launcher, lombok_jar, and java_debug_jar settings - User-provided paths take precedence over managed installations - Update check_updates behavior: ignored for components with custom paths - Refactor component resolution logic for consistent precedence handling Documentation improvements: - Restructure README with dedicated Configuration Options section - Add detailed explanation of check_updates modes and behavior - Document new custom path options with clear examples - Fix typos and improve grammar throughout - Enhance code comments for better clarity --- README.md | 12 +++++++++--- src/config.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ src/debugger.rs | 9 +++++++++ src/java.rs | 28 +++++++++++++++++++++------ 4 files changed, 91 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3c92d26..62d7d06 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,20 @@ Here is a common `settings.json` including the above mentioned configurations: "lombok_support": true, "jdk_auto_download": false, - // Controls when to check for updates for JDTLS, Lombok, and Debugger: - // + // 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" - "check_updates": "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 7ba15a8..a2f45b6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -76,3 +76,54 @@ pub fn get_check_updates(configuration: &Option) -> CheckUpdates { } 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 37bea3e..aac5135 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -10,6 +10,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_string, should_use_local_or_download}, }; @@ -106,11 +107,19 @@ impl Debugger { &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")? diff --git a/src/java.rs b/src/java.rs index ad92845..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, @@ -110,7 +110,16 @@ impl Java { &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()) { @@ -275,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, &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))?; Some(format!("-javaagent:{canonical_lombok_jar_path}")) @@ -285,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 { @@ -303,9 +319,9 @@ impl Extension for Java { } // download debugger if not exists - if let Err(err) = self - .debugger()? - .get_or_download(language_server_id, &configuration) + if let Err(err) = + self.debugger()? + .get_or_download(language_server_id, &configuration, worktree) { println!("Failed to download debugger: {err}"); };