Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down
76 changes: 76 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value>, worktree: &Worktree) -> Option<String> {
// try to read the value from settings
if let Some(configuration) = configuration
Expand Down Expand Up @@ -51,3 +59,71 @@ pub fn is_lombok_enabled(configuration: &Option<Value>) -> bool {
})
.unwrap_or(true)
}

pub fn get_check_updates(configuration: &Option<Value>) -> 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<Value>, worktree: &Worktree) -> Option<String> {
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<Value>, worktree: &Worktree) -> Option<String> {
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<Value>, worktree: &Worktree) -> Option<String> {
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
}
46 changes: 45 additions & 1 deletion src/debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<PathBuf> {
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<PathBuf>,
Expand All @@ -80,10 +106,28 @@ impl Debugger {
pub fn get_or_download(
&mut self,
language_server_id: &LanguageServerId,
configuration: &Option<Value>,
worktree: &Worktree,
) -> zed::Result<PathBuf> {
// 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)
}

Expand Down
40 changes: 32 additions & 8 deletions src/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,6 +74,7 @@ impl Java {
fn language_server_binary_path(
&mut self,
language_server_id: &LanguageServerId,
configuration: &Option<Value>,
) -> zed::Result<PathBuf> {
// Use cached path if exists

Expand All @@ -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)
Expand All @@ -105,14 +106,27 @@ impl Java {
}
}

fn lombok_jar_path(&mut self, language_server_id: &LanguageServerId) -> zed::Result<PathBuf> {
fn lombok_jar_path(
&mut self,
language_server_id: &LanguageServerId,
configuration: &Option<Value>,
worktree: &Worktree,
) -> zed::Result<PathBuf> {
// 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)
Expand Down Expand Up @@ -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}"))
Expand All @@ -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 {
Expand All @@ -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(),
Expand All @@ -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}");
};

Expand Down
20 changes: 19 additions & 1 deletion src/jdtls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand Down Expand Up @@ -153,7 +153,16 @@ pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option<String> {

pub fn try_to_fetch_and_install_latest_jdtls(
language_server_id: &LanguageServerId,
configuration: &Option<Value>,
) -> zed::Result<PathBuf> {
// 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())
Expand Down Expand Up @@ -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<Value>,
) -> zed::Result<PathBuf> {
// 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,
Expand Down
40 changes: 39 additions & 1 deletion src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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
///
Expand Down Expand Up @@ -298,3 +300,39 @@ pub fn remove_all_files_except<P: AsRef<Path>>(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<Value>,
local: Option<PathBuf>,
component_name: &str,
) -> zed::Result<Option<PathBuf>> {
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),
}
}