diff --git a/Cargo.lock b/Cargo.lock index fa06b9bd74..36ca6d7092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,7 @@ dependencies = [ "futures-io", "memchr", "pin-project-lite 0.2.13", + "xz2", ] [[package]] @@ -4081,6 +4082,17 @@ dependencies = [ "url", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mach2" version = "0.4.1" @@ -9609,6 +9621,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index e01f24f727..faac957d56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ resolver = "2" [workspace.dependencies] anyhow = { version = "1.0.57" } async-trait = { version = "0.1" } -async-compression = { version = "0.4", features = ["gzip", "futures-io"] } +async-compression = { version = "0.4", features = ["gzip", "xz", "futures-io"] } chrono = { version = "0.4", features = ["serde"] } ctor = "0.2.6" derive_more = { version = "0.99.17" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index ef0cc1b146..a78f22dfc3 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -202,7 +202,11 @@ pub fn init( ); } } - language("haskell", tree_sitter_haskell::language(), vec![]); + language( + "haskell", + tree_sitter_haskell::language(), + vec![Arc::new(haskell::HaskellLspAdapter)], + ); language( "html", tree_sitter_html::language(), diff --git a/crates/zed/src/languages/haskell.rs b/crates/zed/src/languages/haskell.rs index 8b13789179..56699f1618 100644 --- a/crates/zed/src/languages/haskell.rs +++ b/crates/zed/src/languages/haskell.rs @@ -1 +1,135 @@ +use std::env::consts::ARCH; +use std::{any::Any, path::PathBuf}; +use anyhow::{anyhow, Context, Result}; +use async_compression::futures::bufread::XzDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{io::BufReader, StreamExt}; +use smol::fs; + +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use util::async_maybe; +use util::github::latest_github_release; +use util::{github::GitHubLspBinaryVersion, ResultExt}; + +pub struct HaskellLspAdapter; + +#[async_trait] +impl LspAdapter for HaskellLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("haskell-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "hls" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + // TODO: Release version should be matched against GHC version + let release = latest_github_release( + "haskell/haskell-language-server", + false, + delegate.http_client(), + ) + .await?; + let asset_name = format!( + "haskell-language-server-{}-{}-apple-darwin.tar.xz", + release.name, ARCH + ); + 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, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let destination_path = + container_dir.join(format!("haskell-language-server-{}", version.name)); + if fs::metadata(&destination_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let decompressed_bytes = XzDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&container_dir).await?; + } + let binary_path = destination_path.join("bin/haskell-language-server-wrapper"); + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec!["--lsp".into()], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_maybe!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "haskell-language-server-wrapper") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: Vec::new(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +}