diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 953e3aaaca..857783062a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -5,6 +5,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; mod c; +mod elixir; mod go; mod installation; mod json; @@ -45,7 +46,11 @@ pub async fn init(languages: Arc, _executor: Arc) tree_sitter_cpp::language(), Some(CachedLspAdapter::new(c::CLspAdapter).await), ), - ("elixir", tree_sitter_elixir::language(), None), + ( + "elixir", + tree_sitter_elixir::language(), + Some(CachedLspAdapter::new(elixir::ElixirLspAdapter).await), + ), ( "go", tree_sitter_go::language(), diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs new file mode 100644 index 0000000000..32abbec4c6 --- /dev/null +++ b/crates/zed/src/languages/elixir.rs @@ -0,0 +1,111 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use client::http::HttpClient; +use futures::StreamExt; +pub use language::*; +use smol::fs::{self, File}; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::ResultExt; + +pub struct ElixirLspAdapter; + +#[async_trait] +impl LspAdapter for ElixirLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("elixir-ls".into()) + } + + async fn fetch_latest_server_version( + &self, + http: Arc, + ) -> Result> { + let release = latest_github_release("elixir-lsp/elixir-ls", http).await?; + let asset_name = "elixir-ls.zip"; + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + http: Arc, + container_dir: PathBuf, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); + let version_dir = container_dir.join(format!("elixir-ls_{}", version.name)); + let binary_path = version_dir.join("language_server.sh"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = http + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path) + .await + .with_context(|| format!("failed to create file {}", zip_path.display()))?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::create_dir_all(&version_dir) + .await + .with_context(|| format!("failed to create directory {}", version_dir.display()))?; + let unzip_status = smol::process::Command::new("unzip") + .arg(&zip_path) + .arg("-d") + .arg(&version_dir) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip clangd archive"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + if let Ok(metadata) = fs::metadata(&entry_path).await { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + } + } + + Ok(binary_path) + } + + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + last.ok_or_else(|| anyhow!("no cached binary")) + })() + .await + .log_err() + } +}