Merge pull request #2324 from zed-industries/download-node

Julia created

Automatically download Node for Node based language servers

Change summary

crates/collab/src/tests.rs                  |   6 
crates/language/src/language.rs             |  83 ++++++----
crates/lsp/src/lsp.rs                       |   7 
crates/util/src/paths.rs                    |   1 
crates/zed/src/languages.rs                 |  36 +++-
crates/zed/src/languages/c.rs               |  16 +
crates/zed/src/languages/elixir.rs          |  17 +
crates/zed/src/languages/github.rs          |  45 ++++++
crates/zed/src/languages/go.rs              |  31 ++-
crates/zed/src/languages/html.rs            |  68 ++++++---
crates/zed/src/languages/installation.rs    | 109 ---------------
crates/zed/src/languages/json.rs            | 127 +++++++++-------
crates/zed/src/languages/language_plugin.rs |  26 +--
crates/zed/src/languages/lua.rs             |  34 ++-
crates/zed/src/languages/node_runtime.rs    | 166 +++++++++++++++++++++++
crates/zed/src/languages/python.rs          |  57 +++++--
crates/zed/src/languages/ruby.rs            |  15 -
crates/zed/src/languages/rust.rs            |  16 +
crates/zed/src/languages/typescript.rs      | 102 ++++++++-----
crates/zed/src/languages/yaml.rs            |  60 +++++--
crates/zed/src/main.rs                      |   7 
crates/zed/src/zed.rs                       |  12 +
plugins/json_language/src/lib.rs            |  10 
23 files changed, 662 insertions(+), 389 deletions(-)

Detailed changes

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(),

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<str>);
 
+#[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<OsString>,
+}
+
 /// 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<String>,
     pub initialization_options: Option<Value>,
     pub disk_based_diagnostic_sources: Vec<String>,
     pub disk_based_diagnostics_progress_token: Option<String>,
@@ -93,7 +105,6 @@ pub struct CachedLspAdapter {
 impl CachedLspAdapter {
     pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
         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<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         self.adapter
             .fetch_server_binary(version, http, container_dir)
             .await
     }
 
-    pub async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    pub async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
         self.adapter.cached_server_binary(container_dir).await
     }
 
@@ -182,9 +195,9 @@ pub trait LspAdapter: 'static + Send + Sync {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf>;
+    ) -> Result<LanguageServerBinary>;
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf>;
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary>;
 
     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<String> {
-        Vec::new()
-    }
-
     async fn initialization_options(&self) -> Option<Value> {
         None
     }
@@ -488,7 +497,7 @@ pub struct LanguageRegistry {
     lsp_binary_paths: Mutex<
         HashMap<
             LanguageServerName,
-            Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
+            Shared<BoxFuture<'static, Result<LanguageServerBinary, Arc<anyhow::Error>>>>,
         >,
     >,
     executor: Option<Arc<Background>>,
@@ -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<CachedLspAdapter>,
     language: Arc<Language>,
     http_client: Arc<dyn HttpClient>,
     download_dir: Arc<Path>,
     statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
-) -> Result<PathBuf> {
+) -> Result<LanguageServerBinary> {
     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<CachedLspAdapter>,
     language: Arc<Language>,
     http_client: Arc<dyn HttpClient>,
     container_dir: &Path,
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
-) -> Result<PathBuf> {
+) -> Result<LanguageServerBinary> {
     let container_dir: Arc<Path> = 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<FakeLspAdapter> {
         _: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         _: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         unreachable!();
     }
 
-    async fn cached_server_binary(&self, _: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
         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(

crates/lsp/src/lsp.rs 🔗

@@ -108,7 +108,7 @@ impl LanguageServer {
     pub fn new<T: AsRef<std::ffi::OsStr>>(
         server_id: usize,
         binary_path: &Path,
-        args: &[T],
+        arguments: &[T],
         root_path: &Path,
         cx: AsyncAppContext,
     ) -> Result<Self> {
@@ -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();
         }

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");

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<LanguageRegistry>, themes: Arc<ThemeRegistry>) {
+pub fn init(
+    http: Arc<dyn HttpClient>,
+    background: Arc<Background>,
+    languages: Arc<LanguageRegistry>,
+    themes: Arc<ThemeRegistry>,
+) {
+    let node_runtime = NodeRuntime::new(http, background);
+
     for (name, grammar, lsp_adapter) in [
         (
             "c",
@@ -63,6 +74,7 @@ pub fn init(languages: Arc<LanguageRegistry>, themes: Arc<ThemeRegistry>) {
             "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<LanguageRegistry>, themes: Arc<ThemeRegistry>) {
         (
             "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<LanguageRegistry>, themes: Arc<ThemeRegistry>) {
         (
             "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<LanguageRegistry>, themes: Arc<ThemeRegistry>) {
         (
             "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);

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<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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 {:?}",

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<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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()

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<GithubReleaseAsset>,
+}
+
+#[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<dyn HttpClient>,
+) -> Result<GithubRelease, anyhow::Error> {
+    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)
+}

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<OsString> {
+    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<String> {
-        vec!["-mode=stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
@@ -47,7 +47,7 @@ impl super::LspAdapter for GoLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<Option<String>>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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"))
             }

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<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct HtmlLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 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<NodeRuntime>) -> 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<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        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<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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 {:?}",

crates/zed/src/languages/installation.rs 🔗

@@ -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<String>,
-}
-
-#[derive(Deserialize, Default)]
-struct NpmInfoDistTags {
-    latest: Option<String>,
-}
-
-#[derive(Deserialize)]
-pub(crate) struct GithubRelease {
-    pub name: String,
-    pub assets: Vec<GithubReleaseAsset>,
-}
-
-#[derive(Deserialize)]
-pub(crate) struct GithubReleaseAsset {
-    pub name: String,
-    pub browser_download_url: String,
-}
-
-pub async fn npm_package_latest_version(name: &str) -> Result<String> {
-    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<Item = (&str, &str)>,
-    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<dyn HttpClient>,
-) -> Result<GithubRelease, anyhow::Error> {
-    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)
-}

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<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
 pub struct JsonLspAdapter {
+    node: Arc<NodeRuntime>,
     languages: Arc<LanguageRegistry>,
     themes: Arc<ThemeRegistry>,
 }
 
 impl JsonLspAdapter {
-    pub fn new(languages: Arc<LanguageRegistry>, themes: Arc<ThemeRegistry>) -> Self {
-        Self { languages, themes }
+    pub fn new(
+        node: Arc<NodeRuntime>,
+        languages: Arc<LanguageRegistry>,
+        themes: Arc<ThemeRegistry>,
+    ) -> Self {
+        JsonLspAdapter {
+            node,
+            languages,
+            themes,
+        }
     }
 }
 
@@ -37,78 +52,80 @@ impl LspAdapter for JsonLspAdapter {
         LanguageServerName("json-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        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<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
-        let version = version.downcast::<GitHubLspBinaryVersion>().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,
-                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
-            )
-            .await?;
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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()

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<Background>) -> Result<PluginLspAdapter> {
 
 pub struct PluginLspAdapter {
     name: WasiFn<(), String>,
-    server_args: WasiFn<(), Vec<String>>,
     fetch_latest_server_version: WasiFn<(), Option<String>>,
-    fetch_server_binary: WasiFn<(PathBuf, String), Result<PathBuf, String>>,
-    cached_server_binary: WasiFn<PathBuf, Option<PathBuf>>,
+    fetch_server_binary: WasiFn<(PathBuf, String), Result<LanguageServerBinary, String>>,
+    cached_server_binary: WasiFn<PathBuf, Option<LanguageServerBinary>>,
     initialization_options: WasiFn<(), String>,
     language_ids: WasiFn<(), Vec<(String, String)>>,
     executor: Arc<Background>,
@@ -47,7 +46,6 @@ impl PluginLspAdapter {
     pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
         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<String> {
-        self.runtime
-            .lock()
-            .await
-            .call(&self.server_args, ())
-            .await
-            .unwrap()
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
@@ -105,7 +94,7 @@ impl LspAdapter for PluginLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = *version.downcast::<String>().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<PathBuf, String> =
+                let result: Result<LanguageServerBinary, String> =
                     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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         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<PathBuf> = runtime.call(&function, container_dir).await.ok()?;
+                let result: Option<LanguageServerBinary> =
+                    runtime.call(&function, container_dir).await.ok()?;
                 runtime.remove_resource(handle).ok()?;
                 result
             })

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<OsString> {
+    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<String> {
-        vec![
-            "--logpath=~/lua-language-server.log".into(),
-            "--loglevel=trace".into(),
-        ]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
@@ -57,7 +57,7 @@ impl super::LspAdapter for LuaLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 
         let binary_path = container_dir.join("bin/lua-language-server");
@@ -77,10 +77,13 @@ impl super::LspAdapter for LuaLspAdapter {
             <fs::Permissions as fs::unix::PermissionsExt>::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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         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"))
             }

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<String>,
+}
+
+#[derive(Deserialize, Default)]
+pub struct NpmInfoDistTags {
+    latest: Option<String>,
+}
+
+pub struct NodeRuntime {
+    http: Arc<dyn HttpClient>,
+    background: Arc<Background>,
+    installation_path: Mutex<Option<Shared<Task<Result<PathBuf, Arc<anyhow::Error>>>>>>,
+}
+
+impl NodeRuntime {
+    pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
+        Arc::new(NodeRuntime {
+            http,
+            background,
+            installation_path: Mutex::new(None),
+        })
+    }
+
+    pub async fn binary_path(&self) -> Result<PathBuf> {
+        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<String> {
+        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<Item = (&str, &str)>,
+        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<PathBuf> {
+        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<dyn HttpClient>) -> Result<PathBuf> {
+        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)
+    }
+}

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<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct PythonLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 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<NodeRuntime>) -> Self {
+        PythonLspAdapter { node }
+    }
 }
 
 #[async_trait]
@@ -20,15 +35,11 @@ impl LspAdapter for PythonLspAdapter {
         LanguageServerName("pyright".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        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<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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 {:?}",

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<String> {
-        vec!["stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
@@ -28,12 +24,15 @@ impl LspAdapter for RubyLanguageServer {
         _version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         _container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         Err(anyhow!("solargraph must be installed manually"))
     }
 
-    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<PathBuf> {
-        Some("solargraph".into())
+    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "solargraph".into(),
+            arguments: vec!["stdio".into()],
+        })
     }
 
     async fn label_for_completion(

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<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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()

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<OsString> {
+    vec![
+        server_path.into(),
+        "--stdio".into(),
+        "--tsserver-path".into(),
+        "node_modules/typescript/lib".into(),
+    ]
+}
+
+pub struct TypeScriptLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 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<NodeRuntime>) -> Self {
+        TypeScriptLspAdapter { node }
+    }
 }
 
 struct Versions {
@@ -27,20 +47,16 @@ impl LspAdapter for TypeScriptLspAdapter {
         LanguageServerName("typescript-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        ["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
-            .into_iter()
-            .map(str::to_string)
-            .collect()
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         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<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let versions = versions.downcast::<Versions>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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 {:?}",

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<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
 
-pub struct YamlLspAdapter;
+pub struct YamlLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 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<NodeRuntime>) -> Self {
+        YamlLspAdapter { node }
+    }
 }
 
 #[async_trait]
@@ -24,15 +39,15 @@ impl LspAdapter for YamlLspAdapter {
         LanguageServerName("yaml-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        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<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().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<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| 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 {:?}",

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());

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);
         }

plugins/json_language/src/lib.rs 🔗

@@ -6,7 +6,7 @@ use serde::Deserialize;
 #[import]
 fn command(string: &str) -> Option<Vec<u8>>;
 
-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<Pa
     let version_dir = container_dir.join(version.as_str());
     fs::create_dir_all(&version_dir)
         .map_err(|_| "failed to create version directory".to_string())?;
-    let binary_path = version_dir.join(BIN_PATH);
+    let binary_path = version_dir.join(SERVER_PATH);
 
     if fs::metadata(&binary_path).is_err() {
         let output = command(&format!(
@@ -76,9 +76,9 @@ pub fn cached_server_binary(container_dir: PathBuf) -> Option<PathBuf> {
     }
 
     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