diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 31edba7131..f3068b7792 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -6,7 +6,7 @@ pub mod proto; #[cfg(test)] mod tests; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use client::http::{self, HttpClient}; use collections::HashSet; use futures::{ @@ -61,10 +61,11 @@ pub trait ToLspPosition { pub struct LspBinaryVersion { pub name: String, - pub url: http::Url, + pub url: Option, } -pub trait LspExt: 'static + Send + Sync { +pub trait LspAdapter: 'static + Send + Sync { + fn name(&self) -> &'static str; fn fetch_latest_server_version( &self, http: Arc, @@ -73,9 +74,9 @@ pub trait LspExt: 'static + Send + Sync { &self, version: LspBinaryVersion, http: Arc, - download_dir: Arc, + container_dir: PathBuf, ) -> BoxFuture<'static, Result>; - fn cached_server_binary(&self, download_dir: Arc) -> BoxFuture<'static, Option>; + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion(&self, _: &lsp::CompletionItem, _: &Language) -> Option { None @@ -83,6 +84,10 @@ pub trait LspExt: 'static + Send + Sync { fn label_for_symbol(&self, _: &str, _: lsp::SymbolKind, _: &Language) -> Option { None } + + fn server_args(&self) -> &[&str] { + &[] + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -140,7 +145,7 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) lsp_ext: Option>, + pub(crate) adapter: Option>, lsp_binary_path: Mutex>>>>>, } @@ -260,7 +265,7 @@ impl LanguageRegistry { .ok_or_else(|| anyhow!("language server download directory has not been assigned")) .log_err()?; - let lsp_ext = language.lsp_ext.clone()?; + let adapter = language.adapter.clone()?; let background = cx.background().clone(); let server_binary_path = { Some( @@ -269,7 +274,7 @@ impl LanguageRegistry { .lock() .get_or_insert_with(|| { get_server_binary_path( - lsp_ext, + adapter.clone(), language.clone(), http_client, download_dir, @@ -285,7 +290,9 @@ impl LanguageRegistry { }?; Some(cx.background().spawn(async move { let server_binary_path = server_binary_path.await?; - let server = lsp::LanguageServer::new(&server_binary_path, &root_path, background)?; + let server_args = adapter.server_args(); + let server = + lsp::LanguageServer::new(&server_binary_path, server_args, &root_path, background)?; Ok(server) })) } @@ -298,22 +305,29 @@ impl LanguageRegistry { } async fn get_server_binary_path( - lsp_ext: Arc, + adapter: Arc, language: Arc, http_client: Arc, download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { + let container_dir = download_dir.join(adapter.name()); + if !container_dir.exists() { + smol::fs::create_dir_all(&container_dir) + .await + .context("failed to create container directory")?; + } + let path = fetch_latest_server_binary_path( - lsp_ext.clone(), + adapter.clone(), language.clone(), http_client, - download_dir.clone(), + &container_dir, statuses.clone(), ) .await; if path.is_err() { - if let Some(cached_path) = lsp_ext.cached_server_binary(download_dir).await { + if let Some(cached_path) = adapter.cached_server_binary(container_dir).await { statuses .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) .await?; @@ -328,10 +342,10 @@ async fn get_server_binary_path( } async fn fetch_latest_server_binary_path( - lsp_ext: Arc, + adapter: Arc, language: Arc, http_client: Arc, - download_dir: Arc, + container_dir: &Path, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { lsp_binary_statuses_tx @@ -340,14 +354,14 @@ async fn fetch_latest_server_binary_path( LanguageServerBinaryStatus::CheckingForUpdate, )) .await?; - let version_info = lsp_ext + let version_info = adapter .fetch_latest_server_version(http_client.clone()) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) .await?; - let path = lsp_ext - .fetch_server_binary(version_info, http_client, download_dir) + let path = adapter + .fetch_server_binary(version_info, http_client, container_dir.to_path_buf()) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) @@ -369,7 +383,7 @@ impl Language { highlight_map: Default::default(), }) }), - lsp_ext: None, + adapter: None, lsp_binary_path: Default::default(), } } @@ -414,8 +428,8 @@ impl Language { Ok(self) } - pub fn with_lsp_ext(mut self, lsp_ext: impl LspExt) -> Self { - self.lsp_ext = Some(Arc::new(lsp_ext)); + pub fn with_lsp_adapter(mut self, lsp_adapter: impl LspAdapter) -> Self { + self.adapter = Some(Arc::new(lsp_adapter)); self } @@ -442,19 +456,19 @@ impl Language { } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { - if let Some(processor) = self.lsp_ext.as_ref() { + if let Some(processor) = self.adapter.as_ref() { processor.process_diagnostics(diagnostics); } } pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option { - self.lsp_ext + self.adapter .as_ref()? .label_for_completion(completion, self) } pub fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option { - self.lsp_ext.as_ref()?.label_for_symbol(name, kind, self) + self.adapter.as_ref()?.label_for_symbol(name, kind, self) } pub fn highlight_text<'a>( diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index dc57c85213..890b6d2fb0 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -102,10 +102,13 @@ struct Error { impl LanguageServer { pub fn new( binary_path: &Path, + args: &[&str], root_path: &Path, background: Arc, ) -> Result> { let mut server = Command::new(binary_path) + .current_dir(root_path) + .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) diff --git a/crates/zed/languages/json/config.toml b/crates/zed/languages/json/config.toml index 782e818676..70201e6877 100644 --- a/crates/zed/languages/json/config.toml +++ b/crates/zed/languages/json/config.toml @@ -5,3 +5,6 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false }, ] + +[language_server] +disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 4285f5f311..0e5adb78fc 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -8,21 +8,16 @@ use regex::Regex; use rust_embed::RustEmbed; use serde::Deserialize; use smol::fs::{self, File}; -use std::{ - borrow::Cow, - env::consts, - path::{Path, PathBuf}, - str, - sync::Arc, -}; +use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; #[derive(RustEmbed)] #[folder = "languages"] struct LanguageDir; -struct RustLsp; -struct CLsp; +struct RustLspAdapter; +struct CLspAdapter; +struct JsonLspAdapter; #[derive(Deserialize)] struct GithubRelease { @@ -36,7 +31,11 @@ struct GithubReleaseAsset { browser_download_url: http::Url, } -impl LspExt for RustLsp { +impl LspAdapter for RustLspAdapter { + fn name(&self) -> &'static str { + "rust-analyzer" + } + fn fetch_latest_server_version( &self, http: Arc, @@ -67,7 +66,7 @@ impl LspExt for RustLsp { .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; Ok(LspBinaryVersion { name: release.name, - url: asset.browser_download_url.clone(), + url: Some(asset.browser_download_url.clone()), }) } .boxed() @@ -77,18 +76,15 @@ impl LspExt for RustLsp { &self, version: LspBinaryVersion, http: Arc, - download_dir: Arc, + container_dir: PathBuf, ) -> BoxFuture<'static, Result> { async move { - let destination_dir_path = download_dir.join("rust-analyzer"); - fs::create_dir_all(&destination_dir_path).await?; - let destination_path = - destination_dir_path.join(format!("rust-analyzer-{}", version.name)); + let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); if fs::metadata(&destination_path).await.is_err() { let response = http .send( - surf::RequestBuilder::new(Method::Get, version.url) + surf::RequestBuilder::new(Method::Get, version.url.unwrap()) .middleware(surf::middleware::Redirect::default()) .build(), ) @@ -103,7 +99,7 @@ impl LspExt for RustLsp { ) .await?; - if let Some(mut entries) = fs::read_dir(&destination_dir_path).await.log_err() { + 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(); @@ -120,13 +116,10 @@ impl LspExt for RustLsp { .boxed() } - fn cached_server_binary(&self, download_dir: Arc) -> BoxFuture<'static, Option> { + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { async move { - let destination_dir_path = download_dir.join("rust-analyzer"); - fs::create_dir_all(&destination_dir_path).await?; - let mut last = None; - let mut entries = fs::read_dir(&destination_dir_path).await?; + let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { last = Some(entry?.path()); } @@ -292,7 +285,11 @@ impl LspExt for RustLsp { } } -impl LspExt for CLsp { +impl LspAdapter for CLspAdapter { + fn name(&self) -> &'static str { + "clangd" + } + fn fetch_latest_server_version( &self, http: Arc, @@ -323,7 +320,7 @@ impl LspExt for CLsp { .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; Ok(LspBinaryVersion { name: release.name, - url: asset.browser_download_url.clone(), + url: Some(asset.browser_download_url.clone()), }) } .boxed() @@ -333,14 +330,9 @@ impl LspExt for CLsp { &self, version: LspBinaryVersion, http: Arc, - download_dir: Arc, + container_dir: PathBuf, ) -> BoxFuture<'static, Result> { async move { - let container_dir = download_dir.join("clangd"); - fs::create_dir_all(&container_dir) - .await - .context("failed to create container directory")?; - let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); let version_dir = container_dir.join(format!("clangd_{}", version.name)); let binary_path = version_dir.join("bin/clangd"); @@ -348,7 +340,7 @@ impl LspExt for CLsp { if fs::metadata(&binary_path).await.is_err() { let response = http .send( - surf::RequestBuilder::new(Method::Get, version.url) + surf::RequestBuilder::new(Method::Get, version.url.unwrap()) .middleware(surf::middleware::Redirect::default()) .build(), ) @@ -390,13 +382,10 @@ impl LspExt for CLsp { .boxed() } - fn cached_server_binary(&self, download_dir: Arc) -> BoxFuture<'static, Option> { + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { async move { - let destination_dir_path = download_dir.join("clangd"); - fs::create_dir_all(&destination_dir_path).await?; - let mut last_clangd_dir = None; - let mut entries = fs::read_dir(&destination_dir_path).await?; + 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_dir() { @@ -421,6 +410,120 @@ impl LspExt for CLsp { fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} } +impl JsonLspAdapter { + const BIN_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +} + +impl LspAdapter for JsonLspAdapter { + fn name(&self) -> &'static str { + "vscode-json-languageserver" + } + + fn server_args(&self) -> &[&str] { + &["--stdio"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result> { + async move { + #[derive(Deserialize)] + struct NpmInfo { + versions: Vec, + } + + let output = smol::process::Command::new("npm") + .args(["info", "vscode-json-languageserver", "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!("failed to execute npm info"))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + + Ok(LspBinaryVersion { + name: info + .versions + .pop() + .ok_or_else(|| anyhow!("no versions found in npm info"))?, + url: Default::default(), + }) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: LspBinaryVersion, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + async move { + let version_dir = container_dir.join(&version.name); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + let output = smol::process::Command::new("npm") + .current_dir(&version_dir) + .arg("install") + .arg(format!("vscode-json-languageserver@{}", version.name)) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!("failed to install vscode-json-languageserver"))?; + } + + 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 { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + async move { + let mut last_version_dir = 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_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} +} + pub fn build_language_registry() -> LanguageRegistry { let mut languages = LanguageRegistry::new(); languages.set_language_server_download_dir( @@ -447,7 +550,7 @@ fn rust() -> Language { .unwrap() .with_outline_query(load_query("rust/outline.scm").as_ref()) .unwrap() - .with_lsp_ext(RustLsp) + .with_lsp_adapter(RustLspAdapter) } fn c() -> Language { @@ -462,7 +565,7 @@ fn c() -> Language { .unwrap() .with_outline_query(load_query("c/outline.scm").as_ref()) .unwrap() - .with_lsp_ext(CLsp) + .with_lsp_adapter(CLspAdapter) } fn json() -> Language { @@ -477,6 +580,7 @@ fn json() -> Language { .unwrap() .with_outline_query(load_query("json/outline.scm").as_ref()) .unwrap() + .with_lsp_adapter(JsonLspAdapter) } fn markdown() -> Language { @@ -498,7 +602,7 @@ fn load_query(path: &str) -> Cow<'static, str> { mod tests { use super::*; use gpui::color::Color; - use language::LspExt; + use language::LspAdapter; use theme::SyntaxTheme; #[test] @@ -525,7 +629,7 @@ mod tests { }, ], }; - RustLsp.process_diagnostics(&mut params); + RustLspAdapter.process_diagnostics(&mut params); assert_eq!(params.diagnostics[0].message, "use of moved value `a`");