diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9d82a7f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,57 @@ +use zed_extension_api::{ + Worktree, + serde_json::Value, + settings::{CommandSettings, LspSettings}, +}; + +pub(super) fn get_initialization_options( + language_server_id: &str, + worktree: &Worktree, +) -> Option { + LspSettings::for_worktree(language_server_id, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.initialization_options) +} + +pub(super) fn get_workspace_configuration( + language_server_id: &str, + worktree: &Worktree, +) -> Option { + LspSettings::for_worktree(language_server_id, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.settings) +} + +pub(super) fn get_binary_settings( + language_server_id: &str, + worktree: &Worktree, +) -> Option { + LspSettings::for_worktree(language_server_id, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary) +} + +pub(super) fn get_binary_path(binary_settings: &Option) -> Option { + binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.path.clone()) +} + +pub(super) fn get_binary_args(binary_settings: &Option) -> Option> { + binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) +} + +pub(super) fn get_binary_env( + binary_settings: &Option, +) -> Option> { + binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.env.clone()) + .map(|env| { + env.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) +} diff --git a/src/lib.rs b/src/lib.rs index 1f7decb..5dc1e2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,46 +1,108 @@ +mod config; +mod util; + use std::fs; use zed_extension_api::{ Architecture, Command, DownloadedFileType, Extension, GithubReleaseOptions, LanguageServerId, - Os, Result, Worktree, current_platform, download_file, latest_github_release, - make_file_executable, register_extension, + LanguageServerInstallationStatus, Os, Result, Worktree, current_platform, download_file, + latest_github_release, make_file_executable, register_extension, serde_json::Value, + set_language_server_installation_status, }; +struct JustLspBinary { + path: String, + args: Vec, + env: Vec<(String, String)>, +} + struct JustExtension { cached_binary_path: Option, } impl JustExtension { + const LANGUAGE_SERVER_ID: &'static str = "just-lsp"; + fn language_server_binary_path( &mut self, - _language_server_id: &LanguageServerId, + language_server_id: &LanguageServerId, worktree: &Worktree, - ) -> Result { + ) -> Result { + let (os, arch) = current_platform(); + let extension = match os { + Os::Windows => ".exe", + _ => "", + }; + + let binary_name = format!("{}{extension}", Self::LANGUAGE_SERVER_ID); + let binary_settings = config::get_binary_settings(Self::LANGUAGE_SERVER_ID, worktree); + let binary_args = config::get_binary_args(&binary_settings).unwrap_or_default(); + let binary_env = config::get_binary_env(&binary_settings).unwrap_or_default(); + // Check if already cached - if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) + if let Some(binary_path) = &self.cached_binary_path + && fs::metadata(binary_path).is_ok_and(|stat| stat.is_file()) { - return Ok(path.clone()); + return Ok(JustLspBinary { + path: binary_path.clone(), + args: binary_args, + env: binary_env, + }); + } + + // Check if just-lsp path was specified + if let Some(binary_path) = config::get_binary_path(&binary_settings) { + self.cached_binary_path = Some(binary_path.clone()); + return Ok(JustLspBinary { + path: binary_path, + args: binary_args, + env: binary_env, + }); } // Check if just-lsp is on PATH - if let Some(path) = worktree.which("just-lsp") { - self.cached_binary_path = Some(path.clone()); - return Ok(path); + if let Some(binary_path) = worktree.which(Self::LANGUAGE_SERVER_ID) { + self.cached_binary_path = Some(binary_path.clone()); + return Ok(JustLspBinary { + path: binary_path, + args: binary_args, + env: binary_env, + }); } // Download and install just-lsp - let release = latest_github_release( + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + // Check if already downloaded when fetching the latest GitHub release fails + let release = match latest_github_release( "terror/just-lsp", GithubReleaseOptions { require_assets: true, pre_release: false, }, - )?; + ) { + Ok(release) => release, + Err(_) => { + if let Some(binary_path) = + util::find_existing_binary(Self::LANGUAGE_SERVER_ID, &binary_name) + { + self.cached_binary_path = Some(binary_path.clone()); + return Ok(JustLspBinary { + path: binary_path, + args: binary_args, + env: binary_env, + }); + } + return Err("Failed to download just-lsp".to_string()); + } + }; - let (os, arch) = current_platform(); let asset_name = format!( - "just-lsp-{version}-{target}.{extension}", + "{}-{version}-{target}.{extension}", + Self::LANGUAGE_SERVER_ID, version = release.version, target = match (os, arch) { (Os::Mac, Architecture::Aarch64) => "aarch64-apple-darwin", @@ -63,33 +125,35 @@ impl JustExtension { .find(|asset| asset.name == asset_name) .ok_or_else(|| format!("Asset {} not found in release", asset_name))?; - let version_dir = format!("just-lsp-{}", release.version); - let binary_path = format!( - "{}/just-lsp{}", - version_dir, - match os { - Os::Windows => ".exe", - _ => "", - } - ); + let version_dir = format!("{}-{}", Self::LANGUAGE_SERVER_ID, release.version); + let binary_path = format!("{}/{}", version_dir, binary_name); - // Check if already downloaded + // Check if already downloaded latest version if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - download_file( - &asset.download_url, - &version_dir, - match os { - Os::Windows => DownloadedFileType::Zip, - _ => DownloadedFileType::GzipTar, - }, - ) - .map_err(|e| format!("Failed to download just-lsp: {}", e))?; + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + let file_type = match os { + Os::Windows => DownloadedFileType::Zip, + _ => DownloadedFileType::GzipTar, + }; + + download_file(&asset.download_url, &version_dir, file_type) + .map_err(|e| format!("Failed to download just-lsp: {}", e))?; make_file_executable(&binary_path)?; + + util::remove_outdated_versions(Self::LANGUAGE_SERVER_ID, &version_dir)?; } self.cached_binary_path = Some(binary_path.clone()); - Ok(binary_path) + Ok(JustLspBinary { + path: binary_path, + args: binary_args, + env: binary_env, + }) } } @@ -105,14 +169,36 @@ impl Extension for JustExtension { language_server_id: &LanguageServerId, worktree: &Worktree, ) -> Result { - let path = self.language_server_binary_path(language_server_id, worktree)?; + let just_lsp = self.language_server_binary_path(language_server_id, worktree)?; Ok(Command { - command: path, - args: vec![], - env: Default::default(), + command: just_lsp.path, + args: just_lsp.args, + env: just_lsp.env, }) } + + fn language_server_initialization_options( + &mut self, + _language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> Result> { + let settings = config::get_initialization_options(Self::LANGUAGE_SERVER_ID, worktree) + .unwrap_or_default(); + + Ok(Some(settings)) + } + + fn language_server_workspace_configuration( + &mut self, + _language_server_id: &LanguageServerId, + worktree: &Worktree, + ) -> Result> { + let settings = config::get_workspace_configuration(Self::LANGUAGE_SERVER_ID, worktree) + .unwrap_or_default(); + + Ok(Some(settings)) + } } register_extension!(JustExtension); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..00ebb86 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,31 @@ +use std::fs; + +use zed_extension_api::Result; + +pub(super) fn remove_outdated_versions(language_server_id: &str, version_dir: &str) -> Result<()> { + let entries = fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str().is_none_or(|file_name| { + file_name.starts_with(language_server_id) && file_name != version_dir + }) { + fs::remove_dir_all(entry.path()).ok(); + } + } + Ok(()) +} + +pub(super) fn find_existing_binary(language_server_id: &str, binary_name: &str) -> Option { + fs::read_dir(".").ok()?.flatten().find_map(|entry| { + let binary_path = entry.path().join(binary_name); + + if binary_path.is_file() + && let Some(binary_dir) = entry.file_name().to_str() + && binary_dir.starts_with(language_server_id) + { + Some(binary_path.to_string_lossy().to_string()) + } else { + None + } + }) +}