Initial impl of `NodeRuntime` w/JSON borked and a deadlock :)

Julia and Antonio Scandurra created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>

Change summary

crates/language/src/language.rs             |  64 +++-----
crates/lsp/src/lsp.rs                       |  17 +-
crates/zed/src/languages.rs                 |  36 +++-
crates/zed/src/languages/c.rs               |   4 
crates/zed/src/languages/elixir.rs          |   4 
crates/zed/src/languages/go.rs              |   8 
crates/zed/src/languages/html.rs            |  70 +++++----
crates/zed/src/languages/installation.rs    | 119 ----------------
crates/zed/src/languages/json.rs            |  41 +++--
crates/zed/src/languages/language_plugin.rs |  13 -
crates/zed/src/languages/lua.rs             |  10 -
crates/zed/src/languages/node_runtime.rs    | 159 +++++++++++++++++++++++
crates/zed/src/languages/python.rs          |  55 ++++---
crates/zed/src/languages/ruby.rs            |   6 
crates/zed/src/languages/rust.rs            |   4 
crates/zed/src/languages/typescript.rs      | 100 ++++++++------
crates/zed/src/languages/yaml.rs            |  66 +++++----
crates/zed/src/main.rs                      |  18 +-
crates/zed/src/zed.rs                       |   8 +
plugins/json_language/src/lib.rs            |  10 
20 files changed, 436 insertions(+), 376 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -29,6 +29,7 @@ use std::{
     any::Any,
     borrow::Cow,
     cell::RefCell,
+    ffi::OsString,
     fmt::Debug,
     hash::Hash,
     mem,
@@ -86,7 +87,7 @@ pub enum ServerExecutionKind {
 #[derive(Debug, Clone, Deserialize)]
 pub struct LanguageServerBinary {
     pub path: PathBuf,
-    pub arguments: Vec<String>,
+    pub arguments: Vec<OsString>,
 }
 
 /// Represents a Language Server, with certain cached sync properties.
@@ -184,8 +185,6 @@ impl CachedLspAdapter {
 pub trait LspAdapter: 'static + Send + Sync {
     async fn name(&self) -> LanguageServerName;
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind;
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
@@ -494,7 +493,6 @@ pub struct LanguageRegistry {
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
     lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
     login_shell_env_loaded: Shared<Task<()>>,
-    node_path: Shared<Task<Option<PathBuf>>>,
     #[allow(clippy::type_complexity)]
     lsp_binary_paths: Mutex<
         HashMap<
@@ -516,7 +514,7 @@ struct LanguageRegistryState {
 }
 
 impl LanguageRegistry {
-    pub fn new(login_shell_env_loaded: Task<()>, node_path: Task<Option<PathBuf>>) -> Self {
+    pub fn new(login_shell_env_loaded: Task<()>) -> Self {
         let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
         Self {
             state: RwLock::new(LanguageRegistryState {
@@ -532,7 +530,6 @@ impl LanguageRegistry {
             lsp_binary_statuses_tx,
             lsp_binary_statuses_rx,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
-            node_path: node_path.shared(),
             lsp_binary_paths: Default::default(),
             executor: None,
         }
@@ -540,7 +537,7 @@ impl LanguageRegistry {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test() -> Self {
-        Self::new(Task::ready(()), Task::Ready(None))
+        Self::new(Task::ready(()))
     }
 
     pub fn set_executor(&mut self, executor: Arc<Background>) {
@@ -795,6 +792,8 @@ impl LanguageRegistry {
                 Ok(server)
             }));
         }
+        
+        dbg!();
 
         let download_dir = self
             .language_server_download_dir
@@ -806,18 +805,18 @@ 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();
-        let node_path = self.node_path.clone();
+        dbg!();
 
         Some(cx.spawn(|cx| async move {
             login_shell_env_loaded.await;
-            let node_path = node_path.await;
 
-            let server_binary = this
+            let binary = this
                 .lsp_binary_paths
                 .lock()
                 .entry(adapter.name.clone())
                 .or_insert_with(|| {
-                    get_server_binary(
+                    dbg!();
+                    get_binary(
                         adapter.clone(),
                         language.clone(),
                         http_client,
@@ -829,30 +828,19 @@ impl LanguageRegistry {
                     .shared()
                 })
                 .clone()
-                .map_err(|e| anyhow!(e));
-
-            let server_binary = server_binary.await?;
-            let server_name = server_binary
-                .path
-                .file_name()
-                .map(|name| name.to_string_lossy().to_string());
-
-            let mut command = match adapter.adapter.server_execution_kind().await {
-                ServerExecutionKind::Node => {
-                    let node_path = node_path
-                        .ok_or(anyhow!("Missing Node path for Node based language server"))?;
-                    let node_binary = node_path.join("bin/node");
-                    let mut command = smol::process::Command::new(node_binary);
-                    command.arg(dbg!(server_binary.path));
-                    command
-                }
-
-                ServerExecutionKind::Launch => smol::process::Command::new(server_binary.path),
-            };
+                .map_err(|e| anyhow!(e))
+                .await?;
+            dbg!();
 
-            command.args(dbg!(&server_binary.arguments));
-            let server = lsp::LanguageServer::new(server_id, server_name, command, &root_path, cx)?;
+            let server = lsp::LanguageServer::new(
+                server_id,
+                &binary.path,
+                &binary.arguments,
+                &root_path,
+                cx,
+            )?;
 
+            dbg!();
             Ok(server)
         }))
     }
@@ -882,7 +870,7 @@ impl Default for LanguageRegistry {
     }
 }
 
-async fn get_server_binary(
+async fn get_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
     http_client: Arc<dyn HttpClient>,
@@ -896,7 +884,7 @@ async fn get_server_binary(
             .context("failed to create container directory")?;
     }
 
-    let binary = fetch_latest_server_binary(
+    let binary = fetch_latest_binary(
         adapter.clone(),
         language.clone(),
         http_client,
@@ -924,7 +912,7 @@ async fn get_server_binary(
     binary
 }
 
-async fn fetch_latest_server_binary(
+async fn fetch_latest_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
     http_client: Arc<dyn HttpClient>,
@@ -1471,10 +1459,6 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         LanguageServerName(self.name.into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,

crates/lsp/src/lsp.rs 🔗

@@ -105,10 +105,10 @@ struct Error {
 }
 
 impl LanguageServer {
-    pub fn new(
+    pub fn new<T: AsRef<std::ffi::OsStr>>(
         server_id: usize,
-        server_name: Option<String>,
-        mut command: process::Command,
+        binary_path: &Path,
+        arguments: &[T],
         root_path: &Path,
         cx: AsyncAppContext,
     ) -> Result<Self> {
@@ -118,13 +118,14 @@ impl LanguageServer {
             root_path.parent().unwrap_or_else(|| Path::new("/"))
         };
 
-        let mut server = dbg!(command
+        let mut server = process::Command::new(binary_path)
             .current_dir(working_dir)
+            .args(arguments)
             .stdin(Stdio::piped())
             .stdout(Stdio::piped())
             .stderr(Stdio::inherit())
-            .kill_on_drop(true))
-        .spawn()?;
+            .kill_on_drop(true)
+            .spawn()?;
 
         let stdin = server.stdin.take().unwrap();
         let stout = server.stdout.take().unwrap();
@@ -147,8 +148,8 @@ impl LanguageServer {
             },
         );
 
-        if let Some(name) = server_name {
-            server.name = name;
+        if let Some(name) = binary_path.file_name() {
+            server.name = name.to_string_lossy().to_string();
         }
         Ok(server)
     }

crates/zed/src/languages.rs 🔗

@@ -1,5 +1,8 @@
 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;
@@ -12,14 +15,13 @@ mod installation;
 mod json;
 mod language_plugin;
 mod lua;
+mod node_runtime;
 mod python;
 mod ruby;
 mod rust;
 mod typescript;
 mod yaml;
 
-pub use installation::ensure_node_installation_dir;
-
 // 1. Add tree-sitter-{language} parser to zed crate
 // 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below
 // 3. Add config.toml to the newly created language directory using existing languages as a template
@@ -34,7 +36,14 @@ pub use installation::ensure_node_installation_dir;
 #[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",
@@ -65,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(),
             ))),
@@ -77,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",
@@ -92,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",
@@ -137,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 🔗

@@ -16,10 +16,6 @@ impl super::LspAdapter for CLspAdapter {
         LanguageServerName("clangd".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,

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

@@ -17,10 +17,6 @@ impl LspAdapter for ElixirLspAdapter {
         LanguageServerName("elixir-ls".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,

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

@@ -7,10 +7,10 @@ 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<String> {
+fn server_binary_arguments() -> Vec<OsString> {
     vec!["-mode=stdio".into()]
 }
 
@@ -27,10 +27,6 @@ impl super::LspAdapter for GoLspAdapter {
         LanguageServerName("gopls".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,

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

@@ -1,23 +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::{LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind};
+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;
 
-fn server_binary_arguments() -> Vec<String> {
-    vec!["--stdio".into()]
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
 }
 
-pub struct HtmlLspAdapter;
+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]
@@ -26,24 +37,21 @@ impl LspAdapter for HtmlLspAdapter {
         LanguageServerName("vscode-html-language-server".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Node
-    }
-
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(
-            Box::new(npm_package_latest_version(http, "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(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
@@ -51,15 +59,15 @@ impl LspAdapter for HtmlLspAdapter {
         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(
-                http,
-                [("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 {
@@ -74,8 +82,8 @@ impl LspAdapter for HtmlLspAdapter {
         }
 
         Ok(LanguageServerBinary {
-            path: binary_path,
-            arguments: server_binary_arguments(),
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
         })
     }
 
@@ -90,11 +98,11 @@ 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() {
+            let server_path = last_version_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
                 Ok(LanguageServerBinary {
-                    path: bin_path,
-                    arguments: server_binary_arguments(),
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
                 })
             } else {
                 Err(anyhow!(

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

@@ -1,34 +1,14 @@
-use anyhow::{anyhow, Context, Result};
-use async_compression::futures::bufread::GzipDecoder;
-use async_tar::Archive;
+use anyhow::{Context, Result};
 use client::http::HttpClient;
-use futures::io::BufReader;
 use serde::Deserialize;
-use smol::fs::{self};
 use smol::io::AsyncReadExt;
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-};
+use std::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,
@@ -41,101 +21,6 @@ pub(crate) struct GithubReleaseAsset {
     pub browser_download_url: String,
 }
 
-pub async fn ensure_node_installation_dir(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
-    eprintln!("ensure_node_installation_dir");
-
-    let version = "v18.15.0";
-    let arch = "arm64";
-
-    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 url = format!("https://nodejs.org/dist/{version}/node-{version}-darwin-{arch}.tar.gz");
-        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?;
-        eprintln!("unpacked");
-    }
-
-    Ok(node_dir)
-}
-
-pub async fn npm_package_latest_version(http: Arc<dyn HttpClient>, name: &str) -> Result<String> {
-    let node_dir = ensure_node_installation_dir(http).await?;
-    let node_binary = node_dir.join("bin/npm");
-    let npm_file = node_dir.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(
-    http: Arc<dyn HttpClient>,
-    packages: impl IntoIterator<Item = (&str, &str)>,
-    directory: &Path,
-) -> Result<()> {
-    let node_dir = ensure_node_installation_dir(http).await?;
-    let node_binary = node_dir.join("bin/npm");
-    let npm_file = node_dir.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(())
-}
-
 pub(crate) async fn latest_github_release(
     repo_name_with_owner: &str,
     http: Arc<dyn HttpClient>,

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

@@ -1,4 +1,7 @@
-use super::installation::{latest_github_release, GitHubLspBinaryVersion};
+use super::{
+    installation::{latest_github_release, GitHubLspBinaryVersion},
+    node_runtime::NodeRuntime,
+};
 use anyhow::{anyhow, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
@@ -6,15 +9,14 @@ use client::http::HttpClient;
 use collections::HashMap;
 use futures::{future::BoxFuture, io::BufReader, FutureExt, StreamExt};
 use gpui::MutableAppContext;
-use language::{
-    LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind,
-};
+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 std::{
     any::Any,
     env::consts,
+    ffi::OsString,
     future,
     path::{Path, PathBuf},
     sync::Arc,
@@ -22,18 +24,27 @@ use std::{
 use theme::ThemeRegistry;
 use util::{paths, ResultExt, StaffMode};
 
-fn server_binary_arguments() -> Vec<String> {
-    vec!["--stdio".into()]
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    dbg!(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,
+        }
     }
 }
 
@@ -43,10 +54,6 @@ impl LspAdapter for JsonLspAdapter {
         LanguageServerName("json-language-server".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Node
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
@@ -105,8 +112,8 @@ impl LspAdapter for JsonLspAdapter {
         }
 
         Ok(LanguageServerBinary {
-            path: destination_path,
-            arguments: server_binary_arguments(),
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&destination_path),
         })
     }
 
@@ -118,8 +125,10 @@ impl LspAdapter for JsonLspAdapter {
                 last = Some(entry?.path());
             }
             anyhow::Ok(LanguageServerBinary {
-                path: last.ok_or_else(|| anyhow!("no cached binary"))?,
-                arguments: server_binary_arguments(),
+                path: self.node.binary_path().await?,
+                arguments: server_binary_arguments(
+                    &last.ok_or_else(|| anyhow!("no cached binary"))?,
+                ),
             })
         })()
         .await

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::{LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
 use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
 use std::{any::Any, path::PathBuf, sync::Arc};
 use util::ResultExt;
@@ -32,7 +32,6 @@ pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
 
 pub struct PluginLspAdapter {
     name: WasiFn<(), String>,
-    server_execution_kind: WasiFn<(), ServerExecutionKind>,
     fetch_latest_server_version: WasiFn<(), Option<String>>,
     fetch_server_binary: WasiFn<(PathBuf, String), Result<LanguageServerBinary, String>>,
     cached_server_binary: WasiFn<PathBuf, Option<LanguageServerBinary>>,
@@ -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_execution_kind: plugin.function("server_execution_kind")?,
             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_execution_kind(&self) -> ServerExecutionKind {
-        self.runtime
-            .lock()
-            .await
-            .call(&self.server_execution_kind, ())
-            .await
-            .unwrap()
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,

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,7 +6,7 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use client::http::HttpClient;
 use futures::{io::BufReader, StreamExt};
-use language::{LanguageServerBinary, LanguageServerName, ServerExecutionKind};
+use language::{LanguageServerBinary, LanguageServerName};
 use smol::fs;
 use util::{async_iife, ResultExt};
 
@@ -15,7 +15,7 @@ use super::installation::{latest_github_release, GitHubLspBinaryVersion};
 #[derive(Copy, Clone)]
 pub struct LuaLspAdapter;
 
-fn server_binary_arguments() -> Vec<String> {
+fn server_binary_arguments() -> Vec<OsString> {
     vec![
         "--logpath=~/lua-language-server.log".into(),
         "--loglevel=trace".into(),
@@ -28,10 +28,6 @@ impl super::LspAdapter for LuaLspAdapter {
         LanguageServerName("lua-language-server".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,

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

@@ -0,0 +1,159 @@
+use anyhow::{anyhow, Context, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use client::http::HttpClient;
+use futures::{future::Shared, FutureExt, TryFutureExt};
+use gpui::{executor::Background, Task};
+use parking_lot::Mutex;
+use serde::Deserialize;
+use smol::{fs, io::BufReader};
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+#[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 {
+                            let version = "v18.15.0";
+                            let arch = "arm64";
+
+                            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 url = format!("https://nodejs.org/dist/{version}/node-{version}-darwin-{arch}.tar.gz");
+                                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)
+                        }
+                        .map_err(Arc::new),
+                    )
+                    .shared()
+            })
+            .clone();
+
+        match task.await {
+            Ok(path) => Ok(path),
+            Err(error) => Err(anyhow!("{}", error)),
+        }
+    }
+}

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

@@ -1,21 +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::{LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind};
+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()]
+}
 
-fn server_binary_arguments() -> Vec<String> {
-    vec!["--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]
@@ -24,21 +35,17 @@ impl LspAdapter for PythonLspAdapter {
         LanguageServerName("pyright".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Node
-    }
-
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version(http, "pyright").await?) as Box<_>)
+        Ok(Box::new(self.node.npm_package_latest_version("pyright").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<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
@@ -46,10 +53,12 @@ impl LspAdapter for PythonLspAdapter {
         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(http, [("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 {
@@ -64,8 +73,8 @@ impl LspAdapter for PythonLspAdapter {
         }
 
         Ok(LanguageServerBinary {
-            path: binary_path,
-            arguments: server_binary_arguments(),
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
         })
     }
 
@@ -80,11 +89,11 @@ 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() {
+            let server_path = last_version_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
                 Ok(LanguageServerBinary {
-                    path: bin_path,
-                    arguments: server_binary_arguments(),
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
                 })
             } else {
                 Err(anyhow!(

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::{LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind};
+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_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,

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

@@ -19,10 +19,6 @@ impl LspAdapter for RustLspAdapter {
         LanguageServerName("rust-analyzer".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Launch
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,

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

@@ -1,26 +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::{LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind};
+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;
 
-fn server_binary_arguments() -> Vec<String> {
-    ["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
-        .into_iter()
-        .map(Into::into)
-        .collect()
+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;
+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 {
@@ -34,24 +47,23 @@ impl LspAdapter for TypeScriptLspAdapter {
         LanguageServerName("typescript-language-server".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Node
-    }
-
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(Versions {
-            typescript_version: npm_package_latest_version(http.clone(), "typescript").await?,
-            server_version: npm_package_latest_version(http, "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<_>)
     }
 
     async fn fetch_server_binary(
         &self,
         versions: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<LanguageServerBinary> {
         let versions = versions.downcast::<Versions>().unwrap();
@@ -62,21 +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(
-                http,
-                [
-                    ("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 {
@@ -91,8 +103,8 @@ impl LspAdapter for TypeScriptLspAdapter {
         }
 
         Ok(LanguageServerBinary {
-            path: binary_path,
-            arguments: server_binary_arguments(),
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
         })
     }
 
@@ -107,17 +119,17 @@ 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() {
+            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: new_bin_path,
-                    arguments: server_binary_arguments(),
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&new_server_path),
                 })
-            } else if old_bin_path.exists() {
+            } else if old_server_path.exists() {
                 Ok(LanguageServerBinary {
-                    path: old_bin_path,
-                    arguments: server_binary_arguments(),
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&old_server_path),
                 })
             } else {
                 Err(anyhow!(

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

@@ -1,25 +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::{LanguageServerBinary, LanguageServerName, LspAdapter, ServerExecutionKind};
+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() -> Vec<String> {
-    vec!["--stdio".into()]
+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]
@@ -28,21 +39,21 @@ impl LspAdapter for YamlLspAdapter {
         LanguageServerName("yaml-language-server".into())
     }
 
-    async fn server_execution_kind(&self) -> ServerExecutionKind {
-        ServerExecutionKind::Node
-    }
-
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version(http, "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(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
@@ -50,15 +61,12 @@ impl LspAdapter for YamlLspAdapter {
         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(
-                http,
-                [("yaml-language-server", version.as_str())],
-                &version_dir,
-            )
-            .await?;
+        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() {
                 while let Some(entry) = entries.next().await {
@@ -73,8 +81,8 @@ impl LspAdapter for YamlLspAdapter {
         }
 
         Ok(LanguageServerBinary {
-            path: binary_path,
-            arguments: server_binary_arguments(),
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
         })
     }
 
@@ -89,11 +97,11 @@ 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() {
+            let server_path = last_version_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
                 Ok(LanguageServerBinary {
-                    path: bin_path,
-                    arguments: server_binary_arguments(),
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
                 })
             } else {
                 Err(anyhow!(

crates/zed/src/main.rs 🔗

@@ -81,15 +81,6 @@ fn main() {
         })
     };
 
-    let node_path = {
-        let http = http.clone();
-        app.background().spawn(async move {
-            languages::ensure_node_installation_dir(http.clone())
-                .await
-                .log_err()
-        })
-    };
-
     let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
     let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded();
     app.on_open_urls(move |urls, _| {
@@ -144,11 +135,16 @@ fn main() {
         }
 
         let client = client::Client::new(http.clone(), cx);
-        let mut languages = LanguageRegistry::new(login_shell_env_loaded, node_path);
+        let mut languages = LanguageRegistry::new(login_shell_env_loaded);
         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,6 +652,7 @@ 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, TestAppContext, ViewHandle,
@@ -1850,7 +1851,12 @@ mod tests {
         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