diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 6ebfdc90b72f81ba81fa4f3e8058ef63c24930de..91af40dc5a688077422d4e892aad59b5ad237c10 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -13,9 +13,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{ - executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle, -}; +use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle}; use language::LanguageRegistry; use parking_lot::Mutex; use project::{Project, WorktreeId}; @@ -188,7 +186,7 @@ impl TestServer { let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), - languages: Arc::new(LanguageRegistry::new(Task::ready(()))), + languages: Arc::new(LanguageRegistry::test()), themes: ThemeRegistry::new((), cx.font_cache()), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ed02b8de59ec2fa85e5e09571c5d9d423eaed513..69053e9fc4c58e0711efa9e1ecb3f9c9833d242a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -29,6 +29,7 @@ use std::{ any::Any, borrow::Cow, cell::RefCell, + ffi::OsString, fmt::Debug, hash::Hash, mem, @@ -77,12 +78,23 @@ pub trait ToLspPosition { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LanguageServerName(pub Arc); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub enum ServerExecutionKind { + Launch, + Node, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LanguageServerBinary { + pub path: PathBuf, + pub arguments: Vec, +} + /// Represents a Language Server, with certain cached sync properties. /// Uses [`LspAdapter`] under the hood, but calls all 'static' methods /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, - pub server_args: Vec, pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, @@ -93,7 +105,6 @@ pub struct CachedLspAdapter { impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; - let server_args = adapter.server_args().await; let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = @@ -102,7 +113,6 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, - server_args, initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, @@ -123,13 +133,16 @@ impl CachedLspAdapter { version: Box, http: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { self.adapter .fetch_server_binary(version, http, container_dir) .await } - pub async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + pub async fn cached_server_binary( + &self, + container_dir: PathBuf, + ) -> Option { self.adapter.cached_server_binary(container_dir).await } @@ -182,9 +195,9 @@ pub trait LspAdapter: 'static + Send + Sync { version: Box, http: Arc, container_dir: PathBuf, - ) -> Result; + ) -> Result; - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option; + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option; async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} @@ -207,10 +220,6 @@ pub trait LspAdapter: 'static + Send + Sync { None } - async fn server_args(&self) -> Vec { - Vec::new() - } - async fn initialization_options(&self) -> Option { None } @@ -488,7 +497,7 @@ pub struct LanguageRegistry { lsp_binary_paths: Mutex< HashMap< LanguageServerName, - Shared>>>, + Shared>>>, >, >, executor: Option>, @@ -794,14 +803,15 @@ impl LanguageRegistry { let adapter = language.adapter.clone()?; let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); + Some(cx.spawn(|cx| async move { login_shell_env_loaded.await; - let server_binary_path = this - .lsp_binary_paths - .lock() + + let mut lock = this.lsp_binary_paths.lock(); + let entry = lock .entry(adapter.name.clone()) .or_insert_with(|| { - get_server_binary_path( + get_binary( adapter.clone(), language.clone(), http_client, @@ -812,18 +822,18 @@ impl LanguageRegistry { .boxed() .shared() }) - .clone() - .map_err(|e| anyhow!(e)); + .clone(); + drop(lock); + let binary = entry.clone().map_err(|e| anyhow!(e)).await?; - let server_binary_path = server_binary_path.await?; - let server_args = &adapter.server_args; let server = lsp::LanguageServer::new( server_id, - &server_binary_path, - server_args, + &binary.path, + &binary.arguments, &root_path, cx, )?; + Ok(server) })) } @@ -853,13 +863,13 @@ impl Default for LanguageRegistry { } } -async fn get_server_binary_path( +async fn get_binary( adapter: Arc, language: Arc, http_client: Arc, download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, -) -> Result { +) -> Result { let container_dir = download_dir.join(adapter.name.0.as_ref()); if !container_dir.exists() { smol::fs::create_dir_all(&container_dir) @@ -867,7 +877,7 @@ async fn get_server_binary_path( .context("failed to create container directory")?; } - let path = fetch_latest_server_binary_path( + let binary = fetch_latest_binary( adapter.clone(), language.clone(), http_client, @@ -875,12 +885,13 @@ async fn get_server_binary_path( statuses.clone(), ) .await; - if let Err(error) = path.as_ref() { - if let Some(cached_path) = adapter.cached_server_binary(container_dir).await { + + if let Err(error) = binary.as_ref() { + if let Some(cached) = adapter.cached_server_binary(container_dir).await { statuses .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) .await?; - return Ok(cached_path); + return Ok(cached); } else { statuses .broadcast(( @@ -892,16 +903,16 @@ async fn get_server_binary_path( .await?; } } - path + binary } -async fn fetch_latest_server_binary_path( +async fn fetch_latest_binary( adapter: Arc, language: Arc, http_client: Arc, container_dir: &Path, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, -) -> Result { +) -> Result { let container_dir: Arc = container_dir.into(); lsp_binary_statuses_tx .broadcast(( @@ -915,13 +926,13 @@ async fn fetch_latest_server_binary_path( lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) .await?; - let path = adapter + let binary = adapter .fetch_server_binary(version_info, http_client, container_dir.to_path_buf()) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) .await?; - Ok(path) + Ok(binary) } impl Language { @@ -1454,11 +1465,11 @@ impl LspAdapter for Arc { _: Box, _: Arc, _: PathBuf, - ) -> Result { + ) -> Result { unreachable!(); } - async fn cached_server_binary(&self, _: PathBuf) -> Option { + async fn cached_server_binary(&self, _: PathBuf) -> Option { unreachable!(); } @@ -1516,7 +1527,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { - let mut languages = LanguageRegistry::new(Task::ready(())); + let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background()); let languages = Arc::new(languages); languages.register( diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ad94423e11ad3db1136c2ee1c07b3766690ebd8b..80bf0a70f65fdf8b541da65fdb0f41f1bf8c6dbf 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -108,7 +108,7 @@ impl LanguageServer { pub fn new>( server_id: usize, binary_path: &Path, - args: &[T], + arguments: &[T], root_path: &Path, cx: AsyncAppContext, ) -> Result { @@ -117,9 +117,10 @@ impl LanguageServer { } else { root_path.parent().unwrap_or_else(|| Path::new("/")) }; + let mut server = process::Command::new(binary_path) .current_dir(working_dir) - .args(args) + .args(arguments) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) @@ -128,7 +129,6 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stout = server.stdout.take().unwrap(); - let mut server = Self::new_internal( server_id, stdin, @@ -147,6 +147,7 @@ impl LanguageServer { ); }, ); + if let Some(name) = binary_path.file_name() { server.name = name.to_string_lossy().to_string(); } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 8698d6891e18e7e3e960a210ec7b3f23e308da72..63c3c6d884a4e91fdeaff3766adc97a99072ec24 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -4,6 +4,7 @@ lazy_static::lazy_static! { pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); + pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 1acee4bad4d09814a656bd21e6536278bd62889a..c49c77f076d73d344345926fb3129fe74f6cf8e7 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,17 +1,21 @@ use anyhow::Context; +use client::http::HttpClient; +use gpui::executor::Background; pub use language::*; +use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; use theme::ThemeRegistry; mod c; mod elixir; +mod github; mod go; mod html; -mod installation; mod json; mod language_plugin; mod lua; +mod node_runtime; mod python; mod ruby; mod rust; @@ -32,7 +36,14 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init(languages: Arc, themes: Arc) { +pub fn init( + http: Arc, + background: Arc, + languages: Arc, + themes: Arc, +) { + let node_runtime = NodeRuntime::new(http, background); + for (name, grammar, lsp_adapter) in [ ( "c", @@ -63,6 +74,7 @@ pub fn init(languages: Arc, themes: Arc) { "json", tree_sitter_json::language(), Some(Arc::new(json::JsonLspAdapter::new( + node_runtime.clone(), languages.clone(), themes.clone(), ))), @@ -75,7 +87,9 @@ pub fn init(languages: Arc, themes: Arc) { ( "python", tree_sitter_python::language(), - Some(Arc::new(python::PythonLspAdapter)), + Some(Arc::new(python::PythonLspAdapter::new( + node_runtime.clone(), + ))), ), ( "rust", @@ -90,22 +104,28 @@ pub fn init(languages: Arc, themes: Arc) { ( "tsx", tree_sitter_typescript::language_tsx(), - Some(Arc::new(typescript::TypeScriptLspAdapter)), + Some(Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone(), + ))), ), ( "typescript", tree_sitter_typescript::language_typescript(), - Some(Arc::new(typescript::TypeScriptLspAdapter)), + Some(Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone(), + ))), ), ( "javascript", tree_sitter_typescript::language_tsx(), - Some(Arc::new(typescript::TypeScriptLspAdapter)), + Some(Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone(), + ))), ), ( "html", tree_sitter_html::language(), - Some(Arc::new(html::HtmlLspAdapter)), + Some(Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))), ), ( "ruby", @@ -135,7 +155,7 @@ pub fn init(languages: Arc, themes: Arc) { ( "yaml", tree_sitter_yaml::language(), - Some(Arc::new(yaml::YamlLspAdapter)), + Some(Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))), ), ] { languages.register(name, load_config(name), grammar, lsp_adapter, load_queries); diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 9fbb12857f74c14745d08253d1707327381fbf36..906592fc2d4b92f947b3328caa5182e32614fb1e 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -1,4 +1,4 @@ -use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; @@ -39,7 +39,7 @@ impl super::LspAdapter for CLspAdapter { version: Box, http: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::().unwrap(); let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); let version_dir = container_dir.join(format!("clangd_{}", version.name)); @@ -81,10 +81,13 @@ impl super::LspAdapter for CLspAdapter { } } - Ok(binary_path) + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec![], + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { let mut last_clangd_dir = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -97,7 +100,10 @@ impl super::LspAdapter for CLspAdapter { let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; let clangd_bin = clangd_dir.join("bin/clangd"); if clangd_bin.exists() { - Ok(clangd_bin) + Ok(LanguageServerBinary { + path: clangd_bin, + arguments: vec![], + }) } else { Err(anyhow!( "missing clangd binary in directory {:?}", diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index 75b35bb630b80379c6654f30b9cbb36e885f1594..9f921a0c402c1d87f21e96aa3ecded5de55b1899 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -1,4 +1,4 @@ -use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; @@ -40,7 +40,7 @@ impl LspAdapter for ElixirLspAdapter { version: Box, http: Arc, container_dir: PathBuf, - ) -> Result { + ) -> 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)); @@ -94,17 +94,24 @@ impl LspAdapter for ElixirLspAdapter { } } - Ok(binary_path) + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec![], + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + 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")) + last.map(|path| LanguageServerBinary { + path, + arguments: vec![], + }) + .ok_or_else(|| anyhow!("no cached binary")) })() .await .log_err() diff --git a/crates/zed/src/languages/github.rs b/crates/zed/src/languages/github.rs new file mode 100644 index 0000000000000000000000000000000000000000..8fdef507908bbd1690a2f0e7b1ce735827e2a04b --- /dev/null +++ b/crates/zed/src/languages/github.rs @@ -0,0 +1,45 @@ +use anyhow::{Context, Result}; +use client::http::HttpClient; +use serde::Deserialize; +use smol::io::AsyncReadExt; +use std::sync::Arc; + +pub struct GitHubLspBinaryVersion { + pub name: String, + pub url: String, +} + +#[derive(Deserialize)] +pub(crate) struct GithubRelease { + pub name: String, + pub assets: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct GithubReleaseAsset { + pub name: String, + pub browser_download_url: String, +} + +pub(crate) async fn latest_github_release( + repo_name_with_owner: &str, + http: Arc, +) -> Result { + let mut response = http + .get( + &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"), + Default::default(), + true, + ) + .await + .context("error fetching latest release")?; + let mut body = Vec::new(); + response + .body_mut() + .read_to_end(&mut body) + .await + .context("error reading latest release")?; + let release: GithubRelease = + serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?; + Ok(release) +} diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index dd819338d0bf9a3a36566c97b72dd248a90a8e6d..9af309839fedb17a47b98b2d45ffe7fa79b0d940 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -1,4 +1,4 @@ -use super::installation::latest_github_release; +use super::github::latest_github_release; use anyhow::{anyhow, Result}; use async_trait::async_trait; use client::http::HttpClient; @@ -7,9 +7,13 @@ pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::{fs, process}; -use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc}; +use std::{any::Any, ffi::OsString, ops::Range, path::PathBuf, str, sync::Arc}; use util::ResultExt; +fn server_binary_arguments() -> Vec { + vec!["-mode=stdio".into()] +} + #[derive(Copy, Clone)] pub struct GoLspAdapter; @@ -23,10 +27,6 @@ impl super::LspAdapter for GoLspAdapter { LanguageServerName("gopls".into()) } - async fn server_args(&self) -> Vec { - vec!["-mode=stdio".into()] - } - async fn fetch_latest_server_version( &self, http: Arc, @@ -47,7 +47,7 @@ impl super::LspAdapter for GoLspAdapter { version: Box, _: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::>().unwrap(); let this = *self; @@ -68,7 +68,10 @@ impl super::LspAdapter for GoLspAdapter { } } - return Ok(binary_path.to_path_buf()); + return Ok(LanguageServerBinary { + path: binary_path.to_path_buf(), + arguments: server_binary_arguments(), + }); } } } else if let Some(path) = this.cached_server_binary(container_dir.clone()).await { @@ -102,10 +105,13 @@ impl super::LspAdapter for GoLspAdapter { let binary_path = container_dir.join(&format!("gopls_{version}")); fs::rename(&installed_binary_path, &binary_path).await?; - Ok(binary_path.to_path_buf()) + Ok(LanguageServerBinary { + path: binary_path.to_path_buf(), + arguments: server_binary_arguments(), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { let mut last_binary_path = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -122,7 +128,10 @@ impl super::LspAdapter for GoLspAdapter { } if let Some(path) = last_binary_path { - Ok(path) + Ok(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + }) } else { Err(anyhow!("no cached binary")) } diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index 5497841d886939a3e55a5dbe2a12ea6f07045f87..a2cfbac96b4ff47b7cf06e49551185f1fa1892cd 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,19 +1,34 @@ -use super::installation::{npm_install_packages, npm_package_latest_version}; +use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter}; +use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use serde_json::json; use smol::fs; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; use util::ResultExt; -pub struct HtmlLspAdapter; +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct HtmlLspAdapter { + node: Arc, +} impl HtmlLspAdapter { - const BIN_PATH: &'static str = + const SERVER_PATH: &'static str = "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server"; + + pub fn new(node: Arc) -> Self { + HtmlLspAdapter { node } + } } #[async_trait] @@ -22,15 +37,15 @@ impl LspAdapter for HtmlLspAdapter { LanguageServerName("vscode-html-language-server".into()) } - async fn server_args(&self) -> Vec { - vec!["--stdio".into()] - } - async fn fetch_latest_server_version( &self, _: Arc, ) -> Result> { - Ok(Box::new(npm_package_latest_version("vscode-langservers-extracted").await?) as Box<_>) + Ok(Box::new( + self.node + .npm_package_latest_version("vscode-langservers-extracted") + .await?, + ) as Box<_>) } async fn fetch_server_binary( @@ -38,20 +53,21 @@ impl LspAdapter for HtmlLspAdapter { version: Box, _: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::().unwrap(); let version_dir = container_dir.join(version.as_str()); fs::create_dir_all(&version_dir) .await .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); + let server_path = version_dir.join(Self::SERVER_PATH); - if fs::metadata(&binary_path).await.is_err() { - npm_install_packages( - [("vscode-langservers-extracted", version.as_str())], - &version_dir, - ) - .await?; + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + [("vscode-langservers-extracted", version.as_str())], + &version_dir, + ) + .await?; if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { while let Some(entry) = entries.next().await { @@ -65,10 +81,13 @@ impl LspAdapter for HtmlLspAdapter { } } - Ok(binary_path) + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -79,9 +98,12 @@ impl LspAdapter for HtmlLspAdapter { } } 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) + let server_path = last_version_dir.join(Self::SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } else { Err(anyhow!( "missing executable in directory {:?}", diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs deleted file mode 100644 index c5aff17e566b69b33e788ded686fd2eb8acef1d3..0000000000000000000000000000000000000000 --- a/crates/zed/src/languages/installation.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use client::http::HttpClient; - -use serde::Deserialize; -use smol::io::AsyncReadExt; -use std::{path::Path, sync::Arc}; - -pub struct GitHubLspBinaryVersion { - pub name: String, - pub url: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "kebab-case")] -struct NpmInfo { - #[serde(default)] - dist_tags: NpmInfoDistTags, - versions: Vec, -} - -#[derive(Deserialize, Default)] -struct NpmInfoDistTags { - latest: Option, -} - -#[derive(Deserialize)] -pub(crate) struct GithubRelease { - pub name: String, - pub assets: Vec, -} - -#[derive(Deserialize)] -pub(crate) struct GithubReleaseAsset { - pub name: String, - pub browser_download_url: String, -} - -pub async fn npm_package_latest_version(name: &str) -> Result { - let output = smol::process::Command::new("npm") - .args(["-fetch-retry-mintimeout", "2000"]) - .args(["-fetch-retry-maxtimeout", "5000"]) - .args(["info", name, "--json"]) - .output() - .await - .context("failed to run npm info")?; - if !output.status.success() { - Err(anyhow!( - "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ))?; - } - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - info.dist_tags - .latest - .or_else(|| info.versions.pop()) - .ok_or_else(|| anyhow!("no version found for npm package {}", name)) -} - -pub async fn npm_install_packages( - packages: impl IntoIterator, - directory: &Path, -) -> Result<()> { - let output = smol::process::Command::new("npm") - .args(["-fetch-retry-mintimeout", "2000"]) - .args(["-fetch-retry-maxtimeout", "5000"]) - .arg("install") - .arg("--prefix") - .arg(directory) - .args( - packages - .into_iter() - .map(|(name, version)| format!("{name}@{version}")), - ) - .output() - .await - .context("failed to run npm install")?; - if !output.status.success() { - Err(anyhow!( - "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ))?; - } - Ok(()) -} - -pub(crate) async fn latest_github_release( - repo_name_with_owner: &str, - http: Arc, -) -> Result { - let mut response = http - .get( - &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"), - Default::default(), - true, - ) - .await - .context("error fetching latest release")?; - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading latest release")?; - let release: GithubRelease = - serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?; - Ok(release) -} diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index d5c59d28d119b35ac6b23bbed9c384f303d04e08..479308f370e6268d25d2b7bfae04ed383432e4d2 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,18 +1,17 @@ -use super::installation::{latest_github_release, GitHubLspBinaryVersion}; -use anyhow::{anyhow, Result}; -use async_compression::futures::bufread::GzipDecoder; +use super::node_runtime::NodeRuntime; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; use collections::HashMap; -use futures::{future::BoxFuture, io::BufReader, FutureExt, StreamExt}; +use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; -use language::{LanguageRegistry, LanguageServerName, LspAdapter}; +use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; use serde_json::json; use settings::{keymap_file_json_schema, settings_file_json_schema}; -use smol::fs::{self, File}; +use smol::fs; use std::{ any::Any, - env::consts, + ffi::OsString, future, path::{Path, PathBuf}, sync::Arc, @@ -20,14 +19,30 @@ use std::{ use theme::ThemeRegistry; use util::{paths, ResultExt, StaffMode}; +const SERVER_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + pub struct JsonLspAdapter { + node: Arc, languages: Arc, themes: Arc, } impl JsonLspAdapter { - pub fn new(languages: Arc, themes: Arc) -> Self { - Self { languages, themes } + pub fn new( + node: Arc, + languages: Arc, + themes: Arc, + ) -> Self { + JsonLspAdapter { + node, + languages, + themes, + } } } @@ -37,78 +52,80 @@ impl LspAdapter for JsonLspAdapter { LanguageServerName("json-language-server".into()) } - async fn server_args(&self) -> Vec { - vec!["--stdio".into()] - } - async fn fetch_latest_server_version( &self, - http: Arc, + _: Arc, ) -> Result> { - let release = latest_github_release("zed-industries/json-language-server", http).await?; - let asset_name = format!("json-language-server-darwin-{}.gz", consts::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( + self.node + .npm_package_latest_version("vscode-json-languageserver") + .await?, + ) as Box<_>) } async fn fetch_server_binary( &self, version: Box, - http: Arc, + _: Arc, container_dir: PathBuf, - ) -> Result { - let version = version.downcast::().unwrap(); - let destination_path = container_dir.join(format!( - "json-language-server-{}-{}", - version.name, - consts::ARCH - )); - - if fs::metadata(&destination_path).await.is_err() { - let mut response = http - .get(&version.url, Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let mut file = File::create(&destination_path).await?; - futures::io::copy(decompressed_bytes, &mut file).await?; - fs::set_permissions( - &destination_path, - ::from_mode(0o755), - ) - .await?; + ) -> Result { + let version = version.downcast::().unwrap(); + let version_dir = container_dir.join(version.as_str()); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let server_path = version_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + [("vscode-json-languageserver", version.as_str())], + &version_dir, + ) + .await?; 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() != destination_path { - fs::remove_file(&entry_path).await.log_err(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); } } } } } - Ok(destination_path) + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { - let mut last = None; + let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { - last = Some(entry?.path()); + 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 server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) } - last.ok_or_else(|| anyhow!("no cached binary")) })() .await .log_err() diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index bd9a9d005fd7643ab880786f92de2055a69a9380..38f50d2d88710d66d43cdb7cafe932f92c1be57d 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -4,7 +4,7 @@ use client::http::HttpClient; use collections::HashMap; use futures::lock::Mutex; use gpui::executor::Background; -use language::{LanguageServerName, LspAdapter}; +use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn}; use std::{any::Any, path::PathBuf, sync::Arc}; use util::ResultExt; @@ -32,10 +32,9 @@ pub async fn new_json(executor: Arc) -> Result { pub struct PluginLspAdapter { name: WasiFn<(), String>, - server_args: WasiFn<(), Vec>, fetch_latest_server_version: WasiFn<(), Option>, - fetch_server_binary: WasiFn<(PathBuf, String), Result>, - cached_server_binary: WasiFn>, + fetch_server_binary: WasiFn<(PathBuf, String), Result>, + cached_server_binary: WasiFn>, initialization_options: WasiFn<(), String>, language_ids: WasiFn<(), Vec<(String, String)>>, executor: Arc, @@ -47,7 +46,6 @@ impl PluginLspAdapter { pub async fn new(mut plugin: Plugin, executor: Arc) -> Result { Ok(Self { name: plugin.function("name")?, - server_args: plugin.function("server_args")?, fetch_latest_server_version: plugin.function("fetch_latest_server_version")?, fetch_server_binary: plugin.function("fetch_server_binary")?, cached_server_binary: plugin.function("cached_server_binary")?, @@ -72,15 +70,6 @@ impl LspAdapter for PluginLspAdapter { LanguageServerName(name.into()) } - async fn server_args<'a>(&'a self) -> Vec { - self.runtime - .lock() - .await - .call(&self.server_args, ()) - .await - .unwrap() - } - async fn fetch_latest_server_version( &self, _: Arc, @@ -105,7 +94,7 @@ impl LspAdapter for PluginLspAdapter { version: Box, _: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = *version.downcast::().unwrap(); let runtime = self.runtime.clone(); let function = self.fetch_server_binary; @@ -113,7 +102,7 @@ impl LspAdapter for PluginLspAdapter { .spawn(async move { let mut runtime = runtime.lock().await; let handle = runtime.attach_path(&container_dir)?; - let result: Result = + let result: Result = runtime.call(&function, (container_dir, version)).await?; runtime.remove_resource(handle)?; result.map_err(|e| anyhow!("{}", e)) @@ -121,7 +110,7 @@ impl LspAdapter for PluginLspAdapter { .await } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { let runtime = self.runtime.clone(); let function = self.cached_server_binary; @@ -129,7 +118,8 @@ impl LspAdapter for PluginLspAdapter { .spawn(async move { let mut runtime = runtime.lock().await; let handle = runtime.attach_path(&container_dir).ok()?; - let result: Option = runtime.call(&function, container_dir).await.ok()?; + let result: Option = + runtime.call(&function, container_dir).await.ok()?; runtime.remove_resource(handle).ok()?; result }) diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 4bcffca9084257bd7595b8c19735418cc2b8d65b..7ffdac5218cb92a6a50622ae91e30eb0d8a07ead 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -1,4 +1,4 @@ -use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; +use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; use anyhow::{anyhow, bail, Result}; use async_compression::futures::bufread::GzipDecoder; @@ -6,28 +6,28 @@ use async_tar::Archive; use async_trait::async_trait; use client::http::HttpClient; use futures::{io::BufReader, StreamExt}; -use language::LanguageServerName; +use language::{LanguageServerBinary, LanguageServerName}; use smol::fs; use util::{async_iife, ResultExt}; -use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use super::github::{latest_github_release, GitHubLspBinaryVersion}; #[derive(Copy, Clone)] pub struct LuaLspAdapter; +fn server_binary_arguments() -> Vec { + vec![ + "--logpath=~/lua-language-server.log".into(), + "--loglevel=trace".into(), + ] +} + #[async_trait] impl super::LspAdapter for LuaLspAdapter { async fn name(&self) -> LanguageServerName { LanguageServerName("lua-language-server".into()) } - async fn server_args(&self) -> Vec { - vec![ - "--logpath=~/lua-language-server.log".into(), - "--loglevel=trace".into(), - ] - } - async fn fetch_latest_server_version( &self, http: Arc, @@ -57,7 +57,7 @@ impl super::LspAdapter for LuaLspAdapter { version: Box, http: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::().unwrap(); let binary_path = container_dir.join("bin/lua-language-server"); @@ -77,10 +77,13 @@ impl super::LspAdapter for LuaLspAdapter { ::from_mode(0o755), ) .await?; - Ok(binary_path) + Ok(LanguageServerBinary { + path: binary_path, + arguments: server_binary_arguments(), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { async_iife!({ let mut last_binary_path = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -97,7 +100,10 @@ impl super::LspAdapter for LuaLspAdapter { } if let Some(path) = last_binary_path { - Ok(path) + Ok(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + }) } else { Err(anyhow!("no cached binary")) } diff --git a/crates/zed/src/languages/node_runtime.rs b/crates/zed/src/languages/node_runtime.rs new file mode 100644 index 0000000000000000000000000000000000000000..41cbefbb732fc14fd70397213dde450582209716 --- /dev/null +++ b/crates/zed/src/languages/node_runtime.rs @@ -0,0 +1,166 @@ +use anyhow::{anyhow, bail, Context, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use client::http::HttpClient; +use futures::{future::Shared, FutureExt}; +use gpui::{executor::Background, Task}; +use parking_lot::Mutex; +use serde::Deserialize; +use smol::{fs, io::BufReader}; +use std::{ + env::consts, + path::{Path, PathBuf}, + sync::Arc, +}; + +const VERSION: &str = "v18.15.0"; + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct NpmInfo { + #[serde(default)] + dist_tags: NpmInfoDistTags, + versions: Vec, +} + +#[derive(Deserialize, Default)] +pub struct NpmInfoDistTags { + latest: Option, +} + +pub struct NodeRuntime { + http: Arc, + background: Arc, + installation_path: Mutex>>>>>, +} + +impl NodeRuntime { + pub fn new(http: Arc, background: Arc) -> Arc { + Arc::new(NodeRuntime { + http, + background, + installation_path: Mutex::new(None), + }) + } + + pub async fn binary_path(&self) -> Result { + let installation_path = self.install_if_needed().await?; + Ok(installation_path.join("bin/node")) + } + + pub async fn npm_package_latest_version(&self, name: &str) -> Result { + let installation_path = self.install_if_needed().await?; + let node_binary = installation_path.join("bin/node"); + let npm_file = installation_path.join("bin/npm"); + + let output = smol::process::Command::new(node_binary) + .arg(npm_file) + .args(["-fetch-retry-mintimeout", "2000"]) + .args(["-fetch-retry-maxtimeout", "5000"]) + .args(["-fetch-timeout", "5000"]) + .args(["info", name, "--json"]) + .output() + .await + .context("failed to run npm info")?; + + if !output.status.success() { + Err(anyhow!( + "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ))?; + } + + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .ok_or_else(|| anyhow!("no version found for npm package {}", name)) + } + + pub async fn npm_install_packages( + &self, + packages: impl IntoIterator, + directory: &Path, + ) -> Result<()> { + let installation_path = self.install_if_needed().await?; + let node_binary = installation_path.join("bin/node"); + let npm_file = installation_path.join("bin/npm"); + + let output = smol::process::Command::new(node_binary) + .arg(npm_file) + .args(["-fetch-retry-mintimeout", "2000"]) + .args(["-fetch-retry-maxtimeout", "5000"]) + .args(["-fetch-timeout", "5000"]) + .arg("install") + .arg("--prefix") + .arg(directory) + .args( + packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")), + ) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ))?; + } + Ok(()) + } + + async fn install_if_needed(&self) -> Result { + let task = self + .installation_path + .lock() + .get_or_insert_with(|| { + let http = self.http.clone(); + self.background + .spawn(async move { Self::install(http).await.map_err(Arc::new) }) + .shared() + }) + .clone(); + + match task.await { + Ok(path) => Ok(path), + Err(error) => Err(anyhow!("{}", error)), + } + } + + async fn install(http: Arc) -> Result { + let arch = match consts::ARCH { + "x86_64" => "x64", + "aarch64" => "arm64", + other => bail!("Running on unsupported platform: {other}"), + }; + + let folder_name = format!("node-{VERSION}-darwin-{arch}"); + let node_containing_dir = util::paths::SUPPORT_DIR.join("node"); + let node_dir = node_containing_dir.join(folder_name); + let node_binary = node_dir.join("bin/node"); + + if fs::metadata(&node_binary).await.is_err() { + _ = fs::remove_dir_all(&node_containing_dir).await; + fs::create_dir(&node_containing_dir) + .await + .context("error creating node containing dir")?; + + let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz"); + let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); + let mut response = http + .get(&url, Default::default(), true) + .await + .context("error downloading Node binary tarball")?; + + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&node_containing_dir).await?; + } + + anyhow::Ok(node_dir) + } +} diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 1391494ab1ea1e324caf9da769c34b5962121679..9a09c63bb6ca2447667ba41e484a0fb990412522 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -1,17 +1,32 @@ -use super::installation::{npm_install_packages, npm_package_latest_version}; +use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter}; +use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use smol::fs; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; use util::ResultExt; -pub struct PythonLspAdapter; +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct PythonLspAdapter { + node: Arc, +} impl PythonLspAdapter { - const BIN_PATH: &'static str = "node_modules/pyright/langserver.index.js"; + const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js"; + + pub fn new(node: Arc) -> Self { + PythonLspAdapter { node } + } } #[async_trait] @@ -20,15 +35,11 @@ impl LspAdapter for PythonLspAdapter { LanguageServerName("pyright".into()) } - async fn server_args(&self) -> Vec { - vec!["--stdio".into()] - } - async fn fetch_latest_server_version( &self, _: Arc, ) -> Result> { - Ok(Box::new(npm_package_latest_version("pyright").await?) as Box<_>) + Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>) } async fn fetch_server_binary( @@ -36,16 +47,18 @@ impl LspAdapter for PythonLspAdapter { version: Box, _: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::().unwrap(); let version_dir = container_dir.join(version.as_str()); fs::create_dir_all(&version_dir) .await .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); + let server_path = version_dir.join(Self::SERVER_PATH); - if fs::metadata(&binary_path).await.is_err() { - npm_install_packages([("pyright", version.as_str())], &version_dir).await?; + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages([("pyright", version.as_str())], &version_dir) + .await?; if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { while let Some(entry) = entries.next().await { @@ -59,10 +72,13 @@ impl LspAdapter for PythonLspAdapter { } } - Ok(binary_path) + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -73,9 +89,12 @@ impl LspAdapter for PythonLspAdapter { } } 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) + let server_path = last_version_dir.join(Self::SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } else { Err(anyhow!( "missing executable in directory {:?}", diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index cbfb5e35a70f06da84c5fa551845bb63385e3f94..662c1f464d1fd1f216b0af37925950658e083ba4 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use client::http::HttpClient; -use language::{LanguageServerName, LspAdapter}; +use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use std::{any::Any, path::PathBuf, sync::Arc}; pub struct RubyLanguageServer; @@ -12,10 +12,6 @@ impl LspAdapter for RubyLanguageServer { LanguageServerName("solargraph".into()) } - async fn server_args(&self) -> Vec { - vec!["stdio".into()] - } - async fn fetch_latest_server_version( &self, _: Arc, @@ -28,12 +24,15 @@ impl LspAdapter for RubyLanguageServer { _version: Box, _: Arc, _container_dir: PathBuf, - ) -> Result { + ) -> Result { Err(anyhow!("solargraph must be installed manually")) } - async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option { - Some("solargraph".into()) + async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option { + Some(LanguageServerBinary { + path: "solargraph".into(), + arguments: vec!["stdio".into()], + }) } async fn label_for_completion( diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index a8f7fcbc4dcf34ff3e017be53cfd967a0e185dfe..0f8e90d7b268a71712375f0fda0406f0745110d6 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,4 +1,4 @@ -use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; @@ -42,7 +42,7 @@ impl LspAdapter for RustLspAdapter { version: Box, http: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::().unwrap(); let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); @@ -72,17 +72,23 @@ impl LspAdapter for RustLspAdapter { } } - Ok(destination_path) + Ok(LanguageServerBinary { + path: destination_path, + arguments: Default::default(), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + 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")) + anyhow::Ok(LanguageServerBinary { + path: last.ok_or_else(|| anyhow!("no cached binary"))?, + arguments: Default::default(), + }) })() .await .log_err() diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 5290158deaf037b1a9ddb8a52211871c58a2a815..f9baf4f8f78f14c9ab6a7d7616b3affd3b992c37 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,19 +1,39 @@ -use super::installation::{npm_install_packages, npm_package_latest_version}; +use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; use futures::StreamExt; -use language::{LanguageServerName, LspAdapter}; +use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use serde_json::json; use smol::fs; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; use util::ResultExt; -pub struct TypeScriptLspAdapter; +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![ + server_path.into(), + "--stdio".into(), + "--tsserver-path".into(), + "node_modules/typescript/lib".into(), + ] +} + +pub struct TypeScriptLspAdapter { + node: Arc, +} impl TypeScriptLspAdapter { - const OLD_BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; - const NEW_BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs"; + const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; + const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs"; + + pub fn new(node: Arc) -> Self { + TypeScriptLspAdapter { node } + } } struct Versions { @@ -27,20 +47,16 @@ impl LspAdapter for TypeScriptLspAdapter { LanguageServerName("typescript-language-server".into()) } - async fn server_args(&self) -> Vec { - ["--stdio", "--tsserver-path", "node_modules/typescript/lib"] - .into_iter() - .map(str::to_string) - .collect() - } - async fn fetch_latest_server_version( &self, _: Arc, ) -> Result> { Ok(Box::new(Versions { - typescript_version: npm_package_latest_version("typescript").await?, - server_version: npm_package_latest_version("typescript-language-server").await?, + typescript_version: self.node.npm_package_latest_version("typescript").await?, + server_version: self + .node + .npm_package_latest_version("typescript-language-server") + .await?, }) as Box<_>) } @@ -49,7 +65,7 @@ impl LspAdapter for TypeScriptLspAdapter { versions: Box, _: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let versions = versions.downcast::().unwrap(); let version_dir = container_dir.join(&format!( "typescript-{}:server-{}", @@ -58,20 +74,21 @@ impl LspAdapter for TypeScriptLspAdapter { fs::create_dir_all(&version_dir) .await .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::NEW_BIN_PATH); - - if fs::metadata(&binary_path).await.is_err() { - npm_install_packages( - [ - ("typescript", versions.typescript_version.as_str()), - ( - "typescript-language-server", - versions.server_version.as_str(), - ), - ], - &version_dir, - ) - .await?; + let server_path = version_dir.join(Self::NEW_SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + [ + ("typescript", versions.typescript_version.as_str()), + ( + "typescript-language-server", + versions.server_version.as_str(), + ), + ], + &version_dir, + ) + .await?; if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { while let Some(entry) = entries.next().await { @@ -85,10 +102,13 @@ impl LspAdapter for TypeScriptLspAdapter { } } - Ok(binary_path) + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -99,12 +119,18 @@ impl LspAdapter for TypeScriptLspAdapter { } } let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let old_bin_path = last_version_dir.join(Self::OLD_BIN_PATH); - let new_bin_path = last_version_dir.join(Self::NEW_BIN_PATH); - if new_bin_path.exists() { - Ok(new_bin_path) - } else if old_bin_path.exists() { - Ok(old_bin_path) + let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH); + let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH); + if new_server_path.exists() { + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&new_server_path), + }) + } else if old_server_path.exists() { + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&old_server_path), + }) } else { Err(anyhow!( "missing executable in directory {:?}", diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 9750ecce88d6f5ea73824bb801d9ac73764d69d9..b6e82842dea80c777c1885e33fedfaf8fda439d5 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -1,21 +1,36 @@ +use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; -use language::{LanguageServerName, LspAdapter}; +use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use serde_json::Value; use settings::Settings; use smol::fs; -use std::{any::Any, future, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + ffi::OsString, + future, + path::{Path, PathBuf}, + sync::Arc, +}; use util::ResultExt; -use super::installation::{npm_install_packages, npm_package_latest_version}; +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} -pub struct YamlLspAdapter; +pub struct YamlLspAdapter { + node: Arc, +} impl YamlLspAdapter { - const BIN_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; + const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; + + pub fn new(node: Arc) -> Self { + YamlLspAdapter { node } + } } #[async_trait] @@ -24,15 +39,15 @@ impl LspAdapter for YamlLspAdapter { LanguageServerName("yaml-language-server".into()) } - async fn server_args(&self) -> Vec { - vec!["--stdio".into()] - } - async fn fetch_latest_server_version( &self, _: Arc, ) -> Result> { - Ok(Box::new(npm_package_latest_version("yaml-language-server").await?) as Box<_>) + Ok(Box::new( + self.node + .npm_package_latest_version("yaml-language-server") + .await?, + ) as Box<_>) } async fn fetch_server_binary( @@ -40,16 +55,17 @@ impl LspAdapter for YamlLspAdapter { version: Box, _: Arc, container_dir: PathBuf, - ) -> Result { + ) -> Result { let version = version.downcast::().unwrap(); let version_dir = container_dir.join(version.as_str()); fs::create_dir_all(&version_dir) .await .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); + let server_path = version_dir.join(Self::SERVER_PATH); - if fs::metadata(&binary_path).await.is_err() { - npm_install_packages([("yaml-language-server", version.as_str())], &version_dir) + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages([("yaml-language-server", version.as_str())], &version_dir) .await?; if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { @@ -64,10 +80,13 @@ impl LspAdapter for YamlLspAdapter { } } - Ok(binary_path) + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -78,9 +97,12 @@ impl LspAdapter for YamlLspAdapter { } } 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) + let server_path = last_version_dir.join(Self::SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) } else { Err(anyhow!( "missing executable in directory {:?}", diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b1813bab5000405a5db428304397d7e7cb115f30..fb6c6227c35af0bf350580e873f591629532c781 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -139,7 +139,12 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - languages::init(languages.clone(), themes.clone()); + languages::init( + http.clone(), + cx.background().clone(), + languages.clone(), + themes.clone(), + ); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); cx.set_global(client.clone()); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b0239d234c8a30476ee915bbec4f5fec04e3faa1..788be77e7587b46f191d344867a96ca9af7539f8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -652,9 +652,10 @@ fn open_bundled_file( mod tests { use super::*; use assets::Assets; + use client::test::FakeHttpClient; use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use gpui::{ - executor::Deterministic, AssetSource, MutableAppContext, Task, TestAppContext, ViewHandle, + executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, }; use language::LanguageRegistry; use project::{Project, ProjectPath}; @@ -1846,11 +1847,16 @@ mod tests { #[gpui::test] fn test_bundled_languages(cx: &mut MutableAppContext) { - let mut languages = LanguageRegistry::new(Task::ready(())); + let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let themes = ThemeRegistry::new((), cx.font_cache().clone()); - languages::init(languages.clone(), themes); + languages::init( + FakeHttpClient::with_404_response(), + cx.background().clone(), + languages.clone(), + themes, + ); for name in languages.language_names() { languages.language_for_name(&name); } diff --git a/plugins/json_language/src/lib.rs b/plugins/json_language/src/lib.rs index b3c70da7a1d4b097be6c7704bb7bc1dd18c05f38..e18d9ce74b288fe8209d74ddf8d9552808817900 100644 --- a/plugins/json_language/src/lib.rs +++ b/plugins/json_language/src/lib.rs @@ -6,7 +6,7 @@ use serde::Deserialize; #[import] fn command(string: &str) -> Option>; -const BIN_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; #[export] pub fn name() -> &'static str { @@ -38,7 +38,7 @@ pub fn fetch_server_binary(container_dir: PathBuf, version: String) -> Result Option { } let last_version_dir = last_version_dir?; - let bin_path = last_version_dir.join(BIN_PATH); - if bin_path.exists() { - Some(bin_path) + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Some(server_path) } else { println!("no binary found"); None