From 79910ba9311b92a8922d6ba45f4ab7c34ab333c6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Feb 2022 16:11:40 -0800 Subject: [PATCH] Show more information in lsp status bar item * Distinguish between checking for updates and downloading * Show dismissable error message when downloading failed and there is no cached server. Co-Authored-By: Nathan Sobo --- Cargo.lock | 16 +++- crates/language/Cargo.toml | 1 + crates/language/src/language.rs | 120 +++++++++++++++++++++++------ crates/workspace/src/lsp_status.rs | 98 +++++++++++++++++++---- crates/zed/src/language.rs | 114 +++++++++++++++------------ crates/zed/src/zed.rs | 2 + 6 files changed, 262 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bf51789b764aeb7380f3487c84dad4c030b176a..c3e6f6f884e677f905c96d0946618faccecb7ed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,17 @@ dependencies = [ "syn", ] +[[package]] +name = "async-broadcast" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" +dependencies = [ + "easy-parallel", + "event-listener", + "futures-core", +] + [[package]] name = "async-channel" version = "1.6.1" @@ -1926,9 +1937,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" @@ -2619,6 +2630,7 @@ name = "language" version = "0.1.0" dependencies = [ "anyhow", + "async-broadcast", "async-trait", "client", "clock", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 5a3637c3bb956982213a0bccfdf6b1f322b405e8..ec90b9c76afc0ee278b2dc059bc105d5ce763d43 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -30,6 +30,7 @@ text = { path = "../text" } theme = { path = "../theme" } util = { path = "../util" } anyhow = "1.0.38" +async-broadcast = "0.3.4" async-trait = "0.1" futures = "0.3" lazy_static = "1.4" diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fd91d2e546c0673a8088a05088125064c6712690..7979850899fa1adab0529f01b508c122fba47a82 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,7 +7,7 @@ pub mod proto; mod tests; use anyhow::{anyhow, Result}; -use client::http::HttpClient; +use client::http::{self, HttpClient}; use collections::HashSet; use futures::{ future::{BoxFuture, Shared}, @@ -17,7 +17,6 @@ use gpui::{AppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; use parking_lot::Mutex; -use postage::watch; use serde::Deserialize; use std::{ cell::RefCell, @@ -59,11 +58,22 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } +pub struct LspBinaryVersion { + pub name: String, + pub url: http::Url, +} + pub trait LspExt: 'static + Send + Sync { - fn fetch_latest_language_server( + fn fetch_latest_server_version( &self, http: Arc, + ) -> BoxFuture<'static, Result>; + fn fetch_server_binary( + &self, + version: LspBinaryVersion, + http: Arc, ) -> BoxFuture<'static, Result>; + fn cached_server_binary(&self) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -118,7 +128,7 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) lsp_ext: Option>, + pub(crate) lsp_ext: Option>, lsp_binary_path: Mutex>>>>>, } @@ -131,19 +141,28 @@ pub struct Grammar { pub(crate) highlight_map: Mutex, } +#[derive(Clone)] +pub enum LanguageServerBinaryStatus { + CheckingForUpdate, + Downloading, + Downloaded, + Cached, + Failed, +} + pub struct LanguageRegistry { languages: Vec>, - pending_lsp_binaries_tx: Arc>>, - pending_lsp_binaries_rx: watch::Receiver, + lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, } impl LanguageRegistry { pub fn new() -> Self { - let (pending_lsp_binaries_tx, pending_lsp_binaries_rx) = watch::channel(); + let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); Self { languages: Default::default(), - pending_lsp_binaries_tx: Arc::new(Mutex::new(pending_lsp_binaries_tx)), - pending_lsp_binaries_rx, + lsp_binary_statuses_tx, + lsp_binary_statuses_rx, } } @@ -211,7 +230,7 @@ impl LanguageRegistry { } } - let lsp_ext = language.lsp_ext.as_ref()?; + let lsp_ext = language.lsp_ext.clone()?; let background = cx.background().clone(); let server_binary_path = { Some( @@ -219,15 +238,13 @@ impl LanguageRegistry { .lsp_binary_path .lock() .get_or_insert_with(|| { - let pending_lsp_binaries_tx = self.pending_lsp_binaries_tx.clone(); - let language_server_path = - lsp_ext.fetch_latest_language_server(http_client); - async move { - *pending_lsp_binaries_tx.lock().borrow_mut() += 1; - let path = language_server_path.map_err(Arc::new).await; - *pending_lsp_binaries_tx.lock().borrow_mut() -= 1; - path - } + get_server_binary_path( + lsp_ext, + language.clone(), + http_client, + self.lsp_binary_statuses_tx.clone(), + ) + .map_err(Arc::new) .boxed() .shared() }) @@ -242,11 +259,68 @@ impl LanguageRegistry { })) } - pub fn pending_lsp_binaries(&self) -> watch::Receiver { - self.pending_lsp_binaries_rx.clone() + pub fn language_server_binary_statuses( + &self, + ) -> async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)> { + self.lsp_binary_statuses_rx.clone() } } +async fn get_server_binary_path( + lsp_ext: Arc, + language: Arc, + http_client: Arc, + statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, +) -> Result { + let path = fetch_latest_server_binary_path( + lsp_ext.clone(), + language.clone(), + http_client, + statuses.clone(), + ) + .await; + if path.is_err() { + if let Some(cached_path) = lsp_ext.cached_server_binary().await { + statuses + .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) + .await?; + return Ok(cached_path); + } else { + statuses + .broadcast((language.clone(), LanguageServerBinaryStatus::Failed)) + .await?; + } + } + path +} + +async fn fetch_latest_server_binary_path( + lsp_ext: Arc, + language: Arc, + http_client: Arc, + lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, +) -> Result { + lsp_binary_statuses_tx + .broadcast(( + language.clone(), + LanguageServerBinaryStatus::CheckingForUpdate, + )) + .await?; + let version_info = lsp_ext + .fetch_latest_server_version(http_client.clone()) + .await?; + lsp_binary_statuses_tx + .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) + .await?; + let path = lsp_ext + .fetch_server_binary(version_info, http_client) + .await?; + lsp_binary_statuses_tx + .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) + .await?; + Ok(path) +} + impl Language { pub fn new(config: LanguageConfig, ts_language: Option) -> Self { Self { @@ -306,8 +380,8 @@ impl Language { Ok(self) } - pub fn with_lsp_ext(mut self, processor: impl LspExt) -> Self { - self.lsp_ext = Some(Box::new(processor)); + pub fn with_lsp_ext(mut self, lsp_ext: impl LspExt) -> Self { + self.lsp_ext = Some(Arc::new(lsp_ext)); self } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index e254a05a931ea255a76b9a3d76b324783b64112b..093f10b1435fa0aa528c4d7cf2add2c0baa34089 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,13 +1,24 @@ use crate::{ItemViewHandle, Settings, StatusItemView}; use futures::StreamExt; -use gpui::{elements::*, Entity, RenderContext, View, ViewContext}; -use language::LanguageRegistry; +use gpui::{ + action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View, + ViewContext, +}; +use language::{LanguageRegistry, LanguageServerBinaryStatus}; use postage::watch; use std::sync::Arc; +action!(DismissErrorMessage); + pub struct LspStatus { - pending_lsp_binaries: usize, settings_rx: watch::Receiver, + checking_for_update: Vec, + downloading: Vec, + failed: Vec, +} + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(LspStatus::dismiss_error_message); } impl LspStatus { @@ -16,12 +27,33 @@ impl LspStatus { settings_rx: watch::Receiver, cx: &mut ViewContext, ) -> Self { - let mut pending_lsp_binaries = languages.pending_lsp_binaries(); + let mut status_events = languages.language_server_binary_statuses(); cx.spawn_weak(|this, mut cx| async move { - while let Some(pending_lsp_binaries) = pending_lsp_binaries.next().await { + while let Some((language, event)) = status_events.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.pending_lsp_binaries = pending_lsp_binaries; + for vector in [ + &mut this.checking_for_update, + &mut this.downloading, + &mut this.failed, + ] { + vector.retain(|name| name != language.name()); + } + + match event { + LanguageServerBinaryStatus::CheckingForUpdate => { + this.checking_for_update.push(language.name().to_string()); + } + LanguageServerBinaryStatus::Downloading => { + this.downloading.push(language.name().to_string()); + } + LanguageServerBinaryStatus::Failed => { + this.failed.push(language.name().to_string()); + } + LanguageServerBinaryStatus::Downloaded + | LanguageServerBinaryStatus::Cached => {} + } + cx.notify(); }); } else { @@ -31,10 +63,17 @@ impl LspStatus { }) .detach(); Self { - pending_lsp_binaries: 0, settings_rx, + checking_for_update: Default::default(), + downloading: Default::default(), + failed: Default::default(), } } + + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + self.failed.clear(); + cx.notify(); + } } impl Entity for LspStatus { @@ -46,16 +85,49 @@ impl View for LspStatus { "LspStatus" } - fn render(&mut self, _: &mut RenderContext) -> ElementBox { - if self.pending_lsp_binaries == 0 { - Empty::new().boxed() - } else { - let theme = &self.settings_rx.borrow().theme; + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings_rx.borrow().theme; + if !self.downloading.is_empty() { + Label::new( + format!( + "Downloading {} language server{}...", + self.downloading.join(", "), + if self.downloading.len() > 1 { "s" } else { "" } + ), + theme.workspace.status_bar.lsp_message.clone(), + ) + .boxed() + } else if !self.checking_for_update.is_empty() { Label::new( - "Downloading language servers...".to_string(), + format!( + "Checking for updates to {} language server{}...", + self.checking_for_update.join(", "), + if self.checking_for_update.len() > 1 { + "s" + } else { + "" + } + ), theme.workspace.status_bar.lsp_message.clone(), ) .boxed() + } else if !self.failed.is_empty() { + MouseEventHandler::new::(0, cx, |_, _| { + Label::new( + format!( + "Failed to download {} language server{}. Click to dismiss.", + self.failed.join(", "), + if self.failed.len() > 1 { "s" } else { "" } + ), + theme.workspace.status_bar.lsp_message.clone(), + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(|cx| cx.dispatch_action(DismissErrorMessage)) + .boxed() + } else { + Empty::new().boxed() } } } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 1203e87309e5ecf66a1afb59f8def33e2855f771..9c8810b7ff161838defd1f15e92ed8297cb137e8 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -9,7 +9,7 @@ use rust_embed::RustEmbed; use serde::Deserialize; use smol::fs::{self, File}; use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; -use util::ResultExt; +use util::{ResultExt, TryFutureExt}; #[derive(RustEmbed)] #[folder = "languages"] @@ -29,12 +29,13 @@ struct GithubReleaseAsset { browser_download_url: http::Url, } -impl RustLsp { - async fn download( - destination_dir_path: PathBuf, +impl LspExt for RustLsp { + fn fetch_latest_server_version( + &self, http: Arc, - ) -> anyhow::Result { - let release = http + ) -> BoxFuture<'static, Result> { + async move { + let release = http .send( surf::RequestBuilder::new( Method::Get, @@ -51,40 +52,23 @@ impl RustLsp { .body_json::() .await .map_err(|err| anyhow!("error parsing 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_err() { - let response = http - .send( - surf::RequestBuilder::new(Method::Get, asset.browser_download_url.clone()) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .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?; + 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 release found matching {:?}", asset_name))?; + Ok(LspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }) } - - Ok::<_, anyhow::Error>(destination_path) + .boxed() } -} -impl LspExt for RustLsp { - fn fetch_latest_language_server( + fn fetch_server_binary( &self, + version: LspBinaryVersion, http: Arc, ) -> BoxFuture<'static, Result> { async move { @@ -92,31 +76,59 @@ impl LspExt for RustLsp { .ok_or_else(|| anyhow!("can't determine home directory"))? .join(".zed/rust-analyzer"); fs::create_dir_all(&destination_dir_path).await?; + let destination_path = + destination_dir_path.join(format!("rust-analyzer-{}", version.name)); + + if fs::metadata(&destination_path).await.is_err() { + let response = http + .send( + surf::RequestBuilder::new(Method::Get, version.url) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .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?; - let downloaded_bin_path = Self::download(destination_dir_path.clone(), http).await; - let mut last_cached_bin_path = None; - 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 Ok(downloaded_bin_path) = downloaded_bin_path.as_ref() { - if downloaded_bin_path != entry_path.as_path() { + 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 entry_path.as_path() != destination_path { fs::remove_file(&entry_path).await.log_err(); } } - last_cached_bin_path = Some(entry_path); } } } - if downloaded_bin_path.is_err() { - if let Some(last_cached_bin_path) = last_cached_bin_path { - return Ok(last_cached_bin_path); - } - } + Ok(destination_path) + } + .boxed() + } - downloaded_bin_path + fn cached_server_binary(&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 last = None; + let mut entries = fs::read_dir(&destination_dir_path).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + last.ok_or_else(|| anyhow!("no cached binary")) } + .log_err() .boxed() } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7178ff44eb7e3dae0b0df21b792d7ea6f16a66bd..cdc24e5a52e5d74381d28fd3e10e5d547ea41d12 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -43,6 +43,8 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { } }); + workspace::lsp_status::init(cx); + cx.add_bindings(vec![ Binding::new("cmd-=", AdjustBufferFontSize(1.), None), Binding::new("cmd--", AdjustBufferFontSize(-1.), None),