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)).
This commit is contained in:
Joseph T. Lyons 2024-03-15 11:40:28 -04:00 committed by GitHub
parent cb16003133
commit 276139f792
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 213 additions and 94 deletions

1
Cargo.lock generated
View File

@ -6091,6 +6091,7 @@ dependencies = [
"async-trait",
"futures 0.3.28",
"log",
"semver",
"serde",
"serde_json",
"smol",

View File

@ -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<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary>;

View File

@ -49,19 +49,22 @@ impl LspAdapter for AstroLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -50,19 +50,22 @@ impl LspAdapter for CssLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -48,19 +48,22 @@ impl LspAdapter for DockerfileLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -53,19 +53,22 @@ impl LspAdapter for ElmLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -50,19 +50,22 @@ impl LspAdapter for HtmlLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -102,19 +102,22 @@ impl LspAdapter for JsonLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -51,18 +51,30 @@ impl LspAdapter for IntelephenseLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<IntelephenseVersion>().unwrap();
let latest_version = latest_version.downcast::<IntelephenseVersion>().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,

View File

@ -48,19 +48,22 @@ impl LspAdapter for PrismaLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -52,19 +52,22 @@ impl LspAdapter for PurescriptLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -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<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -49,19 +49,22 @@ impl LspAdapter for SvelteLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -51,19 +51,22 @@ impl LspAdapter for TailwindLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -71,22 +71,33 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<TypeScriptVersions>().unwrap();
let latest_version = latest_version.downcast::<TypeScriptVersions>().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(),
),
],
)

View File

@ -86,6 +86,7 @@ impl super::LspAdapter for VueLspAdapter {
let version = version.downcast::<VueLspVersion>().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(

View File

@ -52,19 +52,22 @@ impl LspAdapter for YamlLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
let latest_version = latest_version.downcast::<String>().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?;
}

View File

@ -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

View File

@ -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<Value> = 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",