From 1bf85214a4579342dca1cdfb002a76053525f764 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 28 Apr 2023 16:42:36 -0400 Subject: [PATCH] Source ESLint server from Github rather than 3rd party NPM package --- crates/copilot/src/copilot.rs | 2 +- crates/node_runtime/src/node_runtime.rs | 46 ++++++++++--- crates/util/src/github.rs | 32 ++++++--- crates/zed/src/languages/c.rs | 2 +- crates/zed/src/languages/elixir.rs | 2 +- crates/zed/src/languages/go.rs | 2 +- crates/zed/src/languages/html.rs | 2 +- crates/zed/src/languages/json.rs | 2 +- crates/zed/src/languages/lua.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- crates/zed/src/languages/rust.rs | 8 +-- crates/zed/src/languages/typescript.rs | 90 +++++++++++++++---------- crates/zed/src/languages/yaml.rs | 2 +- 13 files changed, 128 insertions(+), 66 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 6bc3622ab170f35547a920c702a3ab8ae349548f..f7ff163424ecde9dfff5ff4709d0a69616c44099 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -932,7 +932,7 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { ///Check for the latest copilot language server and download it if we haven't already async fn fetch_latest(http: Arc) -> anyhow::Result { - let release = latest_github_release("zed-industries/copilot", http.clone()).await?; + let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?; let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name)); diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 079b6a5e45a97b920925777b5d6be96d1bca49e0..e2a8d0d0032e23ea6751855acce33d90bded46d8 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -5,7 +5,7 @@ use futures::{future::Shared, FutureExt}; use gpui::{executor::Background, Task}; use parking_lot::Mutex; use serde::Deserialize; -use smol::{fs, io::BufReader}; +use smol::{fs, io::BufReader, process::Command}; use std::{ env::consts, path::{Path, PathBuf}, @@ -48,12 +48,41 @@ impl NodeRuntime { Ok(installation_path.join("bin/node")) } + pub async fn run_npm_subcommand( + &self, + directory: &Path, + subcommand: &str, + args: &[&str], + ) -> 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 = Command::new(node_binary) + .arg(npm_file) + .arg(subcommand) + .args(args) + .current_dir(directory) + .output() + .await?; + + if !output.status.success() { + return Err(anyhow!( + "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + Ok(()) + } + pub async fn npm_package_latest_version(&self, name: &str) -> 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) + let output = Command::new(node_binary) .arg(npm_file) .args(["-fetch-retry-mintimeout", "2000"]) .args(["-fetch-retry-maxtimeout", "5000"]) @@ -64,11 +93,11 @@ impl NodeRuntime { .context("failed to run npm info")?; if !output.status.success() { - Err(anyhow!( + return 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)?; @@ -80,14 +109,14 @@ impl NodeRuntime { pub async fn npm_install_packages( &self, - packages: impl IntoIterator, directory: &Path, + packages: impl IntoIterator, ) -> 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) + let output = Command::new(node_binary) .arg(npm_file) .args(["-fetch-retry-mintimeout", "2000"]) .args(["-fetch-retry-maxtimeout", "5000"]) @@ -103,12 +132,13 @@ impl NodeRuntime { .output() .await .context("failed to run npm install")?; + if !output.status.success() { - Err(anyhow!( + return Err(anyhow!( "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) - ))?; + )); } Ok(()) } diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index 3bb4baa2937536f6c77d407ec22bea1c0320829f..b1e981ae4963e19829a91cc3cd961d604c9d1643 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -1,5 +1,5 @@ use crate::http::HttpClient; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use futures::AsyncReadExt; use serde::Deserialize; use std::sync::Arc; @@ -12,7 +12,10 @@ pub struct GitHubLspBinaryVersion { #[derive(Deserialize, Debug)] pub struct GithubRelease { pub name: String, + #[serde(rename = "prerelease")] + pub pre_release: bool, pub assets: Vec, + pub tarball_url: String, } #[derive(Deserialize, Debug)] @@ -23,16 +26,18 @@ pub struct GithubReleaseAsset { pub async fn latest_github_release( repo_name_with_owner: &str, + pre_release: bool, http: Arc, ) -> Result { let mut response = http .get( - &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"), + &format!("https://api.github.com/repos/{repo_name_with_owner}/releases"), Default::default(), true, ) .await .context("error fetching latest release")?; + let mut body = Vec::new(); response .body_mut() @@ -40,13 +45,20 @@ pub async fn latest_github_release( .await .context("error reading latest release")?; - let release = serde_json::from_slice::(body.as_slice()); - if release.is_err() { - log::error!( - "Github API response text: {:?}", - String::from_utf8_lossy(body.as_slice()) - ); - } + let releases = match serde_json::from_slice::>(body.as_slice()) { + Ok(releases) => releases, + + Err(_) => { + log::error!( + "Error deserializing Github API response text: {:?}", + String::from_utf8_lossy(body.as_slice()) + ); + return Err(anyhow!("error deserializing latest release")); + } + }; - release.context("error deserializing latest release") + releases + .into_iter() + .find(|release| release.pre_release == pre_release) + .ok_or(anyhow!("Failed to find a release")) } diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index e142028196deb8c5af3a19f32d5e5b3c1716c9af..84c5798b07d53bbcbe9dbb930cedb6f80db2c4ad 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -23,7 +23,7 @@ impl super::LspAdapter for CLspAdapter { &self, http: Arc, ) -> Result> { - let release = latest_github_release("clangd/clangd", http).await?; + let release = latest_github_release("clangd/clangd", false, http).await?; let asset_name = format!("clangd-mac-{}.zip", release.name); let asset = release .assets diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index a2debcdb2d5a3f6c08a476dd6a05bd36e9fe8315..2939a0fa5f942b9631b2ecd9e13d6c1e5c4de17e 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -24,7 +24,7 @@ impl LspAdapter for ElixirLspAdapter { &self, http: Arc, ) -> Result> { - let release = latest_github_release("elixir-lsp/elixir-ls", http).await?; + let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?; let asset_name = "elixir-ls.zip"; let asset = release .assets diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 760c5f353d0d98c6f58d85d5472a8c519a2c37da..ed24abb45c40b6b2cc2c2336a746557c03505745 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -33,7 +33,7 @@ impl super::LspAdapter for GoLspAdapter { &self, http: Arc, ) -> Result> { - let release = latest_github_release("golang/tools", http).await?; + let release = latest_github_release("golang/tools", false, http).await?; let version: Option = release.name.strip_prefix("gopls/v").map(str::to_string); if version.is_none() { log::warn!( diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index be5493b4cb8f8037375f9282f0c1ae5bd03240e6..68f780c3af73b3febd1fdcbeaba1a2a95bb36b37 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -57,8 +57,8 @@ impl LspAdapter for HtmlLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node .npm_install_packages( - [("vscode-langservers-extracted", version.as_str())], &container_dir, + [("vscode-langservers-extracted", version.as_str())], ) .await?; } diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 5c3edfba25538f02f597d4e66d6d78c1c7c548a4..d87d36abfef7139033d8f9526e877d2ba4efb826 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -76,8 +76,8 @@ impl LspAdapter for JsonLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node .npm_install_packages( - [("vscode-json-languageserver", version.as_str())], &container_dir, + [("vscode-json-languageserver", version.as_str())], ) .await?; } diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 2a18138cb71193c9f07e2c02f3335959e85da123..f204eb2555f5327d0e70df60963db771acd772ab 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -30,7 +30,7 @@ impl super::LspAdapter for LuaLspAdapter { &self, http: Arc, ) -> Result> { - let release = latest_github_release("LuaLS/lua-language-server", http).await?; + let release = latest_github_release("LuaLS/lua-language-server", false, http).await?; let version = release.name.clone(); let platform = match consts::ARCH { "x86_64" => "x64", diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 08476c9c216277d7b80c20fabe5ac72795008df8..acd31e82059b0d04aa476340e30e87b4c1867a9d 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -53,7 +53,7 @@ impl LspAdapter for PythonLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node - .npm_install_packages([("pyright", version.as_str())], &container_dir) + .npm_install_packages(&container_dir, [("pyright", version.as_str())]) .await?; } diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3808444ad9073da0f30b493d1b88262b19492cf8..92fb5bc3b2739cf8a63a2d3717c23e7642e50963 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -24,18 +24,17 @@ impl LspAdapter for RustLspAdapter { &self, http: Arc, ) -> Result> { - let release = latest_github_release("rust-analyzer/rust-analyzer", http).await?; + let release = latest_github_release("rust-analyzer/rust-analyzer", false, http).await?; let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); let asset = release .assets .iter() .find(|asset| asset.name == asset_name) .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; - let version = GitHubLspBinaryVersion { + Ok(Box::new(GitHubLspBinaryVersion { name: release.name, url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) + })) } async fn fetch_server_binary( @@ -77,6 +76,7 @@ impl LspAdapter for RustLspAdapter { while let Some(entry) = entries.next().await { last = Some(entry?.path()); } + anyhow::Ok(LanguageServerBinary { path: last.ok_or_else(|| anyhow!("no cached binary"))?, arguments: Default::default(), diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index e4a540dcd8efc1fcc315852aa3bed47a87f1965e..54d61e91cae5975198c34fedb93e8551ce0ddc08 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,4 +1,6 @@ use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt}; use gpui::AppContext; @@ -6,7 +8,7 @@ use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use lsp::CodeActionKind; use node_runtime::NodeRuntime; use serde_json::{json, Value}; -use smol::fs; +use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ any::Any, ffi::OsString, @@ -14,8 +16,8 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::http::HttpClient; -use util::ResultExt; +use util::{fs::remove_matching, github::latest_github_release, http::HttpClient}; +use util::{github::GitHubLspBinaryVersion, ResultExt}; fn typescript_server_binary_arguments(server_path: &Path) -> Vec { vec![ @@ -69,24 +71,24 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_server_binary( &self, - versions: Box, + version: Box, _: Arc, container_dir: PathBuf, ) -> Result { - let versions = versions.downcast::().unwrap(); + let version = version.downcast::().unwrap(); let server_path = container_dir.join(Self::NEW_SERVER_PATH); if fs::metadata(&server_path).await.is_err() { self.node .npm_install_packages( + &container_dir, [ - ("typescript", versions.typescript_version.as_str()), + ("typescript", version.typescript_version.as_str()), ( "typescript-language-server", - versions.server_version.as_str(), + version.server_version.as_str(), ), ], - &container_dir, ) .await?; } @@ -172,8 +174,7 @@ pub struct EsLintLspAdapter { } impl EsLintLspAdapter { - const SERVER_PATH: &'static str = - "node_modules/vscode-langservers-extracted/lib/eslint-language-server/eslintServer.js"; + const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js"; #[allow(unused)] pub fn new(node: Arc) -> Self { @@ -228,30 +229,50 @@ impl LspAdapter for EsLintLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + http: Arc, ) -> Result> { - Ok(Box::new( - self.node - .npm_package_latest_version("vscode-langservers-extracted") - .await?, - )) + // At the time of writing the latest vscode-eslint release was released in 2020 and requires + // special custom LSP protocol extensions be handled to fully initalize. Download the latest + // prerelease instead to sidestep this issue + let release = latest_github_release("microsoft/vscode-eslint", true, http).await?; + Ok(Box::new(GitHubLspBinaryVersion { + name: release.name, + url: release.tarball_url, + })) } async fn fetch_server_binary( &self, - versions: Box, - _: Arc, + version: Box, + http: Arc, container_dir: PathBuf, ) -> Result { - let version = versions.downcast::().unwrap(); - let server_path = container_dir.join(Self::SERVER_PATH); + let version = version.downcast::().unwrap(); + let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name)); + let server_path = destination_path.join(Self::SERVER_PATH); if fs::metadata(&server_path).await.is_err() { + remove_matching(&container_dir, |entry| entry != destination_path).await; + + let mut response = http + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&destination_path).await?; + + let mut dir = fs::read_dir(&destination_path).await?; + let first = dir.next().await.ok_or(anyhow!("missing first file"))??; + let repo_root = destination_path.join("vscode-eslint"); + fs::rename(first.path(), &repo_root).await?; + self.node - .npm_install_packages( - [("vscode-langservers-extracted", version.as_str())], - &container_dir, - ) + .run_npm_subcommand(&repo_root, "install", &[]) + .await?; + + self.node + .run_npm_subcommand(&repo_root, "run-script", &["compile"]) .await?; } @@ -263,18 +284,17 @@ impl LspAdapter for EsLintLspAdapter { async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { (|| async move { - let server_path = container_dir.join(Self::SERVER_PATH); - if server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: eslint_server_binary_arguments(&server_path), - }) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - container_dir - )) + // This is unfortunate but we don't know what the version is to build a path directly + let mut dir = fs::read_dir(&container_dir).await?; + let first = dir.next().await.ok_or(anyhow!("missing first file"))??; + if !first.file_type().await?.is_dir() { + return Err(anyhow!("First entry is not a directory")); } + + Ok(LanguageServerBinary { + path: first.path().join(Self::SERVER_PATH), + arguments: Default::default(), + }) })() .await .log_err() diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index fadc74b698279d0b6320bf35c4abd28b666ea7c1..fed76cd5b9e399bdfed5a4756617395eb746852b 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -61,7 +61,7 @@ impl LspAdapter for YamlLspAdapter { if fs::metadata(&server_path).await.is_err() { self.node - .npm_install_packages([("yaml-language-server", version.as_str())], &container_dir) + .npm_install_packages(&container_dir, [("yaml-language-server", version.as_str())]) .await?; }