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",