From 276139f792f8c88564d252908dd41fa502fd6695 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Mar 2024 11:40:28 -0400 Subject: [PATCH] Implement updating for node-based language servers (#9361) Fixes: https://github.com/zed-industries/zed/issues/9234 This doesn't address `vue` as it has a slightly different install code, but it should be fairly simple to add - I'll add it in in a follow-up. This PR will allow all (except `vue`) node-based language servers to update. It is mostly just throwing in a method into the `NodeRuntime` trait that is used for checking if a package doesn't exist locally, or is out of date, by checking the version against what's newest, and installing. If any parsing of the `package.json` data fails along the way, it assumes something has gone awry on the users system, logs the error, and then proceeds with trying to install the package, so that users don't get stuck on version if their package has some bad data. Outside of adding this method, it just adds that check in all of the language server's individual `fetch_server_binary` methods. Release Notes: - Added updating for node-based language servers ([#9234](https://github.com/zed-industries/zed/issues/9234)). --- Cargo.lock | 1 + crates/language/src/language.rs | 6 +-- crates/languages/src/astro.rs | 17 ++++---- crates/languages/src/css.rs | 17 ++++---- crates/languages/src/dockerfile.rs | 17 ++++---- crates/languages/src/elm.rs | 17 ++++---- crates/languages/src/html.rs | 17 ++++---- crates/languages/src/json.rs | 17 ++++---- crates/languages/src/php.rs | 20 +++++++-- crates/languages/src/prisma.rs | 17 ++++---- crates/languages/src/purescript.rs | 17 ++++---- crates/languages/src/python.rs | 15 ++++--- crates/languages/src/svelte.rs | 17 ++++---- crates/languages/src/tailwind.rs | 17 ++++---- crates/languages/src/typescript.rs | 21 +++++++--- crates/languages/src/vue.rs | 1 + crates/languages/src/yaml.rs | 17 ++++---- crates/node_runtime/Cargo.toml | 1 + crates/node_runtime/src/node_runtime.rs | 55 +++++++++++++++++++++++++ 19 files changed, 213 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6693f9639a..d23485768f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6091,6 +6091,7 @@ dependencies = [ "async-trait", "futures 0.3.28", "log", + "semver", "serde", "serde_json", "smol", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b44bf83017..8c2596f11f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -336,12 +336,12 @@ pub trait LspAdapter: 'static + Send + Sync { name.clone(), LanguageServerBinaryStatus::CheckingForUpdate, ); - let version_info = self.fetch_latest_server_version(delegate.as_ref()).await?; + let latest_version = self.fetch_latest_server_version(delegate.as_ref()).await?; log::info!("downloading language server {:?}", name.0); delegate.update_status(self.name(), LanguageServerBinaryStatus::Downloading); let mut binary = self - .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate.as_ref()) + .fetch_server_binary(latest_version, container_dir.to_path_buf(), delegate.as_ref()) .await; delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded); @@ -408,7 +408,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result; diff --git a/crates/languages/src/astro.rs b/crates/languages/src/astro.rs index 95aa150d61..75db8e9e94 100644 --- a/crates/languages/src/astro.rs +++ b/crates/languages/src/astro.rs @@ -49,19 +49,22 @@ impl LspAdapter for AstroLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "@astrojs/language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_npm_package = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_npm_package { self.node - .npm_install_packages( - &container_dir, - &[("@astrojs/language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index ba55ed3134..a91ff8befc 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -50,19 +50,22 @@ impl LspAdapter for CssLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "vscode-langservers-extracted"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("vscode-langservers-extracted", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/dockerfile.rs b/crates/languages/src/dockerfile.rs index 35ee844d89..3d768c577d 100644 --- a/crates/languages/src/dockerfile.rs +++ b/crates/languages/src/dockerfile.rs @@ -48,19 +48,22 @@ impl LspAdapter for DockerfileLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "dockerfile-language-server-nodejs"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("dockerfile-language-server-nodejs", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/elm.rs b/crates/languages/src/elm.rs index 5f99649ee9..b82b92941e 100644 --- a/crates/languages/src/elm.rs +++ b/crates/languages/src/elm.rs @@ -53,19 +53,22 @@ impl LspAdapter for ElmLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "@elm-tooling/elm-language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("@elm-tooling/elm-language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/html.rs b/crates/languages/src/html.rs index 3935d20456..a8dbfd47ba 100644 --- a/crates/languages/src/html.rs +++ b/crates/languages/src/html.rs @@ -50,19 +50,22 @@ impl LspAdapter for HtmlLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "vscode-langservers-extracted"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("vscode-langservers-extracted", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 1b32876155..2c9cc76ac4 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -102,19 +102,22 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "vscode-json-languageserver"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("vscode-json-languageserver", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/php.rs b/crates/languages/src/php.rs index 0fcb2b3b23..1e539826da 100644 --- a/crates/languages/src/php.rs +++ b/crates/languages/src/php.rs @@ -51,18 +51,30 @@ impl LspAdapter for IntelephenseLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _delegate: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(Self::SERVER_PATH); + let package_name = "intelephense"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package( + package_name, + &server_path, + &container_dir, + latest_version.0.as_str(), + ) + .await; + + if should_install_language_server { self.node - .npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())]) + .npm_install_packages(&container_dir, &[(package_name, latest_version.0.as_str())]) .await?; } + Ok(LanguageServerBinary { path: self.node.binary_path().await?, env: None, diff --git a/crates/languages/src/prisma.rs b/crates/languages/src/prisma.rs index 17fcb5fd3f..40f65babf0 100644 --- a/crates/languages/src/prisma.rs +++ b/crates/languages/src/prisma.rs @@ -48,19 +48,22 @@ impl LspAdapter for PrismaLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "@prisma/language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("@prisma/language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/purescript.rs b/crates/languages/src/purescript.rs index 8787826a18..e5a167f7ae 100644 --- a/crates/languages/src/purescript.rs +++ b/crates/languages/src/purescript.rs @@ -52,19 +52,22 @@ impl LspAdapter for PurescriptLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "purescript-language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_npm_package = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_npm_package { self.node - .npm_install_packages( - &container_dir, - &[("purescript-language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index bd9eab3ced..48f5b29210 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -3,7 +3,6 @@ use async_trait::async_trait; use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; -use smol::fs; use std::{ any::Any, ffi::OsString, @@ -43,16 +42,22 @@ impl LspAdapter for PythonLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "pyright"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages(&container_dir, &[("pyright", version.as_str())]) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/svelte.rs b/crates/languages/src/svelte.rs index 721c2e6640..58d1dae2ea 100644 --- a/crates/languages/src/svelte.rs +++ b/crates/languages/src/svelte.rs @@ -49,19 +49,22 @@ impl LspAdapter for SvelteLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "svelte-language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("svelte-language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index c49f5d8590..49a60102ad 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -51,19 +51,22 @@ impl LspAdapter for TailwindLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "@tailwindcss/language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("@tailwindcss/language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index bf220130a9..de6d5b3f01 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -71,22 +71,33 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(Self::NEW_SERVER_PATH); + let package_name = "typescript"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package( + package_name, + &server_path, + &container_dir, + latest_version.typescript_version.as_str(), + ) + .await; + + if should_install_language_server { self.node .npm_install_packages( &container_dir, &[ - ("typescript", version.typescript_version.as_str()), + (package_name, latest_version.typescript_version.as_str()), ( "typescript-language-server", - version.server_version.as_str(), + latest_version.server_version.as_str(), ), ], ) diff --git a/crates/languages/src/vue.rs b/crates/languages/src/vue.rs index e29516a5df..6c611d830a 100644 --- a/crates/languages/src/vue.rs +++ b/crates/languages/src/vue.rs @@ -86,6 +86,7 @@ impl super::LspAdapter for VueLspAdapter { let version = version.downcast::().unwrap(); let server_path = container_dir.join(Self::SERVER_PATH); let ts_path = container_dir.join(Self::TYPESCRIPT_PATH); + if fs::metadata(&server_path).await.is_err() { self.node .npm_install_packages( diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 5c288c22b6..ce8544e012 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -52,19 +52,22 @@ impl LspAdapter for YamlLspAdapter { async fn fetch_server_binary( &self, - version: Box, + latest_version: Box, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); + let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); + let package_name = "yaml-language-server"; - if fs::metadata(&server_path).await.is_err() { + let should_install_language_server = self + .node + .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version) + .await; + + if should_install_language_server { self.node - .npm_install_packages( - &container_dir, - &[("yaml-language-server", version.as_str())], - ) + .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())]) .await?; } diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index 7e713a3e2d..1097f85f38 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -19,6 +19,7 @@ async-tar.workspace = true async-trait.workspace = true futures.workspace = true log.workspace = true +semver.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 7317635dd1..59f136d7ec 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -1,7 +1,10 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; +use futures::AsyncReadExt; +use semver::Version; use serde::Deserialize; +use serde_json::Value; use smol::{fs, io::BufReader, lock::Mutex, process::Command}; use std::process::{Output, Stdio}; use std::{ @@ -10,6 +13,7 @@ use std::{ sync::Arc, }; use util::http::HttpClient; +use util::ResultExt; const VERSION: &str = "v18.15.0"; @@ -41,6 +45,56 @@ pub trait NodeRuntime: Send + Sync { async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)]) -> Result<()>; + + async fn should_install_npm_package( + &self, + package_name: &str, + local_executable_path: &Path, + local_package_directory: &PathBuf, + latest_version: &str, + ) -> bool { + // In the case of the local system not having the package installed, + // or in the instances where we fail to parse package.json data, + // we attempt to install the package. + if fs::metadata(local_executable_path).await.is_err() { + return true; + } + + let package_json_path = local_package_directory.join("package.json"); + + let mut contents = String::new(); + + let Some(mut file) = fs::File::open(package_json_path).await.log_err() else { + return true; + }; + + file.read_to_string(&mut contents).await.log_err(); + + let Some(package_json): Option = serde_json::from_str(&contents).log_err() else { + return true; + }; + + let installed_version = package_json + .get("dependencies") + .and_then(|deps| deps.get(package_name)) + .and_then(|server_name| server_name.as_str()); + + let Some(installed_version) = installed_version else { + return true; + }; + + let Some(latest_version) = Version::parse(latest_version).log_err() else { + return true; + }; + + let installed_version = installed_version.trim_start_matches(|c: char| !c.is_ascii_digit()); + + let Some(installed_version) = Version::parse(installed_version).log_err() else { + return true; + }; + + installed_version < latest_version + } } pub struct RealNodeRuntime { @@ -239,6 +293,7 @@ impl NodeRuntime for RealNodeRuntime { let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); arguments.extend_from_slice(&[ + "--save-exact", "--fetch-retry-mintimeout", "2000", "--fetch-retry-maxtimeout",