diff --git a/src/debugger.rs b/src/debugger.rs index 67371df..be05f5f 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -13,7 +13,7 @@ use crate::{ config::get_java_debug_jar, lsp::LspWrapper, util::{ - create_path_if_not_exists, get_curr_dir, path_to_quoted_string, + create_path_if_not_exists, get_curr_dir, mark_checked_once, path_to_quoted_string, should_use_local_or_download, }, }; @@ -124,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); } @@ -162,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) } @@ -263,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 aab5f47..2dcc829 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_quoted_string, remove_all_files_except}; +use crate::util::{ + get_curr_dir, mark_checked_once, path_to_quoted_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 da915fb..8d236ff 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -19,8 +19,8 @@ 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, - remove_all_files_except, should_use_local_or_download, + get_java_major_version, get_latest_versions_from_tag, mark_checked_once, + path_to_quoted_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 ffb573c..e3166b9 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), ); } @@ -253,7 +293,7 @@ fn get_tag_at(github_tags: &Value, index: usize) -> Option<&str> { /// 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) + format!("\"{path_str}\"") } else { path_str } @@ -350,6 +390,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, @@ -362,7 +403,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), } }