From 652ec6fb3e72487aa60183e71275f300941faa78 Mon Sep 17 00:00:00 2001 From: Pseudomata Date: Fri, 26 Jan 2024 19:02:31 -0500 Subject: [PATCH] Ability to manually configure LSP for Haskell Allow users who have HLS installed using `ghcup` to manually provide the path for the Haskell language server --- assets/settings/default.json | 4 + crates/zed/src/languages.rs | 23 +++- crates/zed/src/languages/haskell.rs | 174 +++++++++++----------------- 3 files changed, 89 insertions(+), 112 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 01165c9e31..dda8b8af67 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -451,6 +451,10 @@ "deno": { "enable": false }, + // Settings specific to the Haskell integration + "haskell": { + "lsp": "none" + }, // Different settings for specific languages. "languages": { "Plain Text": { diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index a78f22dfc3..56f9d17f19 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -7,7 +7,7 @@ use settings::Settings; use std::{borrow::Cow, str, sync::Arc}; use util::{asset_str, paths::PLUGINS_DIR}; -use self::{deno::DenoSettings, elixir::ElixirSettings}; +use self::{deno::DenoSettings, elixir::ElixirSettings, haskell::HaskellSettings}; mod c; mod css; @@ -55,6 +55,7 @@ pub fn init( ) { ElixirSettings::register(cx); DenoSettings::register(cx); + HaskellSettings::register(cx); let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) @@ -202,11 +203,21 @@ pub fn init( ); } } - language( - "haskell", - tree_sitter_haskell::language(), - vec![Arc::new(haskell::HaskellLspAdapter)], - ); + + match &HaskellSettings::get(None, cx).lsp { + haskell::HaskellLspSetting::None => { + language("haskell", tree_sitter_haskell::language(), vec![]) + } + haskell::HaskellLspSetting::Local { path, arguments } => language( + "haskell", + tree_sitter_haskell::language(), + vec![Arc::new(haskell::LocalLspAdapter { + path: path.clone(), + arguments: arguments.clone(), + })], + ), + } + language( "html", tree_sitter_html::language(), diff --git a/crates/zed/src/languages/haskell.rs b/crates/zed/src/languages/haskell.rs index 832907e265..14a8ba798e 100644 --- a/crates/zed/src/languages/haskell.rs +++ b/crates/zed/src/languages/haskell.rs @@ -1,140 +1,102 @@ -use std::env::consts::ARCH; -use std::ffi::OsString; -use std::{any::Any, path::PathBuf}; - -use anyhow::{anyhow, Context, Result}; -use async_compression::futures::bufread::XzDecoder; -use async_tar::Archive; +use anyhow::Result; 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}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Settings; +use std::ops::Deref; +use std::{any::Any, path::PathBuf}; -fn server_binary_arguments() -> Vec { - vec!["--lsp".into()] +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct HaskellSettings { + pub lsp: HaskellLspSetting, } -pub struct HaskellLspAdapter; +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HaskellLspSetting { + None, + Local { + path: String, + arguments: Vec, + }, +} + +#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] +pub struct HaskellSettingsContent { + lsp: Option, +} + +impl Settings for HaskellSettings { + const KEY: Option<&'static str> = Some("haskell"); + + type FileContent = HaskellSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} + +pub struct LocalLspAdapter { + pub path: String, + pub arguments: Vec, +} #[async_trait] -impl LspAdapter for HaskellLspAdapter { +impl LspAdapter for LocalLspAdapter { fn name(&self) -> LanguageServerName { - LanguageServerName("haskell-language-server".into()) + LanguageServerName("local-hls".into()) } fn short_name(&self) -> &'static str { - "hls" + "local-hls" } async fn fetch_latest_server_version( &self, - delegate: &dyn LspAdapterDelegate, + _: &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<_>) + Ok(Box::new(()) as Box<_>) } async fn fetch_server_binary( &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, + _: Box, + _: PathBuf, + _: &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?; + let path = shellexpand::full(&self.path)?; Ok(LanguageServerBinary { - path: binary_path, - arguments: server_binary_arguments(), + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } async fn cached_server_binary( &self, - container_dir: PathBuf, + _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir).await + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) } - 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 installation_test_binary(&self, _: PathBuf) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) } } - -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: server_binary_arguments(), - }) - } else { - Err(anyhow!("no cached binary")) - } - }) - .await - .log_err() -}