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), + } +}