From 6388329352e3a15019373c3708e15a579b40ea4c Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Mon, 22 Dec 2025 00:53:02 +0100 Subject: [PATCH 1/4] fix(update): Implement proper "once" behavior for check_updates setting 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 | 13 +++++++-- src/jdk.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++---- src/jdtls.rs | 19 ++++++++++--- src/util.rs | 62 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 67371df..c2eaf52 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -10,10 +10,10 @@ use zed_extension_api::{ }; use crate::{ - config::get_java_debug_jar, + config::{CheckUpdates, get_check_updates, 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, }, }; @@ -131,7 +131,14 @@ impl Debugger { return Ok(path); } - self.get_or_download_fork(language_server_id) + let result = self.get_or_download_fork(language_server_id); + + // Mark as checked once if in Once mode and download was successful + if result.is_ok() && get_check_updates(configuration) == CheckUpdates::Once { + let _ = mark_checked_once(DEBUGGER_INSTALL_PATH, "0.53.2"); + } + + result } fn get_or_download_fork( diff --git a/src/jdk.rs b/src/jdk.rs index aab5f47..1c1af3d 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -2,11 +2,17 @@ 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::{ + config::{CheckUpdates, get_check_updates}, + util::{ + get_curr_dir, has_checked_once, mark_checked_once, path_to_quoted_string, + remove_all_files_except, + }, +}; // Errors const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory"; @@ -15,6 +21,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 +53,58 @@ 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 + match get_check_updates(configuration) { + CheckUpdates::Never => { + if let Some(local_path) = find_latest_local_jdk() { + return get_jdk_bin_path(&local_path); + } + return Err( + "Update checks disabled (never) and no local JDK installation found".to_string(), + ); + } + CheckUpdates::Once => { + // If we have a local installation, use it + if let Some(local_path) = find_latest_local_jdk() { + return get_jdk_bin_path(&local_path); + } + + // If we've already checked once, don't check again + if has_checked_once(JDK_INSTALL_PATH) { + return Err( + "Update check already performed once for JDK. No local installation found." + .to_string(), + ); + } + } + CheckUpdates::Always => { + // Continue to check for updates + } + } + let version = zed::latest_github_release( CORRETTO_REPO, zed_extension_api::GithubReleaseOptions { @@ -58,7 +114,6 @@ 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 @@ -87,12 +142,21 @@ 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 as checked once if in Once mode + if get_check_updates(configuration) == CheckUpdates::Once { + 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..ad2036b 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,12 +15,12 @@ use zed_extension_api::{ }; use crate::{ - config::is_java_autodownload, + config::{CheckUpdates, get_check_updates, 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_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()); } @@ -204,6 +205,11 @@ pub fn try_to_fetch_and_install_latest_jdtls( let _ = remove_all_files_except(prefix, build_directory.as_str()); } + // Mark as checked once if in Once mode + if get_check_updates(configuration) == CheckUpdates::Once { + let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); + } + // return jdtls base path Ok(build_path) } @@ -250,6 +256,11 @@ pub fn try_to_fetch_and_install_latest_lombok( let _ = remove_all_files_except(prefix, jar_name.as_str()); } + // Mark as checked once if in Once mode + if get_check_updates(configuration) == CheckUpdates::Once { + let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version); + } + // else use it Ok(jar_path) } 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), } } From 050eb1ae1efce590b5f2e50071a05a7a03af7b98 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Mon, 22 Dec 2025 12:14:48 +0100 Subject: [PATCH 2/4] fix(update): Create an update marker regardless of the update mode --- src/debugger.rs | 25 ++++++++++++++----------- src/jdk.rs | 45 +++++++++------------------------------------ src/jdtls.rs | 24 +++++++++++------------- 3 files changed, 34 insertions(+), 60 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index c2eaf52..871959a 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -10,7 +10,7 @@ use zed_extension_api::{ }; use crate::{ - config::{CheckUpdates, get_check_updates, get_java_debug_jar}, + config::get_java_debug_jar, lsp::LspWrapper, util::{ create_path_if_not_exists, get_curr_dir, mark_checked_once, path_to_quoted_string, @@ -124,21 +124,18 @@ 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); } - let result = self.get_or_download_fork(language_server_id); - - // Mark as checked once if in Once mode and download was successful - if result.is_ok() && get_check_updates(configuration) == CheckUpdates::Once { - let _ = mark_checked_once(DEBUGGER_INSTALL_PATH, "0.53.2"); - } + - result + self.get_or_download_fork(language_server_id) } fn get_or_download_fork( @@ -169,6 +166,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) } @@ -270,6 +270,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 1c1af3d..1c11045 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -6,12 +6,9 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::{ - config::{CheckUpdates, get_check_updates}, - util::{ - get_curr_dir, has_checked_once, mark_checked_once, 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 @@ -77,32 +74,10 @@ pub fn try_to_fetch_and_install_latest_jdk( let jdk_path = get_curr_dir()?.join(JDK_INSTALL_PATH); // Check if we should use local installation based on update mode - match get_check_updates(configuration) { - CheckUpdates::Never => { - if let Some(local_path) = find_latest_local_jdk() { - return get_jdk_bin_path(&local_path); - } - return Err( - "Update checks disabled (never) and no local JDK installation found".to_string(), - ); - } - CheckUpdates::Once => { - // If we have a local installation, use it - if let Some(local_path) = find_latest_local_jdk() { - return get_jdk_bin_path(&local_path); - } - - // If we've already checked once, don't check again - if has_checked_once(JDK_INSTALL_PATH) { - return Err( - "Update check already performed once for JDK. No local installation found." - .to_string(), - ); - } - } - CheckUpdates::Always => { - // Continue to check for updates - } + if let Some(path) = + should_use_local_or_download(configuration, find_latest_local_jdk(), JDK_INSTALL_PATH)? + { + return get_jdk_bin_path(&path); } let version = zed::latest_github_release( @@ -145,10 +120,8 @@ pub fn try_to_fetch_and_install_latest_jdk( let _ = remove_all_files_except(&jdk_path, version.as_str()); } - // Mark as checked once if in Once mode - if get_check_updates(configuration) == CheckUpdates::Once { - let _ = mark_checked_once(JDK_INSTALL_PATH, &version); - } + // Always mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(JDK_INSTALL_PATH, &version); get_jdk_bin_path(&install_path) } diff --git a/src/jdtls.rs b/src/jdtls.rs index ad2036b..3ac61a9 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,7 +15,7 @@ use zed_extension_api::{ }; use crate::{ - config::{CheckUpdates, get_check_updates, 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, @@ -158,7 +158,7 @@ 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); } @@ -205,10 +205,8 @@ pub fn try_to_fetch_and_install_latest_jdtls( let _ = remove_all_files_except(prefix, build_directory.as_str()); } - // Mark as checked once if in Once mode - if get_check_updates(configuration) == CheckUpdates::Once { - let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); - } + // Always mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); // return jdtls base path Ok(build_path) @@ -219,9 +217,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); } @@ -256,10 +256,8 @@ pub fn try_to_fetch_and_install_latest_lombok( let _ = remove_all_files_except(prefix, jar_name.as_str()); } - // Mark as checked once if in Once mode - if get_check_updates(configuration) == CheckUpdates::Once { - let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version); - } + // Always mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version); // else use it Ok(jar_path) From 7db1b42ea2ebfe0136ceda8947e7526bde9d0035 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Mon, 22 Dec 2025 12:19:16 +0100 Subject: [PATCH 3/4] Run `cargo fmt --all` --- src/debugger.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 871959a..be05f5f 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -133,8 +133,6 @@ impl Debugger { return Ok(path); } - - self.get_or_download_fork(language_server_id) } From 583e3b0fde4be05e9f2cb26f1a461d6c0d30f012 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Mon, 22 Dec 2025 12:44:52 +0100 Subject: [PATCH 4/4] Create the update marker only after download --- src/jdk.rs | 19 +++++++++---------- src/jdtls.rs | 17 +++++++++++------ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/jdk.rs b/src/jdk.rs index 1c11045..2dcc829 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -80,6 +80,12 @@ pub fn try_to_fetch_and_install_latest_jdk( 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 { @@ -91,13 +97,6 @@ pub fn try_to_fetch_and_install_latest_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, @@ -118,10 +117,10 @@ pub fn try_to_fetch_and_install_latest_jdk( // Remove older versions let _ = remove_all_files_except(&jdk_path, version.as_str()); - } - // Always mark the downloaded version for "Once" mode tracking - let _ = mark_checked_once(JDK_INSTALL_PATH, &version); + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(JDK_INSTALL_PATH, &version); + } get_jdk_bin_path(&install_path) } diff --git a/src/jdtls.rs b/src/jdtls.rs index 3ac61a9..8d236ff 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -164,6 +164,11 @@ pub fn try_to_fetch_and_install_latest_jdtls( } // 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()) @@ -203,10 +208,10 @@ pub fn try_to_fetch_and_install_latest_jdtls( // ...and delete other versions let _ = remove_all_files_except(prefix, build_directory.as_str()); - } - // Always mark the downloaded version for "Once" mode tracking - let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); + } // return jdtls base path Ok(build_path) @@ -254,10 +259,10 @@ pub fn try_to_fetch_and_install_latest_lombok( // ...and delete other versions let _ = remove_all_files_except(prefix, jar_name.as_str()); - } - // Always mark the downloaded version for "Once" mode tracking - let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version); + // Mark the downloaded version for "Once" mode tracking + let _ = mark_checked_once(LOMBOK_INSTALL_PATH, &latest_version); + } // else use it Ok(jar_path)