From 8a87341e2cc3f8143d0fca19ccaad1f59bd43e44 Mon Sep 17 00:00:00 2001 From: Riccardo Strina Date: Sat, 13 Jun 2026 17:13:38 +0200 Subject: [PATCH] Implement Component and LanguageServer traits - Prepare the extension to offer multiple LSPs - Standardize retrieval and download of components - Simplify debugger interaction with jdtls --- src/component.rs | 49 ++++ src/debugger.rs | 139 +++++----- src/java.rs | 569 ++++++----------------------------------- src/jdk.rs | 203 ++++++++------- src/jdtls.rs | 409 ++++++++++++++--------------- src/jdtls_server.rs | 375 +++++++++++++++++++++++++++ src/language_server.rs | 53 ++++ src/lsp.rs | 163 +++++------- src/proxy.rs | 260 +++++++++++-------- src/util.rs | 11 +- 10 files changed, 1170 insertions(+), 1061 deletions(-) create mode 100644 src/component.rs create mode 100644 src/jdtls_server.rs create mode 100644 src/language_server.rs diff --git a/src/component.rs b/src/component.rs new file mode 100644 index 0000000..be1577e --- /dev/null +++ b/src/component.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; + +use zed_extension_api::{self as zed, LanguageServerId, Worktree, serde_json::Value}; + +use crate::util::should_use_local_or_download; + +pub trait Component { + const INSTALL_PATH: &'static str; + + fn find_local(&self) -> Option; + + fn loaded(&self) -> bool; + + fn fetch_latest_version(&self) -> zed::Result; + + fn download( + &mut self, + version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result; + + fn get_or_download( + &mut self, + language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, + ) -> zed::Result { + if let Some(path) = self.user_configured_path(configuration, worktree) { + return Ok(PathBuf::from(path)); + } + + if let Some(path) = + should_use_local_or_download(configuration, self.find_local(), Self::INSTALL_PATH)? + { + return Ok(path); + } + + let version = self.fetch_latest_version()?; + self.download(&version, language_server_id) + } + + fn user_configured_path( + &self, + _configuration: &Option, + _worktree: &Worktree, + ) -> Option { + None + } +} diff --git a/src/debugger.rs b/src/debugger.rs index 4507a02..e364000 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -10,8 +10,9 @@ use zed_extension_api::{ }; use crate::{ + component::Component, config::get_java_debug_jar, - lsp::LspWrapper, + lsp, util::{ ArgsStringOrList, create_path_if_not_exists, get_curr_dir, mark_checked_once, path_to_string, should_use_local_or_download, @@ -66,15 +67,13 @@ const JAVA_DEBUG_PLUGIN_FORK_URL: &str = "https://github.com/zed-industries/java 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 { +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") }) @@ -90,52 +89,12 @@ pub fn find_latest_local_debugger() -> Option { } pub struct Debugger { - lsp: LspWrapper, plugin_path: Option, } impl Debugger { - pub fn new(lsp: LspWrapper) -> Debugger { - Debugger { - plugin_path: None, - lsp, - } - } - - pub fn loaded(&self) -> bool { - self.plugin_path.is_some() - } - - 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_INSTALL_PATH, - ) - .map_err(|err| format!("Failed to resolve debugger installation: {err}"))? - { - self.plugin_path = Some(path.clone()); - return Ok(path); - } - - self.get_or_download_fork(language_server_id) + pub fn new() -> Debugger { + Debugger { plugin_path: None } } fn get_or_download_fork( @@ -283,16 +242,13 @@ impl Debugger { Ok(jar_path) } - pub fn start_session(&self) -> zed::Result { - let port = self - .lsp - .get() - .map_err(|err| format!("Failed to acquire LSP client lock: {err}"))? - .request::( - "workspace/executeCommand", - json!({ "command": "vscode.java.startDebugSession" }), - ) - .map_err(|err| format!("Failed to start debug session via LSP: {err}"))?; + pub fn start_session(&self, workspace: &str) -> zed::Result { + let port = lsp::request::( + workspace, + "workspace/executeCommand", + json!({ "command": "vscode.java.startDebugSession" }), + ) + .map_err(|err| format!("Failed to start debug session via LSP: {err}"))?; Ok(TcpArgumentsTemplate { host: None, @@ -325,11 +281,7 @@ impl Debugger { .cloned() .collect::>(); - let entries = self - .lsp - .get() - .map_err(|err| format!("Failed to acquire LSP client lock: {err}"))? - .resolve_main_class(arguments) + let entries = lsp::resolve_main_class(&workspace_folder, arguments) .map_err(|err| format!("Failed to resolve main class: {err}"))? .into_iter() .filter(|entry| { @@ -382,11 +334,7 @@ impl Debugger { let arguments = vec![main_class.clone(), project_name.clone(), scope.clone()]; - let result = self - .lsp - .get() - .map_err(|err| format!("Failed to acquire LSP client lock: {err}"))? - .resolve_class_path(arguments) + let result = lsp::resolve_class_path(&workspace_folder, arguments) .map_err(|err| format!("Failed to resolve classpath: {err}"))?; for resolved in result { @@ -411,6 +359,10 @@ impl Debugger { Ok(config) } + pub fn plugin_path(&self) -> Option<&PathBuf> { + self.plugin_path.as_ref() + } + pub fn inject_plugin_into_options( &self, initialization_options: Option, @@ -456,3 +408,58 @@ impl Debugger { } } } + +impl Component for Debugger { + const INSTALL_PATH: &'static str = DEBUGGER_INSTALL_PATH; + + fn find_local(&self) -> Option { + find_latest_local_debugger() + } + + fn loaded(&self) -> bool { + self.plugin_path.is_some() + } + + fn fetch_latest_version(&self) -> zed::Result { + Ok("0.53.2".to_string()) + } + + fn download( + &mut self, + _version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result { + self.get_or_download_fork(language_server_id) + } + + fn get_or_download( + &mut self, + language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, + ) -> zed::Result { + if let Some(jar_path) = self.user_configured_path(configuration, worktree) { + let path = PathBuf::from(&jar_path); + self.plugin_path = Some(path.clone()); + return Ok(path); + } + + if let Some(path) = + should_use_local_or_download(configuration, self.find_local(), Self::INSTALL_PATH) + .map_err(|err| format!("Failed to resolve debugger installation: {err}"))? + { + self.plugin_path = Some(path.clone()); + return Ok(path); + } + + self.get_or_download_fork(language_server_id) + } + + fn user_configured_path( + &self, + configuration: &Option, + worktree: &Worktree, + ) -> Option { + get_java_debug_jar(configuration, worktree) + } +} diff --git a/src/java.rs b/src/java.rs index 29061d5..042af85 100644 --- a/src/java.rs +++ b/src/java.rs @@ -1,159 +1,102 @@ +mod component; mod config; mod debugger; mod jdk; mod jdtls; +mod jdtls_server; +mod language_server; mod lsp; mod proxy; mod util; -use std::{ - env, - fs::{self, metadata}, - path::PathBuf, - str::FromStr, -}; +use std::str::FromStr; use zed_extension_api::{ - self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, Extension, - LanguageServerId, LanguageServerInstallationStatus, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, Worktree, - lsp::{Completion, CompletionKind, Symbol, SymbolKind}, + self as zed, CodeLabel, DebugAdapterBinary, DebugTaskDefinition, Extension, LanguageServerId, + StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, Worktree, + lsp::{Completion, Symbol}, register_extension, serde_json::{Value, json}, - set_language_server_installation_status, - settings::LspSettings, }; -use crate::{ - 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, - get_jdtls_launcher_from_path, try_to_fetch_and_install_latest_jdtls, - try_to_fetch_and_install_latest_lombok, - }, - lsp::LspWrapper, - util::path_to_string, -}; +use crate::{component::Component, jdtls_server::JdtlsServer, language_server::LanguageServer}; const DEBUG_ADAPTER_NAME: &str = "Java"; -const LSP_INIT_ERROR: &str = "Lsp client is not initialized yet"; struct Java { - cached_binary_path: Option, - cached_lombok_path: Option, - cached_proxy_path: Option, - integrations: Option<(LspWrapper, Debugger)>, + jdtls_server: JdtlsServer, } -impl Java { - fn lsp(&mut self) -> zed::Result<&LspWrapper> { - self.integrations - .as_ref() - .ok_or(LSP_INIT_ERROR.to_string()) - .map(|v| &v.0) - } - - fn debugger(&mut self) -> zed::Result<&mut Debugger> { - self.integrations - .as_mut() - .ok_or(LSP_INIT_ERROR.to_string()) - .map(|v| &mut v.1) - } - - fn init(&mut self, worktree: &Worktree) { - // Initialize lsp client and debugger - - if self.integrations.is_none() { - let lsp = LspWrapper::new(worktree.root_path()); - let debugger = Debugger::new(lsp.clone()); - - self.integrations = Some((lsp, debugger)); +impl Extension for Java { + fn new() -> Self + where + Self: Sized, + { + Self { + jdtls_server: JdtlsServer::new(), } } - fn language_server_binary_path( + fn language_server_command( &mut self, language_server_id: &LanguageServerId, - configuration: &Option, - ) -> zed::Result { - // Use cached path if exists - - if let Some(path) = &self.cached_binary_path - && metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } - - // Check for latest version - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - - match try_to_fetch_and_install_latest_jdtls(language_server_id, configuration) { - Ok(path) => { - self.cached_binary_path = Some(path.clone()); - Ok(path) - } - Err(e) => { - if let Some(local_version) = find_latest_local_jdtls() { - self.cached_binary_path = Some(local_version.clone()); - Ok(local_version) - } else { - Err(e) - } - } + worktree: &Worktree, + ) -> zed::Result { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self.jdtls_server.command(language_server_id, worktree), + id => Err(format!("Unknown language server: {id}")), } } - fn lombok_jar_path( + fn language_server_initialization_options( &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); + ) -> zed::Result> { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self + .jdtls_server + .initialization_options(language_server_id, worktree), + _ => Ok(None), } + } - // 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()); + fn language_server_workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self + .jdtls_server + .workspace_configuration(language_server_id, worktree), + _ => Ok(None), } + } - match try_to_fetch_and_install_latest_lombok(language_server_id, configuration) { - Ok(path) => { - self.cached_lombok_path = Some(path.clone()); - Ok(path) - } - Err(e) => { - if let Some(local_version) = find_latest_local_lombok() { - self.cached_lombok_path = Some(local_version.clone()); - Ok(local_version) - } else { - Err(e) - } - } + fn label_for_completion( + &self, + language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self + .jdtls_server + .label_for_completion(language_server_id, completion), + _ => None, } } -} -impl Extension for Java { - fn new() -> Self - where - Self: Sized, - { - Self { - cached_binary_path: None, - cached_lombok_path: None, - cached_proxy_path: None, - integrations: None, + fn label_for_symbol( + &self, + language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + match language_server_id.as_ref() { + JdtlsServer::SERVER_ID => self + .jdtls_server + .label_for_symbol(language_server_id, symbol), + _ => None, } } @@ -164,7 +107,7 @@ impl Extension for Java { _user_provided_debug_adapter_path: Option, worktree: &Worktree, ) -> zed_extension_api::Result { - if !self.debugger().is_ok_and(|v| v.loaded()) { + if !self.jdtls_server.debugger.loaded() { return Err("Debugger plugin is not loaded".to_string()); } @@ -174,18 +117,12 @@ impl Extension for Java { )); } - if self.integrations.is_some() { - self.lsp()? - .switch_workspace(worktree.root_path()) - .map_err(|err| { - format!("Failed to switch LSP workspace for debug adapter: {err}") - })?; - } + let workspace = worktree.root_path(); Ok(DebugAdapterBinary { command: None, arguments: vec![], - cwd: Some(worktree.root_path()), + cwd: Some(workspace.clone()), envs: vec![], request_args: StartDebuggingRequestArguments { request: self @@ -196,13 +133,15 @@ impl Extension for Java { ) .map_err(|err| format!("Failed to determine debug request kind: {err}"))?, configuration: self - .debugger()? + .jdtls_server + .debugger .inject_config(worktree, config.config) .map_err(|err| format!("Failed to inject debug configuration: {err}"))?, }, connection: Some(zed::resolve_tcp_template( - self.debugger()? - .start_session() + self.jdtls_server + .debugger + .start_session(&workspace) .map_err(|err| format!("Failed to start debug session: {err}"))?, )?), }) @@ -235,10 +174,16 @@ impl Extension for Java { &mut self, config: zed::DebugConfig, ) -> zed::Result { - if !self.debugger().is_ok_and(|v| v.loaded()) { + if !self.jdtls_server.debugger.loaded() { return Err("Debugger plugin is not loaded".to_string()); } + let workspace = self + .jdtls_server + .cached_workspace + .as_deref() + .ok_or("LSP workspace not initialized yet")?; + match config.request { zed::DebugRequest::Attach(attach) => { let debug_config = if let Some(process_id) = attach.process_id { @@ -259,8 +204,9 @@ impl Extension for Java { adapter: config.adapter, build: None, tcp_connection: Some( - self.debugger()? - .start_session() + self.jdtls_server + .debugger + .start_session(workspace) .map_err(|err| format!("Failed to start debug session: {err}"))?, ), label: "Attach to Java process".to_string(), @@ -273,363 +219,6 @@ impl Extension for Java { } } } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result { - let current_dir = - env::current_dir().map_err(|err| format!("Failed to get current directory: {err}"))?; - - let configuration = - self.language_server_workspace_configuration(language_server_id, worktree)?; - - let mut env = Vec::new(); - - if let Some(java_home) = get_java_home(&configuration, worktree) { - env.push(("JAVA_HOME".to_string(), java_home)); - } - - let proxy_path = proxy::binary_path( - &mut self.cached_proxy_path, - &configuration, - language_server_id, - worktree, - ) - .map_err(|err| format!("Failed to get proxy binary path: {err}"))?; - - // proxy takes: workdir, bin, [args...] - let mut args = vec![ - path_to_string(current_dir.clone()) - .map_err(|err| format!("Failed to convert current directory to string: {err}"))?, - ]; - - // 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, &configuration, worktree) - .map_err(|err| format!("Failed to get Lombok jar path: {err}"))?; - let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path)) - .map_err(|err| format!("Failed to convert Lombok jar path to string: {err}"))?; - - Some(format!("-javaagent:{canonical_lombok_jar_path}")) - } else { - None - }; - - self.init(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 { - args.push(format!("--jvm-arg={lombok_jvm_arg}")); - } - } else { - // otherwise we launch ourselves - args.extend( - build_jdtls_launch_args( - &self - .language_server_binary_path(language_server_id, &configuration) - .map_err(|err| format!("Failed to get JDTLS binary path: {err}"))?, - &configuration, - worktree, - lombok_jvm_arg.into_iter().collect(), - language_server_id, - ) - .map_err(|err| format!("Failed to build JDTLS launch arguments: {err}"))?, - ); - } - - // download debugger if not exists - if let Err(err) = - self.debugger()? - .get_or_download(language_server_id, &configuration, worktree) - { - println!("Failed to download debugger: {err}"); - }; - - self.lsp()? - .switch_workspace(worktree.root_path()) - .map_err(|err| format!("Failed to switch LSP workspace: {err}"))?; - - Ok(zed::Command { - command: proxy_path, - args, - env, - }) - } - - fn language_server_initialization_options( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result> { - if self.integrations.is_some() { - self.lsp()? - .switch_workspace(worktree.root_path()) - .map_err(|err| { - format!("Failed to switch LSP workspace for initialization: {err}") - })?; - } - - let mut options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.initialization_options) - .map_err(|err| format!("Failed to get LSP settings for worktree: {err}"))? - .unwrap_or_else(|| json!({})); - - // Inject workspaceFolders default if not already set by the user - let options_obj = options - .as_object_mut() - .ok_or_else(|| "initialization_options is not a JSON object".to_string())?; - if !options_obj.contains_key("workspaceFolders") { - let uri = util::path_to_file_uri(&worktree.root_path()); - options_obj.insert("workspaceFolders".to_string(), json!([uri])); - } - - // Inject extendedClientCapabilities defaults if not already set by the user - let caps = options_obj - .entry("extendedClientCapabilities") - .or_insert_with(|| json!({})); - let caps_obj = caps - .as_object_mut() - .ok_or_else(|| "extendedClientCapabilities is not a JSON object".to_string())?; - caps_obj - .entry("classFileContentsSupport") - .or_insert(json!(true)); - caps_obj - .entry("resolveAdditionalTextEditsSupport") - .or_insert(json!(true)); - - if self.debugger().is_ok_and(|v| v.loaded()) { - return Ok(Some( - self.debugger()? - .inject_plugin_into_options(Some(options)) - .map_err(|err| { - format!("Failed to inject debugger plugin into options: {err}") - })?, - )); - } - - Ok(Some(options)) - } - - fn language_server_workspace_configuration( - &mut self, - language_server_id: &LanguageServerId, - worktree: &Worktree, - ) -> zed::Result> { - if let Ok(Some(settings)) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) - .map(|lsp_settings| lsp_settings.settings) - { - Ok(Some(settings)) - } else { - self.language_server_initialization_options(language_server_id, worktree) - .map(|init_options| { - init_options.and_then(|init_options| init_options.get("settings").cloned()) - }) - } - } - - fn label_for_completion( - &self, - _language_server_id: &LanguageServerId, - completion: Completion, - ) -> Option { - // uncomment when debugging completions - // println!("Java completion: {completion:#?}"); - - completion.kind.and_then(|kind| match kind { - CompletionKind::Field | CompletionKind::Constant => { - let modifiers = match kind { - CompletionKind::Field => "", - CompletionKind::Constant => "static final ", - _ => return None, - }; - let property_type = completion.detail.as_ref().and_then(|detail| { - detail - .split_once(" : ") - .map(|(_, property_type)| format!("{property_type} ")) - })?; - let semicolon = ";"; - let code = format!("{modifiers}{property_type}{}{semicolon}", completion.label); - - Some(CodeLabel { - spans: vec![ - CodeLabelSpan::code_range( - modifiers.len() + property_type.len()..code.len() - semicolon.len(), - ), - CodeLabelSpan::literal(" : ", None), - CodeLabelSpan::code_range( - modifiers.len()..modifiers.len() + property_type.len(), - ), - ], - code, - filter_range: (0..completion.label.len()).into(), - }) - } - CompletionKind::Method => { - let detail = completion.detail?; - let (left, return_type) = detail - .split_once(" : ") - .map(|(left, return_type)| (left, format!("{return_type} "))) - .unwrap_or((&detail, "void".to_string())); - let parameters = left - .find('(') - .map(|parameters_start| &left[parameters_start..]); - let name_and_parameters = - format!("{}{}", completion.label, parameters.unwrap_or("()")); - let braces = " {}"; - let code = format!("{return_type}{name_and_parameters}{braces}"); - let mut spans = vec![CodeLabelSpan::code_range( - return_type.len()..code.len() - braces.len(), - )]; - - if parameters.is_some() { - spans.push(CodeLabelSpan::literal(" : ", None)); - spans.push(CodeLabelSpan::code_range(0..return_type.len())); - } else { - spans.push(CodeLabelSpan::literal(" - ", None)); - spans.push(CodeLabelSpan::literal(detail, None)); - } - - Some(CodeLabel { - spans, - code, - filter_range: (0..completion.label.len()).into(), - }) - } - CompletionKind::Class | CompletionKind::Interface | CompletionKind::Enum => { - let keyword = match kind { - CompletionKind::Class => "class ", - CompletionKind::Interface => "interface ", - CompletionKind::Enum => "enum ", - _ => return None, - }; - let braces = " {}"; - let code = format!("{keyword}{}{braces}", completion.label); - let namespace = completion.detail.and_then(|detail| { - if detail.len() > completion.label.len() { - let prefix_len = detail.len() - completion.label.len() - 1; - Some(detail[..prefix_len].to_string()) - } else { - None - } - }); - let mut spans = vec![CodeLabelSpan::code_range( - keyword.len()..code.len() - braces.len(), - )]; - - if let Some(namespace) = namespace { - spans.push(CodeLabelSpan::literal(format!(" ({namespace})"), None)); - } - - Some(CodeLabel { - spans, - code, - filter_range: (0..completion.label.len()).into(), - }) - } - CompletionKind::Snippet => Some(CodeLabel { - code: String::new(), - spans: vec![CodeLabelSpan::literal( - format!("{} - {}", completion.label, completion.detail?), - None, - )], - filter_range: (0..completion.label.len()).into(), - }), - CompletionKind::Keyword | CompletionKind::Variable => Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..completion.label.len())], - filter_range: (0..completion.label.len()).into(), - code: completion.label, - }), - CompletionKind::Constructor => { - let detail = completion.detail?; - let parameters = &detail[detail.find('(')?..]; - let braces = " {}"; - let code = format!("{}{parameters}{braces}", completion.label); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..code.len() - braces.len())], - code, - filter_range: (0..completion.label.len()).into(), - }) - } - _ => None, - }) - } - - fn label_for_symbol( - &self, - _language_server_id: &LanguageServerId, - symbol: Symbol, - ) -> Option { - let name = &symbol.name; - - match symbol.kind { - SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum => { - let keyword = match symbol.kind { - SymbolKind::Class => "class ", - SymbolKind::Interface => "interface ", - SymbolKind::Enum => "enum ", - _ => return None, - }; - let code = format!("{keyword}{name} {{}}"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], - filter_range: (keyword.len()..keyword.len() + name.len()).into(), - code, - }) - } - SymbolKind::Method | SymbolKind::Function => { - // jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)" - // display: "ReturnType methodName(Type, Type)" (Java declaration order) - let method_name = name.split('(').next().unwrap_or(name); - let after_name = &name[method_name.len()..]; - - let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") { - (p, Some(r)) - } else { - (after_name, None) - }; - - let ret = return_type.unwrap_or("void"); - let class_open = "class _ { "; - let code = format!("{class_open}{ret} {method_name}() {{}} }}"); - - let ret_start = class_open.len(); - let name_start = ret_start + ret.len() + 1; - - // Display: "void methodName(String, int)" - let mut spans = vec![ - CodeLabelSpan::code_range(ret_start..ret_start + ret.len()), - CodeLabelSpan::literal(" ".to_string(), None), - CodeLabelSpan::code_range(name_start..name_start + method_name.len()), - ]; - if !params.is_empty() { - spans.push(CodeLabelSpan::literal(params.to_string(), None)); - } - - // filter on "methodName(params)" portion of displayed text - let type_prefix_len = ret.len() + 1; // "void " - let filter_end = type_prefix_len + method_name.len() + params.len(); - Some(CodeLabel { - spans, - filter_range: (type_prefix_len..filter_end).into(), - code, - }) - } - _ => None, - } - } } register_extension!(Java); diff --git a/src/jdk.rs b/src/jdk.rs index a844c9a..2802da0 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -2,23 +2,123 @@ use std::path::{Path, PathBuf}; use zed_extension_api::{ self as zed, Architecture, DownloadedFileType, LanguageServerId, - LanguageServerInstallationStatus, Os, current_platform, download_file, serde_json::Value, - set_language_server_installation_status, + LanguageServerInstallationStatus, Os, Worktree, current_platform, download_file, + serde_json::Value, set_language_server_installation_status, }; -use crate::util::{ - get_curr_dir, mark_checked_once, path_to_string, remove_all_files_except, - should_use_local_or_download, +use crate::{ + component::Component, + util::{get_curr_dir, mark_checked_once, 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"; -const JDK_INSTALL_PATH: &str = "jdk"; + +pub struct Jdk { + cached_path: Option, +} + +impl Jdk { + pub fn new() -> Self { + Self { cached_path: None } + } + + pub fn get_bin_path( + &mut self, + language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, + ) -> zed::Result { + let install_path = self.get_or_download(language_server_id, configuration, worktree)?; + get_jdk_bin_path(&install_path) + } +} + +impl Component for Jdk { + const INSTALL_PATH: &'static str = "jdk"; + + fn find_local(&self) -> Option { + let jdk_path = get_curr_dir().ok()?.join(Self::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) + } + + fn loaded(&self) -> bool { + self.cached_path.is_some() + } + + fn fetch_latest_version(&self) -> zed::Result { + Ok(zed::latest_github_release( + CORRETTO_REPO, + zed_extension_api::GithubReleaseOptions { + require_assets: false, + pre_release: false, + }, + ) + .map_err(|err| { + format!("Failed to fetch latest Corretto release from {CORRETTO_REPO}: {err}") + })? + .version) + } + + fn download( + &mut self, + version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result { + let jdk_path = get_curr_dir() + .map_err(|err| format!("Failed to get current directory for JDK installation: {err}"))? + .join(Self::INSTALL_PATH); + + let install_path = jdk_path.join(version); + + if !install_path.exists() { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + let platform = get_platform() + .map_err(|err| format!("Failed to detect platform for JDK download: {err}"))?; + let arch = get_architecture() + .map_err(|err| format!("Failed to detect architecture for JDK download: {err}"))?; + + let download_url = build_corretto_url(version, &platform, &arch); + download_file( + download_url.as_str(), + path_to_string(install_path.clone()) + .map_err(|err| format!("Invalid JDK install path {install_path:?}: {err}"))? + .as_str(), + match zed::current_platform().0 { + Os::Windows => DownloadedFileType::Zip, + _ => DownloadedFileType::GzipTar, + }, + ) + .map_err(|err| format!("Failed to download Corretto JDK from {download_url}: {err}"))?; + + let _ = remove_all_files_except(&jdk_path, version); + let _ = mark_checked_once(Self::INSTALL_PATH, version); + } + + self.cached_path = Some(install_path.clone()); + Ok(install_path) + } +} fn build_corretto_url(version: &str, platform: &str, arch: &str) -> String { let template = match zed::current_platform().0 { @@ -32,8 +132,6 @@ fn build_corretto_url(version: &str, platform: &str, arch: &str) -> String { .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 { match zed::current_platform() { (_, Architecture::Aarch64) => Ok("aarch64".to_string()), @@ -50,94 +148,7 @@ 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() - .map_err(|err| format!("Failed to get current directory for JDK installation: {err}"))? - .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) - .map_err(|err| format!("Failed to resolve JDK installation: {err}"))? - { - 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 { - require_assets: false, - pre_release: false, - }, - ) - .map_err(|err| format!("Failed to fetch latest Corretto release from {CORRETTO_REPO}: {err}"))? - .version; - - let install_path = jdk_path.join(&version); - - if !install_path.exists() { - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - - let platform = get_platform() - .map_err(|err| format!("Failed to detect platform for JDK download: {err}"))?; - let arch = get_architecture() - .map_err(|err| format!("Failed to detect architecture for JDK download: {err}"))?; - - let download_url = build_corretto_url(&version, &platform, &arch); - download_file( - download_url.as_str(), - path_to_string(install_path.clone()) - .map_err(|err| format!("Invalid JDK install path {install_path:?}: {err}"))? - .as_str(), - match zed::current_platform().0 { - Os::Windows => DownloadedFileType::Zip, - _ => DownloadedFileType::GzipTar, - }, - ) - .map_err(|err| format!("Failed to download Corretto JDK from {download_url}: {err}"))?; - - // Remove older versions - 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) .map_err(|err| format!("Failed to find JDK directory in {install_path:?}: {err}"))?; diff --git a/src/jdtls.rs b/src/jdtls.rs index 03d01ea..aca1b9c 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,12 +15,13 @@ use zed_extension_api::{ }; use crate::{ - config::is_java_autodownload, - jdk::try_to_fetch_and_install_latest_jdk, + component::Component, + config::{get_lombok_jar, is_java_autodownload}, + jdk::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, mark_checked_once, path_to_string, - remove_all_files_except, should_use_local_or_download, + remove_all_files_except, }, }; @@ -29,10 +30,207 @@ const JDTLS_REPO: &str = "eclipse-jdtls/eclipse.jdt.ls"; const LOMBOK_INSTALL_PATH: &str = "lombok"; const LOMBOK_REPO: &str = "projectlombok/lombok"; -// Errors - 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."; -const JDTLS_VERION_ERROR: &str = "No version to fallback to"; +const JDTLS_VERSION_ERROR: &str = "No version to fallback to"; + +// --- Jdtls Component --- + +pub struct Jdtls { + cached_path: Option, +} + +impl Jdtls { + pub fn new() -> Self { + Self { cached_path: None } + } +} + +impl Component for Jdtls { + const INSTALL_PATH: &'static str = JDTLS_INSTALL_PATH; + + fn find_local(&self) -> Option { + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .filter_map(|path| { + let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + .map(|(path, _)| path) + }) + .ok() + .flatten() + } + + fn loaded(&self) -> bool { + self.cached_path.is_some() + } + + fn fetch_latest_version(&self) -> zed::Result { + let (last, _) = get_latest_versions_from_tag(JDTLS_REPO) + .map_err(|err| format!("Failed to fetch JDTLS versions from {JDTLS_REPO}: {err}"))?; + Ok(last) + } + + fn download( + &mut self, + _version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + let (last, second_last) = get_latest_versions_from_tag(JDTLS_REPO) + .map_err(|err| format!("Failed to fetch JDTLS versions from {JDTLS_REPO}: {err}"))?; + + let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) + .map_or_else( + |_| { + second_last + .ok_or(JDTLS_VERSION_ERROR.to_string()) + .and_then(|fallback| { + download_jdtls_milestone(&fallback) + .map(|milestone| (fallback, milestone.trim_end().to_string())) + }) + }, + |milestone| Ok((last, milestone.trim_end().to_string())), + )?; + + let prefix = PathBuf::from(JDTLS_INSTALL_PATH); + let build_directory = latest_version_build.replace(".tar.gz", ""); + let build_path = prefix.join(&build_directory); + let binary_path = build_path.join("bin").join(get_binary_name()); + + if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + let download_url = format!( + "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" + ); + download_file( + &download_url, + path_to_string(build_path.clone()) + .map_err(|err| format!("Invalid JDTLS build path {build_path:?}: {err}"))? + .as_str(), + DownloadedFileType::GzipTar, + ) + .map_err(|err| format!("Failed to download JDTLS from {download_url}: {err}"))?; + make_file_executable( + path_to_string(&binary_path) + .map_err(|err| format!("Invalid JDTLS binary path {binary_path:?}: {err}"))? + .as_str(), + ) + .map_err(|err| format!("Failed to make JDTLS executable at {binary_path:?}: {err}"))?; + + let _ = remove_all_files_except(prefix, build_directory.as_str()); + let _ = mark_checked_once(JDTLS_INSTALL_PATH, &latest_version); + } + + self.cached_path = Some(build_path.clone()); + Ok(build_path) + } +} + +// --- Lombok Component --- + +pub struct Lombok { + cached_path: Option, +} + +impl Lombok { + pub fn new() -> Self { + Self { cached_path: None } + } +} + +impl Component for Lombok { + const INSTALL_PATH: &'static str = LOMBOK_INSTALL_PATH; + + fn find_local(&self) -> Option { + let prefix = PathBuf::from(LOMBOK_INSTALL_PATH); + read_dir(&prefix) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() + && path.extension().and_then(|ext| ext.to_str()) == Some("jar") + }) + .filter_map(|path| { + let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?; + Some((path, created_time)) + }) + .max_by_key(|&(_, time)| time) + .map(|(path, _)| path) + }) + .ok() + .flatten() + } + + fn loaded(&self) -> bool { + self.cached_path.is_some() + } + + fn fetch_latest_version(&self) -> zed::Result { + let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO) + .map_err(|err| format!("Failed to fetch Lombok versions from {LOMBOK_REPO}: {err}"))?; + Ok(latest_version) + } + + fn download( + &mut self, + version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result { + let prefix = LOMBOK_INSTALL_PATH; + let jar_name = format!("lombok-{version}.jar"); + let jar_path = Path::new(prefix).join(&jar_name); + + if !metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + create_path_if_not_exists(prefix) + .map_err(|err| format!("Failed to create Lombok directory '{prefix}': {err}"))?; + let download_url = format!("https://projectlombok.org/downloads/{jar_name}"); + download_file( + &download_url, + path_to_string(jar_path.clone()) + .map_err(|err| format!("Invalid Lombok jar path {jar_path:?}: {err}"))? + .as_str(), + DownloadedFileType::Uncompressed, + ) + .map_err(|err| format!("Failed to download Lombok from {download_url}: {err}"))?; + + let _ = remove_all_files_except(prefix, jar_name.as_str()); + let _ = mark_checked_once(LOMBOK_INSTALL_PATH, version); + } + + self.cached_path = Some(jar_path.clone()); + Ok(jar_path) + } + + fn user_configured_path( + &self, + configuration: &Option, + worktree: &Worktree, + ) -> Option { + get_lombok_jar(configuration, worktree) + } +} + +// --- JDTLS launch utilities --- /// Parse a JVM memory string (e.g. "2G", "512m", "1024k") into bytes. fn parse_memory_value(s: &str) -> Option { @@ -52,6 +250,7 @@ pub fn build_jdtls_launch_args( worktree: &Worktree, jvm_args: Vec, language_server_id: &LanguageServerId, + jdk: &mut Jdk, ) -> zed::Result> { if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) { return Ok(vec![jdtls_launcher]); @@ -63,10 +262,10 @@ pub fn build_jdtls_launch_args( .map_err(|err| format!("Failed to determine Java version: {err}"))?; if java_major_version < 21 { if is_java_autodownload(configuration) { - java_executable = - try_to_fetch_and_install_latest_jdk(language_server_id, configuration) - .map_err(|err| format!("Failed to auto-download JDK for JDTLS: {err}"))? - .join(get_java_exec_name()); + java_executable = jdk + .get_bin_path(language_server_id, configuration, worktree) + .map_err(|err| format!("Failed to auto-download JDK for JDTLS: {err}"))? + .join(get_java_exec_name()); } else { return Err(JAVA_VERSION_ERROR.to_string()); } @@ -136,51 +335,6 @@ pub fn build_jdtls_launch_args( Ok(args) } -pub fn find_latest_local_jdtls() -> Option { - let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - // walk the dir where we install jdtls - read_dir(&prefix) - .map(|entries| { - entries - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - // get the most recently created subdirectory - .filter_map(|path| { - let created_time = metadata(&path).and_then(|meta| meta.created()).ok()?; - Some((path, created_time)) - }) - .max_by_key(|&(_, time)| time) - // and return it - .map(|(path, _)| path) - }) - .ok() - .flatten() -} - -pub fn find_latest_local_lombok() -> Option { - let prefix = PathBuf::from(LOMBOK_INSTALL_PATH); - // walk the dir where we install lombok - 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 = metadata(&path).and_then(|meta| meta.created()).ok()?; - Some((path, created_time)) - }) - .max_by_key(|&(_, time)| time) - .map(|(path, _)| path) - }) - .ok() - .flatten() -} - pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { let jdtls_executable_filename = match current_platform().0 { Os::Windows => "jdtls.bat", @@ -190,142 +344,6 @@ pub fn get_jdtls_launcher_from_path(worktree: &Worktree) -> Option { worktree.which(jdtls_executable_filename) } -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_INSTALL_PATH) - .map_err(|err| format!("Failed to resolve JDTLS installation: {err}"))? - { - 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) - .map_err(|err| format!("Failed to fetch JDTLS versions from {JDTLS_REPO}: {err}"))?; - - let (latest_version, latest_version_build) = download_jdtls_milestone(last.as_ref()) - .map_or_else( - |_| { - second_last - .ok_or(JDTLS_VERION_ERROR.to_string()) - .and_then(|fallback| { - download_jdtls_milestone(&fallback) - .map(|milestone| (fallback, milestone.trim_end().to_string())) - }) - }, - |milestone| Ok((last, milestone.trim_end().to_string())), - )?; - - let prefix = PathBuf::from(JDTLS_INSTALL_PATH); - - let build_directory = latest_version_build.replace(".tar.gz", ""); - let build_path = prefix.join(&build_directory); - let binary_path = build_path.join("bin").join(get_binary_name()); - - // If latest version isn't installed, - if !metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - let download_url = format!( - "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" - ); - download_file( - &download_url, - path_to_string(build_path.clone()) - .map_err(|err| format!("Invalid JDTLS build path {build_path:?}: {err}"))? - .as_str(), - DownloadedFileType::GzipTar, - ) - .map_err(|err| format!("Failed to download JDTLS from {download_url}: {err}"))?; - make_file_executable( - path_to_string(&binary_path) - .map_err(|err| format!("Invalid JDTLS binary path {binary_path:?}: {err}"))? - .as_str(), - ) - .map_err(|err| format!("Failed to make JDTLS executable at {binary_path:?}: {err}"))?; - - // ...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 - Ok(build_path) -} - -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_INSTALL_PATH, - ) - .map_err(|err| format!("Failed to resolve Lombok installation: {err}"))? - { - return Ok(path); - } - - // Download latest version - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::CheckingForUpdate, - ); - - let (latest_version, _) = get_latest_versions_from_tag(LOMBOK_REPO) - .map_err(|err| format!("Failed to fetch Lombok versions from {LOMBOK_REPO}: {err}"))?; - let prefix = LOMBOK_INSTALL_PATH; - let jar_name = format!("lombok-{latest_version}.jar"); - let jar_path = Path::new(prefix).join(&jar_name); - - // If latest version isn't installed, - if !metadata(&jar_path).is_ok_and(|stat| stat.is_file()) { - // then download it... - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - create_path_if_not_exists(prefix) - .map_err(|err| format!("Failed to create Lombok directory '{prefix}': {err}"))?; - let download_url = format!("https://projectlombok.org/downloads/{jar_name}"); - download_file( - &download_url, - path_to_string(jar_path.clone()) - .map_err(|err| format!("Invalid Lombok jar path {jar_path:?}: {err}"))? - .as_str(), - DownloadedFileType::Uncompressed, - ) - .map_err(|err| format!("Failed to download Lombok from {download_url}: {err}"))?; - - // ...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 - Ok(jar_path) -} - fn download_jdtls_milestone(version: &str) -> zed::Result { String::from_utf8( fetch( @@ -345,13 +363,11 @@ fn download_jdtls_milestone(version: &str) -> zed::Result { fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result { let plugins_dir = jdtls_base_directory.join("plugins"); - // if we have `org.eclipse.equinox.launcher.jar` use that let specific_launcher = plugins_dir.join("org.eclipse.equinox.launcher.jar"); if specific_launcher.is_file() { return Ok(specific_launcher); } - // else get the first file that matches the glob 'org.eclipse.equinox.launcher_*.jar' let entries = read_dir(&plugins_dir).map_err(|err| format!("Failed to read plugins directory: {err}"))?; @@ -368,11 +384,6 @@ fn find_equinox_launcher(jdtls_base_directory: &Path) -> Result } fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { - // Note: the JDTLS data path is where JDTLS stores its own caches. - // In the unlikely event we can't find the canonical OS-Level cache-path, - // we fall back to the the extension's workdir, which may never get cleaned up. - // In future we may want to deliberately manage caches to be able to force-clean them. - let env = worktree.shell_env(); let base_cachedir = match current_platform().0 { Os::Mac => env @@ -405,9 +416,7 @@ fn get_jdtls_data_path(worktree: &Worktree) -> zed::Result { .map(|path| path.join("caches")) })?; - // caches are unique per worktree-root-path let cache_key = worktree.root_path(); - let hex_digest = get_sha1_hex(&cache_key); let unique_dir_name = format!("jdtls-{hex_digest}"); Ok(base_cachedir.join(unique_dir_name)) diff --git a/src/jdtls_server.rs b/src/jdtls_server.rs new file mode 100644 index 0000000..a09d86c --- /dev/null +++ b/src/jdtls_server.rs @@ -0,0 +1,375 @@ +use std::env; + +use zed_extension_api::{ + self as zed, CodeLabel, CodeLabelSpan, LanguageServerId, Worktree, + lsp::{Completion, CompletionKind, Symbol, SymbolKind}, + serde_json::{Value, json}, + settings::LspSettings, +}; + +use crate::{ + component::Component, + config::{get_java_home, get_jdtls_launcher, is_lombok_enabled}, + debugger::Debugger, + jdk::Jdk, + jdtls::{Jdtls, Lombok, build_jdtls_launch_args, get_jdtls_launcher_from_path}, + language_server::LanguageServer, + proxy::Proxy, + util::{path_to_file_uri, path_to_string}, +}; + +pub struct JdtlsServer { + pub jdtls: Jdtls, + pub lombok: Lombok, + pub proxy: Proxy, + pub jdk: Jdk, + pub debugger: Debugger, + pub cached_workspace: Option, +} + +impl JdtlsServer { + pub fn new() -> Self { + Self { + jdtls: Jdtls::new(), + lombok: Lombok::new(), + proxy: Proxy::new(), + jdk: Jdk::new(), + debugger: Debugger::new(), + cached_workspace: None, + } + } +} + +impl LanguageServer for JdtlsServer { + const SERVER_ID: &'static str = "jdtls"; + + fn command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result { + let current_dir = + env::current_dir().map_err(|err| format!("Failed to get current directory: {err}"))?; + + let configuration = self.workspace_configuration(language_server_id, worktree)?; + + let mut env = Vec::new(); + + if let Some(java_home) = get_java_home(&configuration, worktree) { + env.push(("JAVA_HOME".to_string(), java_home)); + } + + let proxy_path = self + .proxy + .binary_path(&configuration, language_server_id, worktree) + .map_err(|err| format!("Failed to get proxy binary path: {err}"))?; + + let mut args = vec![ + path_to_string(current_dir.clone()) + .map_err(|err| format!("Failed to convert current directory to string: {err}"))?, + ]; + + let lombok_jvm_arg = if is_lombok_enabled(&configuration) { + let lombok_jar_path = self + .lombok + .get_or_download(language_server_id, &configuration, worktree) + .map_err(|err| format!("Failed to get Lombok jar path: {err}"))?; + let canonical_lombok_jar_path = path_to_string(current_dir.join(lombok_jar_path)) + .map_err(|err| format!("Failed to convert Lombok jar path to string: {err}"))?; + + Some(format!("-javaagent:{canonical_lombok_jar_path}")) + } else { + None + }; + + 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) { + args.push(launcher); + if let Some(lombok_jvm_arg) = lombok_jvm_arg { + args.push(format!("--jvm-arg={lombok_jvm_arg}")); + } + } else { + let jdtls_path = self + .jdtls + .get_or_download(language_server_id, &configuration, worktree) + .map_err(|err| format!("Failed to get JDTLS binary path: {err}"))?; + args.extend( + build_jdtls_launch_args( + &jdtls_path, + &configuration, + worktree, + lombok_jvm_arg.into_iter().collect(), + language_server_id, + &mut self.jdk, + ) + .map_err(|err| format!("Failed to build JDTLS launch arguments: {err}"))?, + ); + } + + if let Err(err) = + self.debugger + .get_or_download(language_server_id, &configuration, worktree) + { + println!("Failed to download debugger: {err}"); + }; + + self.cached_workspace = Some(worktree.root_path()); + + Ok(zed::Command { + command: proxy_path, + args, + env, + }) + } + + fn initialization_options( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + let mut options = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.initialization_options) + .map_err(|err| format!("Failed to get LSP settings for worktree: {err}"))? + .unwrap_or_else(|| json!({})); + + let options_obj = options + .as_object_mut() + .ok_or_else(|| "initialization_options is not a JSON object".to_string())?; + if !options_obj.contains_key("workspaceFolders") { + let uri = path_to_file_uri(&worktree.root_path()); + options_obj.insert("workspaceFolders".to_string(), json!([uri])); + } + + let caps = options_obj + .entry("extendedClientCapabilities") + .or_insert_with(|| json!({})); + let caps_obj = caps + .as_object_mut() + .ok_or_else(|| "extendedClientCapabilities is not a JSON object".to_string())?; + caps_obj + .entry("classFileContentsSupport") + .or_insert(json!(true)); + caps_obj + .entry("resolveAdditionalTextEditsSupport") + .or_insert(json!(true)); + + if self.debugger.plugin_path().is_some() { + return Ok(Some( + self.debugger + .inject_plugin_into_options(Some(options)) + .map_err(|err| { + format!("Failed to inject debugger plugin into options: {err}") + })?, + )); + } + + Ok(Some(options)) + } + + fn workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + if let Ok(Some(settings)) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) + { + Ok(Some(settings)) + } else { + self.initialization_options(language_server_id, worktree) + .map(|init_options| { + init_options.and_then(|init_options| init_options.get("settings").cloned()) + }) + } + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + completion.kind.and_then(|kind| match kind { + CompletionKind::Field | CompletionKind::Constant => { + let modifiers = match kind { + CompletionKind::Field => "", + CompletionKind::Constant => "static final ", + _ => return None, + }; + let property_type = completion.detail.as_ref().and_then(|detail| { + detail + .split_once(" : ") + .map(|(_, property_type)| format!("{property_type} ")) + })?; + let semicolon = ";"; + let code = format!("{modifiers}{property_type}{}{semicolon}", completion.label); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range( + modifiers.len() + property_type.len()..code.len() - semicolon.len(), + ), + CodeLabelSpan::literal(" : ", None), + CodeLabelSpan::code_range( + modifiers.len()..modifiers.len() + property_type.len(), + ), + ], + code, + filter_range: (0..completion.label.len()).into(), + }) + } + CompletionKind::Method => { + let detail = completion.detail?; + let (left, return_type) = detail + .split_once(" : ") + .map(|(left, return_type)| (left, format!("{return_type} "))) + .unwrap_or((&detail, "void".to_string())); + let parameters = left + .find('(') + .map(|parameters_start| &left[parameters_start..]); + let name_and_parameters = + format!("{}{}", completion.label, parameters.unwrap_or("()")); + let braces = " {}"; + let code = format!("{return_type}{name_and_parameters}{braces}"); + let mut spans = vec![CodeLabelSpan::code_range( + return_type.len()..code.len() - braces.len(), + )]; + + if parameters.is_some() { + spans.push(CodeLabelSpan::literal(" : ", None)); + spans.push(CodeLabelSpan::code_range(0..return_type.len())); + } else { + spans.push(CodeLabelSpan::literal(" - ", None)); + spans.push(CodeLabelSpan::literal(detail, None)); + } + + Some(CodeLabel { + spans, + code, + filter_range: (0..completion.label.len()).into(), + }) + } + CompletionKind::Class | CompletionKind::Interface | CompletionKind::Enum => { + let keyword = match kind { + CompletionKind::Class => "class ", + CompletionKind::Interface => "interface ", + CompletionKind::Enum => "enum ", + _ => return None, + }; + let braces = " {}"; + let code = format!("{keyword}{}{braces}", completion.label); + let namespace = completion.detail.and_then(|detail| { + if detail.len() > completion.label.len() { + let prefix_len = detail.len() - completion.label.len() - 1; + Some(detail[..prefix_len].to_string()) + } else { + None + } + }); + let mut spans = vec![CodeLabelSpan::code_range( + keyword.len()..code.len() - braces.len(), + )]; + + if let Some(namespace) = namespace { + spans.push(CodeLabelSpan::literal(format!(" ({namespace})"), None)); + } + + Some(CodeLabel { + spans, + code, + filter_range: (0..completion.label.len()).into(), + }) + } + CompletionKind::Snippet => Some(CodeLabel { + code: String::new(), + spans: vec![CodeLabelSpan::literal( + format!("{} - {}", completion.label, completion.detail?), + None, + )], + filter_range: (0..completion.label.len()).into(), + }), + CompletionKind::Keyword | CompletionKind::Variable => Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..completion.label.len())], + filter_range: (0..completion.label.len()).into(), + code: completion.label, + }), + CompletionKind::Constructor => { + let detail = completion.detail?; + let parameters = &detail[detail.find('(')?..]; + let braces = " {}"; + let code = format!("{}{parameters}{braces}", completion.label); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..code.len() - braces.len())], + code, + filter_range: (0..completion.label.len()).into(), + }) + } + _ => None, + }) + } + + fn label_for_symbol( + &self, + _language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + let name = &symbol.name; + + match symbol.kind { + SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum => { + let keyword = match symbol.kind { + SymbolKind::Class => "class ", + SymbolKind::Interface => "interface ", + SymbolKind::Enum => "enum ", + _ => return None, + }; + let code = format!("{keyword}{name} {{}}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + SymbolKind::Method | SymbolKind::Function => { + let method_name = name.split('(').next().unwrap_or(name); + let after_name = &name[method_name.len()..]; + + let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") { + (p, Some(r)) + } else { + (after_name, None) + }; + + let ret = return_type.unwrap_or("void"); + let class_open = "class _ { "; + let code = format!("{class_open}{ret} {method_name}() {{}} }}"); + + let ret_start = class_open.len(); + let name_start = ret_start + ret.len() + 1; + + let mut spans = vec![ + CodeLabelSpan::code_range(ret_start..ret_start + ret.len()), + CodeLabelSpan::literal(" ".to_string(), None), + CodeLabelSpan::code_range(name_start..name_start + method_name.len()), + ]; + if !params.is_empty() { + spans.push(CodeLabelSpan::literal(params.to_string(), None)); + } + + let type_prefix_len = ret.len() + 1; + let filter_end = type_prefix_len + method_name.len() + params.len(); + Some(CodeLabel { + spans, + filter_range: (type_prefix_len..filter_end).into(), + code, + }) + } + _ => None, + } + } +} diff --git a/src/language_server.rs b/src/language_server.rs new file mode 100644 index 0000000..de68986 --- /dev/null +++ b/src/language_server.rs @@ -0,0 +1,53 @@ +use zed_extension_api::{ + self as zed, CodeLabel, LanguageServerId, Worktree, + lsp::{Completion, Symbol}, + serde_json::Value, + settings::LspSettings, +}; + +pub trait LanguageServer { + const SERVER_ID: &'static str; + + fn command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result; + + fn initialization_options( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result>; + + fn workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result> { + if let Ok(Some(settings)) = LspSettings::for_worktree(language_server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) + { + Ok(Some(settings)) + } else { + self.initialization_options(language_server_id, worktree) + .map(|opts| opts.and_then(|o| o.get("settings").cloned())) + } + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + _completion: Completion, + ) -> Option { + None + } + + fn label_for_symbol( + &self, + _language_server_id: &LanguageServerId, + _symbol: Symbol, + ) -> Option { + None + } +} diff --git a/src/lsp.rs b/src/lsp.rs index 5c1b804..d0e3d8b 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,7 +1,6 @@ use std::{ fs::{self}, path::Path, - sync::{Arc, RwLock}, }; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -12,109 +11,83 @@ use zed_extension_api::{ serde_json::{self, Map, Value}, }; -/// -/// `proxy.mjs` starts an HTTP server and writes its port to -/// `${workdir}/proxy/${hex(project_root)}`. -/// -/// This allows us to send LSP requests directly from the Java extension. -/// It’s a temporary workaround until `zed_extension_api` -/// provides the ability to send LSP requests directly. -/// -pub struct LspClient { - workspace: String, +/* +`java-lsp-proxy` starts an HTTP server and writes its port to +`${workdir}/proxy/${hex(project_root)}`. + +This allows us to send LSP requests directly from the Java extension. +It's a temporary workaround until `zed_extension_api` +provides the ability to send LSP requests directly. + */ + +pub fn resolve_class_path( + workspace: &str, + args: Vec>, +) -> zed::Result>> { + request::>>( + workspace, + "workspace/executeCommand", + json!({ + "command": "vscode.java.resolveClasspath", + "arguments": args + }), + ) } -#[derive(Clone)] -pub struct LspWrapper(Arc>); - -impl LspWrapper { - pub fn new(workspace: String) -> Self { - LspWrapper(Arc::new(RwLock::new(LspClient { workspace }))) - } - - pub fn get(&self) -> zed::Result> { - self.0 - .read() - .map_err(|err| format!("LspClient RwLock poisoned during read: {err}")) - } - - pub fn switch_workspace(&self, workspace: String) -> zed::Result<()> { - let mut lock = self - .0 - .write() - .map_err(|err| format!("LspClient RwLock poisoned during read: {err}"))?; - - lock.workspace = workspace; - - Ok(()) - } +pub fn resolve_main_class(workspace: &str, args: Vec) -> zed::Result> { + request::>( + workspace, + "workspace/executeCommand", + json!({ + "command": "vscode.java.resolveMainClass", + "arguments": args + }), + ) } -impl LspClient { - pub fn resolve_class_path(&self, args: Vec>) -> zed::Result>> { - self.request::>>( - "workspace/executeCommand", - json!({ - "command": "vscode.java.resolveClasspath", - "arguments": args - }), - ) - } +pub fn request(workspace: &str, method: &str, params: Value) -> Result +where + T: DeserializeOwned, +{ + let port = { + let filename = string_to_hex(workspace); - pub fn resolve_main_class(&self, args: Vec) -> zed::Result> { - self.request::>( - "workspace/executeCommand", - json!({ - "command": "vscode.java.resolveMainClass", - "arguments": args - }), - ) - } + let port_path = Path::new("proxy").join(filename); - pub fn request(&self, method: &str, params: Value) -> Result - where - T: DeserializeOwned, - { - // We cannot cache it because the user may restart the LSP - let port = { - let filename = string_to_hex(&self.workspace); - - let port_path = Path::new("proxy").join(filename); - - if !fs::metadata(&port_path).is_ok_and(|file| file.is_file()) { - return Err("Failed to find lsp port file".to_owned()); - } - - fs::read_to_string(port_path) - .map_err(|err| format!("Failed to read LSP proxy port from file: {err}"))? - .parse::() - .map_err(|err| format!("Failed to parse LSP proxy port (file corrupted): {err}"))? - }; - - let mut body = Map::new(); - body.insert("method".to_owned(), Value::String(method.to_owned())); - body.insert("params".to_owned(), params); - - let res = fetch( - &HttpRequest::builder() - .method(HttpMethod::Post) - .url(format!("http://localhost:{port}")) - .body(Value::Object(body).to_string()) - .build()?, - ) - .map_err(|err| format!("Failed to send request to LSP proxy: {err}"))?; - - let data: LspResponse = serde_json::from_slice(&res.body) - .map_err(|err| format!("Failed to parse response from LSP proxy: {err}"))?; - - match data { - LspResponse::Success { result } => Ok(result), - LspResponse::Error { error } => { - Err(format!("{} {} {}", error.code, error.message, error.data)) - } + if !fs::metadata(&port_path).is_ok_and(|file| file.is_file()) { + return Err("Failed to find lsp port file".to_owned()); + } + + fs::read_to_string(port_path) + .map_err(|err| format!("Failed to read LSP proxy port from file: {err}"))? + .parse::() + .map_err(|err| format!("Failed to parse LSP proxy port (file corrupted): {err}"))? + }; + + let mut body = Map::new(); + body.insert("method".to_owned(), Value::String(method.to_owned())); + body.insert("params".to_owned(), params); + + let res = fetch( + &HttpRequest::builder() + .method(HttpMethod::Post) + .url(format!("http://localhost:{port}")) + .body(Value::Object(body).to_string()) + .build()?, + ) + .map_err(|err| format!("Failed to send request to LSP proxy: {err}"))?; + + let data: LspResponse = serde_json::from_slice(&res.body) + .map_err(|err| format!("Failed to parse response from LSP proxy: {err}"))?; + + match data { + LspResponse::Success { result } => Ok(result), + LspResponse::Error { error } => { + Err(format!("{} {} {}", error.code, error.message, error.data)) } } } + fn string_to_hex(s: &str) -> String { let mut hex_string = String::new(); for byte in s.as_bytes() { diff --git a/src/proxy.rs b/src/proxy.rs index 72f3958..894a96d 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,151 +1,193 @@ use std::{fs::metadata, path::PathBuf}; -use serde_json::Value; use zed_extension_api::{ self as zed, DownloadedFileType, GithubReleaseOptions, LanguageServerId, - LanguageServerInstallationStatus, Worktree, serde_json, + LanguageServerInstallationStatus, Worktree, serde_json::Value, set_language_server_installation_status, }; -use crate::config::get_lsp_proxy_path; -use crate::util::{mark_checked_once, remove_all_files_except, should_use_local_or_download}; +use crate::{ + component::Component, + config::get_lsp_proxy_path, + util::{mark_checked_once, remove_all_files_except, should_use_local_or_download}, +}; const PROXY_BINARY: &str = "java-lsp-proxy"; const PROXY_INSTALL_PATH: &str = "proxy-bin"; const GITHUB_REPO: &str = "zed-extensions/java"; -fn asset_name() -> zed::Result<(String, DownloadedFileType)> { - let (os, arch) = zed::current_platform(); - let (os_str, file_type) = match os { - zed::Os::Mac => ("darwin", DownloadedFileType::GzipTar), - zed::Os::Linux => ("linux", DownloadedFileType::GzipTar), - zed::Os::Windows => ("windows", DownloadedFileType::Zip), - }; - let arch_str = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X8664 => "x86_64", - _ => return Err("Unsupported architecture".into()), - }; - let ext = if matches!(file_type, DownloadedFileType::Zip) { - "zip" - } else { - "tar.gz" - }; - Ok(( - format!("java-lsp-proxy-{os_str}-{arch_str}.{ext}"), - file_type, - )) +pub struct Proxy { + cached_path: Option, } -fn proxy_exec() -> String { - let (os, _arch) = zed::current_platform(); +impl Proxy { + pub fn new() -> Self { + Self { cached_path: None } + } - match os { - zed::Os::Linux | zed::Os::Mac => PROXY_BINARY.to_string(), - zed::Os::Windows => format!("{PROXY_BINARY}.exe"), + pub fn binary_path( + &mut self, + configuration: &Option, + language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> zed::Result { + let path = self.get_or_download(language_server_id, configuration, worktree)?; + Ok(path.to_string_lossy().to_string()) } } -fn find_latest_local() -> Option { - let local_binary = PathBuf::from(PROXY_INSTALL_PATH).join(proxy_exec()); - if metadata(&local_binary).is_ok_and(|m| m.is_file()) { - return Some(local_binary); - } +impl Component for Proxy { + const INSTALL_PATH: &'static str = PROXY_INSTALL_PATH; - // Check versioned downloads (e.g. proxy-bin/v6.8.12/java-lsp-proxy) - std::fs::read_dir(PROXY_INSTALL_PATH) - .ok()? - .filter_map(Result::ok) - .map(|e| e.path().join(proxy_exec())) - .filter(|p| metadata(p).is_ok_and(|m| m.is_file())) - .last() -} + fn find_local(&self) -> Option { + let local_binary = PathBuf::from(PROXY_INSTALL_PATH).join(proxy_exec()); + if metadata(&local_binary).is_ok_and(|m| m.is_file()) { + return Some(local_binary); + } -pub fn binary_path( - cached: &mut Option, - configuration: &Option, - language_server_id: &LanguageServerId, - worktree: &Worktree, -) -> zed::Result { - // 1. Prioritize explicitly configured custom proxy binary if it exists. - if let Some(path) = get_lsp_proxy_path(configuration, worktree) { - *cached = Some(path.clone()); - return Ok(path); + std::fs::read_dir(PROXY_INSTALL_PATH) + .ok()? + .filter_map(Result::ok) + .map(|e| e.path().join(proxy_exec())) + .filter(|p| metadata(p).is_ok_and(|m| m.is_file())) + .last() } - // 2. Respect check_updates setting (Never/Once/Always) - // Returns Some(path) when local install exists and policy says use it. - // Returns None when policy allows downloading. - // Returns Err when policy is Never/Once-exhausted with no local install. - match should_use_local_or_download(configuration, find_latest_local(), PROXY_INSTALL_PATH) { - Ok(Some(path)) => { - let s = path.to_string_lossy().to_string(); - *cached = Some(s.clone()); - return Ok(s); - } - Ok(None) => { /* policy allows download, continue */ } - Err(_) => { - // Never/Once with no managed install — fall through to PATH as last resort - } + fn loaded(&self) -> bool { + self.cached_path.is_some() } - // 3. Auto-download from GitHub releases - if let Ok((name, file_type)) = asset_name() - && let Ok(release) = zed::latest_github_release( + fn fetch_latest_version(&self) -> zed::Result { + Ok(zed::latest_github_release( GITHUB_REPO, GithubReleaseOptions { require_assets: true, pre_release: false, }, ) - { - let bin_path = format!("{PROXY_INSTALL_PATH}/{}/{}", release.version, proxy_exec()); + .map_err(|err| format!("Failed to fetch latest proxy release from {GITHUB_REPO}: {err}"))? + .version) + } + + fn download( + &mut self, + version: &str, + language_server_id: &LanguageServerId, + ) -> zed::Result { + let (name, file_type) = asset_name()?; + let bin_path = format!("{PROXY_INSTALL_PATH}/{version}/{}", proxy_exec()); if metadata(&bin_path).is_ok() { - *cached = Some(bin_path.clone()); - return Ok(bin_path); + self.cached_path = Some(bin_path.clone()); + return Ok(PathBuf::from(bin_path)); } - if let Some(asset) = release.assets.iter().find(|a| a.name == name) { - let version_dir = format!("{PROXY_INSTALL_PATH}/{}", release.version); - - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::Downloading, - ); - - if zed::download_file(&asset.download_url, &version_dir, file_type).is_ok() { - let _ = zed::make_file_executable(&bin_path); - set_language_server_installation_status( - language_server_id, - &LanguageServerInstallationStatus::None, - ); - let _ = remove_all_files_except(PROXY_INSTALL_PATH, &release.version); - let _ = mark_checked_once(PROXY_INSTALL_PATH, &release.version); - *cached = Some(bin_path.clone()); - return Ok(bin_path); - } - } + let release = zed::latest_github_release( + GITHUB_REPO, + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + ) + .map_err(|err| format!("Failed to fetch proxy release: {err}"))?; + + let asset = release + .assets + .iter() + .find(|a| a.name == name) + .ok_or_else(|| format!("No asset found matching {name:?}"))?; + + let version_dir = format!("{PROXY_INSTALL_PATH}/{version}"); + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file(&asset.download_url, &version_dir, file_type) + .map_err(|err| format!("Failed to download proxy: {err}"))?; + + let _ = zed::make_file_executable(&bin_path); + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::None, + ); + let _ = remove_all_files_except(PROXY_INSTALL_PATH, version); + let _ = mark_checked_once(PROXY_INSTALL_PATH, version); + + self.cached_path = Some(bin_path.clone()); + Ok(PathBuf::from(bin_path)) } - // 4. Fallback: local install (covers "always" mode when download fails) - if let Some(path) = find_latest_local() { - let s = path.to_string_lossy().to_string(); - *cached = Some(s.clone()); - return Ok(s); - } + fn get_or_download( + &mut self, + language_server_id: &LanguageServerId, + configuration: &Option, + worktree: &Worktree, + ) -> zed::Result { + if let Some(path) = self.user_configured_path(configuration, worktree) { + self.cached_path = Some(path.clone()); + return Ok(PathBuf::from(path)); + } + + if let Some(path) = + should_use_local_or_download(configuration, self.find_local(), Self::INSTALL_PATH) + .unwrap_or(None) + { + let s = path.to_string_lossy().to_string(); + self.cached_path = Some(s); + return Ok(path); + } + + if let Ok(version) = self.fetch_latest_version() + && let Ok(path) = self.download(&version, language_server_id) + { + return Ok(path); + } + + if let Some(path) = worktree.which(proxy_exec().as_str()) { + return Ok(PathBuf::from(path)); + } - // 5. Fallback: binary on $PATH - if let Some(path) = worktree.which(proxy_exec().as_str()) { - return Ok(path); + Err(format!("'{}' not found", proxy_exec())) } - // 6. Stale cache fallback - if let Some(path) = cached.as_deref() - && metadata(path).is_ok() - { - return Ok(path.to_string()); + fn user_configured_path( + &self, + configuration: &Option, + worktree: &Worktree, + ) -> Option { + get_lsp_proxy_path(configuration, worktree) } +} - Err(format!("'{}' not found", proxy_exec())) +fn asset_name() -> zed::Result<(String, DownloadedFileType)> { + let (os, arch) = zed::current_platform(); + let (os_str, file_type) = match os { + zed::Os::Mac => ("darwin", DownloadedFileType::GzipTar), + zed::Os::Linux => ("linux", DownloadedFileType::GzipTar), + zed::Os::Windows => ("windows", DownloadedFileType::Zip), + }; + let arch_str = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X8664 => "x86_64", + _ => return Err("Unsupported architecture".into()), + }; + let ext = if matches!(file_type, DownloadedFileType::Zip) { + "zip" + } else { + "tar.gz" + }; + Ok(( + format!("java-lsp-proxy-{os_str}-{arch_str}.{ext}"), + file_type, + )) +} + +fn proxy_exec() -> String { + let (os, _arch) = zed::current_platform(); + match os { + zed::Os::Linux | zed::Os::Mac => PROXY_BINARY.to_string(), + zed::Os::Windows => format!("{PROXY_BINARY}.exe"), + } } diff --git a/src/util.rs b/src/util.rs index 53841ba..72fb41c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,7 +14,7 @@ use zed_extension_api::{ use crate::{ config::{CheckUpdates, get_check_updates, get_java_home, is_java_autodownload}, - jdk::try_to_fetch_and_install_latest_jdk, + jdk::Jdk, }; // Errors @@ -182,10 +182,11 @@ 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, configuration)? - .join(java_executable_filename), - ); + let mut jdk = Jdk::new(); + return Ok(jdk + .get_bin_path(language_server_id, configuration, worktree) + .map_err(|err| format!("Failed to auto-download JDK: {err}"))? + .join(java_executable_filename)); } Err(JAVA_EXEC_NOT_FOUND_ERROR.to_string())