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 | 22 +++++++--- 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 | 23 ++++++++--- 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, 215 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6693f9639af701d9ece2c1a257d41cf237df16b9..d23485768fc9357a1b2ae00fa709f6747fc9525c 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 b44bf830172446d6910b3415da5e12281696940d..8c2596f11fa90f439144d4b5207a0f0c50513a5f 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 95aa150d61648154fa7295b28407b13c79d205b5..75db8e9e945b0a9baef6db91ef3835d5b07727fb 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 ba55ed31345993642aaf15bed77ffb41e06f1140..a91ff8befce50a7394cdc832c837c3f8f2b720e1 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 35ee844d89f3b4a4f2f696494799512a0b595df0..3d768c577d872f8d5cc6c5aa1accf4b23309375c 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 5f99649ee937422066ad6de925f1730d16de0cb2..b82b92941e636633e0a4d6f956a7dae4cd271c05 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 3935d20456bb733573bbfcfb78c6b7a58a81a345..a8dbfd47ba144236e5294a4d30669ef50883f543 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 1b32876155f3aebdf29c1b4e881ef05e9c33cdbf..2c9cc76ac4adcfd6b696587941478eac4c5ca66b 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 0fcb2b3b23dab742787ebadd641792ba06d095e1..1e539826da65661f76bf595b0323888e31765ca8 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); - - if fs::metadata(&server_path).await.is_err() { + let package_name = "intelephense"; + + 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 17fcb5fd3f26f7c9fb70c669cca748966a5e2136..40f65babf015cf861bf2a04b1b576cbfe95a0deb 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 8787826a188830bd07a5246b8dd3c8c4ecdff35b..e5a167f7ae9fa2e60f34f2ed66ed222abd7d74e6 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 bd9eab3ced4f290fd6577afcfecc1060c1018836..48f5b29210181ef97ad775524bcc98c82b299679 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 721c2e6640d9892ccbcf32c8d7b09d7f047a9fdd..58d1dae2ea4cc313cc7834201e6a27f5a9b7011f 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 c49f5d859036103e9f10a8b531f3a3d8a8d2ea69..49a60102ad61172226928721f99f0f2ea6ef7c1a 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 bf220130a9679ca3eb83c1c3d61f0499738e066f..de6d5b3f01fddb63813cc3dd3e64f0ffaa2afcf2 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); - - if fs::metadata(&server_path).await.is_err() { + let package_name = "typescript"; + + 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 e29516a5df2e5067908ca9ef2272caac2863b334..6c611d830a6523904ef486d38fff7920f2069cd0 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 5c288c22b6111149babd246874c233464b2f49e3..ce8544e0120fd9e15398fa1adcf4dc00933937d2 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 7e713a3e2d37904b30a20d7d26342354d2474c0e..1097f85f38954176dc50ee68eeb5f3af38fa1dcd 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 7317635dd103ca3eaffcbe4478da8b485a4175c5..59f136d7ec5e9ab0d096b2583804bd9756328804 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",