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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Install the extension via Zeds extension manager. It should work out of the box

- To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`.

- The option to let the extension automatically download a version of OpenJDK can be enabled by setting `jdk_auto_download` to `true`. When enabled, the extension will only download a JDK if no valid java_home is provided or if the specified one does not meet the minimum version requirement. User-provided JDKs **always** take precedence.

Here is a common `settings.json` including the above mentioned configurations:

```jsonc
Expand All @@ -22,6 +24,7 @@ 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
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ pub fn get_java_home(configuration: &Option<Value>, worktree: &Worktree) -> Opti
}
}

pub fn is_java_autodownload(configuration: &Option<Value>) -> bool {
configuration
.as_ref()
.and_then(|configuration| {
configuration
.pointer("/jdk_auto_download")
.and_then(|enabled| enabled.as_bool())
})
.unwrap_or(false)
}

pub fn is_lombok_enabled(configuration: &Option<Value>) -> bool {
configuration
.as_ref()
Expand Down
2 changes: 2 additions & 0 deletions src/java.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod config;
mod debugger;
mod jdk;
mod jdtls;
mod lsp;
mod util;
Expand Down Expand Up @@ -289,6 +290,7 @@ impl Extension for Java {
&configuration,
worktree,
lombok_jvm_arg.into_iter().collect(),
language_server_id,
)?);
}

Expand Down
124 changes: 124 additions & 0 deletions src/jdk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use std::path::{Path, PathBuf};

use zed_extension_api::{
self as zed, Architecture, DownloadedFileType, LanguageServerId,
LanguageServerInstallationStatus, Os, current_platform, download_file,
set_language_server_installation_status,
};

use crate::util::{get_curr_dir, path_to_string, remove_all_files_except};

// Errors
const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory";
const NO_JDK_DIR_ERROR: &str = "No match for jdk or corretto in the extracted directory";

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";

fn build_corretto_url(version: &str, platform: &str, arch: &str) -> String {
let template = match zed::current_platform().0 {
Os::Windows => CORRETTO_WINDOWS_URL_TEMPLATE,
_ => CORRETTO_UNIX_URL_TEMPLATE,
};

template
.replace("{version}", version)
.replace("{platform}", platform)
.replace("{arch}", arch)
}

// For now keep in this file as they are not used anywhere else
// otherwise move to util
fn get_architecture() -> zed::Result<String> {
match zed::current_platform() {
(_, Architecture::Aarch64) => Ok("aarch64".to_string()),
(_, Architecture::X86) => Ok("x86".to_string()),
(_, Architecture::X8664) => Ok("x64".to_string()),
}
}

fn get_platform() -> zed::Result<String> {
match zed::current_platform() {
(Os::Mac, _) => Ok("macosx".to_string()),
(Os::Linux, _) => Ok("linux".to_string()),
(Os::Windows, _) => Ok("windows".to_string()),
}
}

pub fn try_to_fetch_and_install_latest_jdk(
language_server_id: &LanguageServerId,
) -> zed::Result<PathBuf> {
let version = zed::latest_github_release(
CORRETTO_REPO,
zed_extension_api::GithubReleaseOptions {
require_assets: false,
pre_release: false,
},
)?
.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,
&LanguageServerInstallationStatus::Downloading,
);

let platform = get_platform()?;
let arch = get_architecture()?;

download_file(
build_corretto_url(&version, &platform, &arch).as_str(),
path_to_string(install_path.clone())?.as_str(),
match zed::current_platform().0 {
Os::Windows => DownloadedFileType::Zip,
_ => DownloadedFileType::GzipTar,
},
)?;

// Remove older versions
let _ = remove_all_files_except(jdk_path, version.as_str());
}

// 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)?;

Ok(install_path
.join(extracted_dir)
.join(match current_platform().0 {
Os::Mac => "Contents/Home/bin",
_ => "bin",
}))
}

fn get_extracted_dir(path: &Path) -> zed::Result<String> {
let Ok(mut entries) = path.read_dir() else {
return Err(JDK_DIR_ERROR.to_string());
};

match entries.find_map(|entry| {
let entry = entry.ok()?;
let file_name = entry.file_name();
let name_str = file_name.to_string_lossy().to_string();

if name_str.contains("jdk") || name_str.contains("corretto") {
Some(name_str)
} else {
None
}
}) {
Some(dir_path) => Ok(dir_path),
None => Err(NO_JDK_DIR_ERROR.to_string()),
}
}
22 changes: 16 additions & 6 deletions src/jdtls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,42 @@ use zed_extension_api::{
set_language_server_installation_status,
};

use crate::util::{
get_curr_dir, get_java_executable, get_java_major_version, path_to_string,
remove_all_files_except,
use crate::{
config::is_java_autodownload,
jdk::try_to_fetch_and_install_latest_jdk,
util::{
get_curr_dir, get_java_exec_name, get_java_executable, get_java_major_version,
path_to_string, remove_all_files_except,
},
};

const JDTLS_INSTALL_PATH: &str = "jdtls";
const LOMBOK_INSTALL_PATH: &str = "lombok";

// Errors

const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java 21. If you need to run a JVM < 21, you can specify a different one for JDTLS to use by specifying lsp.jdtls.settings.java.home in the settings";
const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java version 21 to run. You can either specify a different JDK to use by configuring lsp.jdtls.settings.java_home to point to a different JDK, or set lsp.jdtls.settings.jdk_auto_download to true to let the extension automatically download one for you.";

pub fn build_jdtls_launch_args(
jdtls_path: &PathBuf,
configuration: &Option<Value>,
worktree: &Worktree,
jvm_args: Vec<String>,
language_server_id: &LanguageServerId,
) -> zed::Result<Vec<String>> {
if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) {
return Ok(vec![jdtls_launcher]);
}

let java_executable = get_java_executable(configuration, worktree)?;
let mut java_executable = get_java_executable(configuration, worktree, language_server_id)?;
let java_major_version = get_java_major_version(&java_executable)?;
if java_major_version < 21 {
return Err(JAVA_VERSION_ERROR.to_string());
if is_java_autodownload(configuration) {
java_executable =
try_to_fetch_and_install_latest_jdk(language_server_id)?.join(get_java_exec_name());
} else {
return Err(JAVA_VERSION_ERROR.to_string());
}
}

let extension_workdir = get_curr_dir()?;
Expand Down
46 changes: 35 additions & 11 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ use std::{
fs,
path::{Path, PathBuf},
};
use zed_extension_api::{self as zed, Command, Os, Worktree, current_platform, serde_json::Value};
use zed_extension_api::{
self as zed, Command, LanguageServerId, Os, Worktree, current_platform, serde_json::Value,
};

use crate::config::get_java_home;
use crate::{
config::{get_java_home, is_java_autodownload},
jdk::try_to_fetch_and_install_latest_jdk,
};

// Errors
const EXPAND_ERROR: &str = "Failed to expand ~";
Expand Down Expand Up @@ -59,9 +64,10 @@ pub fn get_curr_dir() -> zed::Result<PathBuf> {
}

/// Retrieve the path to a java exec either:
/// - defined by the user in `settings.json`
/// - defined by the user in `settings.json` under option `java_home`
/// - from PATH
/// - from JAVA_HOME
/// - from the bundled OpenJDK if option `jdk_auto_download` is true
///
/// # Arguments
///
Expand All @@ -79,11 +85,9 @@ pub fn get_curr_dir() -> zed::Result<PathBuf> {
pub fn get_java_executable(
configuration: &Option<Value>,
worktree: &Worktree,
language_server_id: &LanguageServerId,
) -> zed::Result<PathBuf> {
let java_executable_filename = match current_platform().0 {
Os::Windows => "java.exe",
_ => "java",
};
let java_executable_filename = get_java_exec_name();

// Get executable from $JAVA_HOME
if let Some(java_home) = get_java_home(configuration, worktree) {
Expand All @@ -93,10 +97,30 @@ pub fn get_java_executable(
return Ok(java_executable);
}
// If we can't, try to get it from $PATH
worktree
.which(java_executable_filename)
.map(PathBuf::from)
.ok_or_else(|| JAVA_EXEC_NOT_FOUND_ERROR.to_string())
if let Some(java_home) = worktree.which(java_executable_filename.as_str()) {
return Ok(PathBuf::from(java_home));
}

// 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)
);
}

Err(JAVA_EXEC_NOT_FOUND_ERROR.to_string())
}

/// Retrieve the executable name for Java on this platform
///
/// # Returns
///
/// Returns the executable java name
pub fn get_java_exec_name() -> String {
match current_platform().0 {
Os::Windows => "java.exe".to_string(),
_ => "java".to_string(),
}
}

/// Retrieve the java major version accessible by the extension
Expand Down