From 793d9e8bbac3e91dbe22c9ea149c3e340c78e1f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 16:07:09 +0100 Subject: [PATCH] Download rust-analyzer from GitHub --- Cargo.lock | 1 + crates/language/src/language.rs | 62 +++++++++------- crates/zed/Cargo.toml | 1 + crates/zed/languages/rust/config.toml | 1 - crates/zed/src/language.rs | 100 ++++++++++++++++++++++---- 5 files changed, 127 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bc19c7ad9..3fec56a450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5741,6 +5741,7 @@ name = "zed" version = "0.15.2" dependencies = [ "anyhow", + "async-compression", "async-recursion", "async-trait", "chat_panel", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 39aa74e943..624ae29b86 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -8,7 +8,10 @@ mod tests; use anyhow::{anyhow, Result}; use collections::HashSet; -use futures::future::BoxFuture; +use futures::{ + future::{BoxFuture, Shared}, + FutureExt, +}; use gpui::{AppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; @@ -55,8 +58,8 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub trait LspPostProcessor: 'static + Send + Sync { - fn download_language_server(&self) -> BoxFuture<'static, Result>; +pub trait LspExt: 'static + Send + Sync { + fn server_bin_path(&self) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -86,7 +89,6 @@ pub struct LanguageConfig { #[derive(Default, Deserialize)] pub struct LanguageServerConfig { - pub binary: String, pub disk_based_diagnostic_sources: HashSet, pub disk_based_diagnostics_progress_token: Option, #[cfg(any(test, feature = "test-support"))] @@ -112,7 +114,8 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) lsp_post_processor: Option>, + pub(crate) lsp_ext: Option>, + lsp_binary_path: Mutex>>>>, } pub struct Grammar { @@ -179,7 +182,8 @@ impl Language { highlight_map: Default::default(), }) }), - lsp_post_processor: None, + lsp_ext: None, + lsp_binary_path: Default::default(), } } @@ -223,8 +227,8 @@ impl Language { Ok(self) } - pub fn with_lsp_post_processor(mut self, processor: impl LspPostProcessor) -> Self { - self.lsp_post_processor = Some(Box::new(processor)); + pub fn with_lsp_ext(mut self, processor: impl LspExt) -> Self { + self.lsp_ext = Some(Box::new(processor)); self } @@ -241,8 +245,8 @@ impl Language { root_path: Arc, cx: &AppContext, ) -> Task>>> { + #[cfg(any(test, feature = "test-support"))] if let Some(config) = &self.config.language_server { - #[cfg(any(test, feature = "test-support"))] if let Some(fake_config) = &config.fake_config { use postage::prelude::Stream; @@ -266,19 +270,20 @@ impl Language { return Task::ready(Ok(Some(server.clone()))); } + } - let background = cx.background().clone(); - let server_binary_path = self - .download_language_server() - .ok_or_else(|| anyhow!("cannot download language server")); - cx.background().spawn(async move { - let server_binary_path = server_binary_path?.await?; + let background = cx.background().clone(); + let server_binary_path = self + .lsp_binary_path() + .ok_or_else(|| anyhow!("cannot locate or download language server")); + cx.background().spawn(async move { + if let Some(server_binary_path) = server_binary_path?.await { let server = lsp::LanguageServer::new(&server_binary_path, &root_path, background)?; Ok(Some(server)) - }) - } else { - Task::ready(Ok(None)) - } + } else { + Ok(None) + } + }) } pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { @@ -296,7 +301,7 @@ impl Language { } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { - if let Some(processor) = self.lsp_post_processor.as_ref() { + if let Some(processor) = self.lsp_ext.as_ref() { processor.process_diagnostics(diagnostics); } } @@ -305,7 +310,7 @@ impl Language { &self, completion: &lsp::CompletionItem, ) -> Option { - self.lsp_post_processor + self.lsp_ext .as_ref()? .label_for_completion(completion, self) } @@ -331,10 +336,17 @@ impl Language { result } - fn download_language_server(&self) -> Option>> { - self.lsp_post_processor - .as_ref() - .map(|processor| processor.download_language_server()) + fn lsp_binary_path(&self) -> Option>> { + if let Some(lsp_ext) = self.lsp_ext.as_ref() { + Some( + self.lsp_binary_path + .lock() + .get_or_insert_with(|| lsp_ext.server_bin_path().shared()) + .clone(), + ) + } else { + None + } } pub fn brackets(&self) -> &[BracketPair] { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3f4cf8a96f..cd581cb90d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -55,6 +55,7 @@ theme_selector = { path = "../theme_selector" } util = { path = "../util" } workspace = { path = "../workspace" } anyhow = "1.0.38" +async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-recursion = "0.3" async-trait = "0.1" crossbeam-channel = "0.5.0" diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/languages/rust/config.toml index 426dcc2b48..e4bf50a929 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/languages/rust/config.toml @@ -11,6 +11,5 @@ brackets = [ ] [language_server] -binary = "rust-analyzer" disk_based_diagnostic_sources = ["rustc"] disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check" diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index e8b88b6a31..93cc777bb7 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -1,22 +1,98 @@ -use anyhow::Result; -use futures::future::BoxFuture; +use anyhow::anyhow; +use async_compression::futures::bufread::GzipDecoder; +use client::http; +use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use lazy_static::lazy_static; use regex::Regex; use rust_embed::RustEmbed; -use std::borrow::Cow; -use std::path::PathBuf; -use std::{str, sync::Arc}; +use serde::Deserialize; +use smol::fs::{self, File}; +use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use util::{ResultExt, TryFutureExt}; #[derive(RustEmbed)] #[folder = "languages"] struct LanguageDir; -struct RustPostProcessor; +struct RustLsp; -impl LspPostProcessor for RustPostProcessor { - fn download_language_server(&self) -> BoxFuture<'static, Result> { - todo!() +#[derive(Deserialize)] +struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +impl RustLsp { + async fn download(destination_dir_path: PathBuf) -> anyhow::Result { + let client = surf::client().with(surf::middleware::Redirect::default()); + let release = client + .get("https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest") + .recv_json::() + .await + .map_err(|err| anyhow!("error getting latest release: {}", err))?; + let release_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); + let asset = release + .assets + .iter() + .find(|asset| asset.name == release_name) + .ok_or_else(|| anyhow!("no release found matching {:?}", release_name))?; + + let destination_path = destination_dir_path.join(format!("rust-analyzer-{}", release.name)); + if fs::metadata(&destination_path).await.is_ok() { + let response = client + .get(&asset.browser_download_url) + .send() + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(response); + let mut file = File::create(&destination_path).await?; + futures::io::copy(decompressed_bytes, &mut file).await?; + fs::set_permissions( + &destination_path, + ::from_mode(0o755), + ) + .await?; + } + + Ok::<_, anyhow::Error>(destination_path) + } +} + +impl LspExt for RustLsp { + fn server_bin_path(&self) -> BoxFuture<'static, Option> { + async move { + let destination_dir_path = dirs::home_dir() + .ok_or_else(|| anyhow!("can't determine home directory"))? + .join(".zed/rust-analyzer"); + fs::create_dir_all(&destination_dir_path).await?; + + let mut server_bin_path = Self::download(destination_dir_path.clone()).await.log_err(); + if let Some(mut entries) = fs::read_dir(&destination_dir_path).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if let Some(downloaded_server_path) = server_bin_path.as_ref() { + if downloaded_server_path != entry_path.as_path() { + fs::remove_file(entry_path).await.log_err(); + } + } else { + server_bin_path = Some(entry_path); + } + } + } + } + + server_bin_path.ok_or_else(|| anyhow!("could not locate or download server")) + } + .log_err() + .boxed() } fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -138,7 +214,7 @@ fn rust() -> Language { .unwrap() .with_outline_query(load_query("rust/outline.scm").as_ref()) .unwrap() - .with_lsp_post_processor(RustPostProcessor) + .with_lsp_ext(RustLsp) } fn markdown() -> Language { @@ -160,7 +236,7 @@ fn load_query(path: &str) -> Cow<'static, str> { mod tests { use super::*; use gpui::color::Color; - use language::LspPostProcessor; + use language::LspExt; use theme::SyntaxTheme; #[test] @@ -187,7 +263,7 @@ mod tests { }, ], }; - RustPostProcessor.process_diagnostics(&mut params); + RustLsp.process_diagnostics(&mut params); assert_eq!(params.diagnostics[0].message, "use of moved value `a`");