From cab8b5a9a39b827cf65c5c49112ba1f90f19651d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 23 Feb 2024 15:18:32 -0800 Subject: [PATCH] Switch LSP prompts to use a non-blocking toast (#8312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a major degradation in usability that some users ran into. Fixes https://github.com/zed-industries/zed/issues/8255 Fixes https://github.com/zed-industries/zed/issues/8229 Release Notes: - Switch from using platform prompts to toasts for LSP prompts. ([8255](https://github.com/zed-industries/zed/issues/8255), [8229](https://github.com/zed-industries/zed/issues/8229)) Screenshot 2024-02-23 at 2 40 05 PM Co-authored-by: Marshall --- crates/gpui_macros/src/style_helpers.rs | 2 + crates/project/src/project.rs | 6 ++ crates/ui/src/components/label/label_like.rs | 2 + crates/ui/src/styled_ext.rs | 11 ++ crates/ui/src/styles/typography.rs | 8 ++ crates/workspace/src/notifications.rs | 107 ++++++++++++++++++- crates/workspace/src/workspace.rs | 30 ++---- 7 files changed, 145 insertions(+), 21 deletions(-) diff --git a/crates/gpui_macros/src/style_helpers.rs b/crates/gpui_macros/src/style_helpers.rs index 00d3672033..6dc5d83ad3 100644 --- a/crates/gpui_macros/src/style_helpers.rs +++ b/crates/gpui_macros/src/style_helpers.rs @@ -399,6 +399,8 @@ fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> { ("72", quote! { rems(18.) }, "288px (18rem)"), ("80", quote! { rems(20.) }, "320px (20rem)"), ("96", quote! { rems(24.) }, "384px (24rem)"), + ("112", quote! { rems(28.) }, "448px (28rem)"), + ("128", quote! { rems(32.) }, "512px (32rem)"), ("auto", quote! { auto() }, "Auto"), ("px", quote! { px(1.) }, "1px"), ("full", quote! { relative(1.) }, "100%"), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8446cf0682..5fb0ede9b5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -229,6 +229,7 @@ pub struct LanguageServerPromptRequest { pub level: PromptLevel, pub message: String, pub actions: Vec, + pub lsp_name: String, response_channel: Sender, } @@ -3022,6 +3023,7 @@ impl Project { cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?; let language_server = pending_server.task.await?; + let name = language_server.name(); language_server .on_notification::({ let adapter = adapter.clone(); @@ -3160,8 +3162,10 @@ impl Project { language_server .on_request::({ let this = this.clone(); + let name = name.to_string(); move |params, mut cx| { let this = this.clone(); + let name = name.to_string(); async move { if let Some(actions) = params.actions { let (tx, mut rx) = smol::channel::bounded(1); @@ -3174,6 +3178,7 @@ impl Project { message: params.message, actions, response_channel: tx, + lsp_name: name.clone(), }; if let Ok(_) = this.update(&mut cx, |_, cx| { @@ -3211,6 +3216,7 @@ impl Project { } }) .detach(); + let mut initialization_options = adapter.adapter.initialization_options(); match (&mut initialization_options, override_options) { (Some(initialization_options), Some(override_options)) => { diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index cddd849b89..04fb193660 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -7,6 +7,7 @@ use crate::prelude::*; pub enum LabelSize { #[default] Default, + Large, Small, XSmall, } @@ -97,6 +98,7 @@ impl RenderOnce for LabelLike { ) }) .map(|this| match self.size { + LabelSize::Large => this.text_ui_lg(), LabelSize::Default => this.text_ui(), LabelSize::Small => this.text_ui_sm(), LabelSize::XSmall => this.text_ui_xs(), diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index b2eaf75ed9..2b4cc2b395 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -35,6 +35,17 @@ pub trait StyledExt: Styled + Sized { self.text_size(size.rems()) } + /// The large size for UI text. + /// + /// `1rem` or `16px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + /// + /// Use `text_ui` for regular-sized text. + fn text_ui_lg(self) -> Self { + self.text_size(UiTextSize::Large.rems()) + } + /// The default size for UI text. /// /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 70cd797d51..1931672af7 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -13,6 +13,13 @@ pub enum UiTextSize { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. #[default] Default, + /// The large size for UI text. + /// + /// `1rem` or `16px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + Large, + /// The small size for UI text. /// /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. @@ -31,6 +38,7 @@ pub enum UiTextSize { impl UiTextSize { pub fn rems(self) -> Rems { match self { + Self::Large => rems(16. / 16.), Self::Default => rems(14. / 16.), Self::Small => rems(12. / 16.), Self::XSmall => rems(10. / 16.), diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 33d5834ae7..94fb597c83 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,10 +1,14 @@ use crate::{Toast, Workspace}; use collections::HashMap; use gpui::{ - AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Global, - PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext, + svg, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, + Global, PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext, }; +use language::DiagnosticSeverity; + use std::{any::TypeId, ops::DerefMut}; +use ui::prelude::*; +use util::ResultExt; pub fn init(cx: &mut AppContext) { cx.set_global(NotificationTracker::new()); @@ -168,6 +172,105 @@ impl Workspace { } } +pub struct LanguageServerPrompt { + request: Option, +} + +impl LanguageServerPrompt { + pub fn new(request: project::LanguageServerPromptRequest) -> Self { + Self { + request: Some(request), + } + } + + async fn select_option(this: View, ix: usize, mut cx: AsyncWindowContext) { + util::async_maybe!({ + let potential_future = this.update(&mut cx, |this, _| { + this.request.take().map(|request| request.respond(ix)) + }); + + potential_future? // App Closed + .ok_or_else(|| anyhow::anyhow!("Response already sent"))? + .await + .ok_or_else(|| anyhow::anyhow!("Stream already closed"))?; + + this.update(&mut cx, |_, cx| cx.emit(DismissEvent))?; + + anyhow::Ok(()) + }) + .await + .log_err(); + } +} + +impl Render for LanguageServerPrompt { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(request) = &self.request else { + return div().id("language_server_prompt_notification"); + }; + + h_flex() + .id("language_server_prompt_notification") + .elevation_3(cx) + .items_start() + .p_2() + .gap_2() + .w_full() + .child( + v_flex() + .overflow_hidden() + .child( + h_flex() + .children( + match request.level { + PromptLevel::Info => None, + PromptLevel::Warning => Some(DiagnosticSeverity::WARNING), + PromptLevel::Critical => Some(DiagnosticSeverity::ERROR), + } + .map(|severity| { + svg() + .size(cx.text_style().font_size) + .flex_none() + .mr_1() + .map(|icon| { + if severity == DiagnosticSeverity::ERROR { + icon.path(IconName::ExclamationTriangle.path()) + .text_color(Color::Error.color(cx)) + } else { + icon.path(IconName::ExclamationTriangle.path()) + .text_color(Color::Warning.color(cx)) + } + }) + }), + ) + .child( + Label::new(format!("{}:", request.lsp_name)) + .size(LabelSize::Default), + ), + ) + .child(Label::new(request.message.to_string())) + .children(request.actions.iter().enumerate().map(|(ix, action)| { + let this_handle = cx.view().clone(); + ui::Button::new(ix, action.title.clone()) + .size(ButtonSize::Large) + .on_click(move |_, cx| { + let this_handle = this_handle.clone(); + cx.spawn(|cx| async move { + LanguageServerPrompt::select_option(this_handle, ix, cx).await + }) + .detach() + }) + })), + ) + .child( + ui::IconButton::new("close", ui::IconName::Close) + .on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))), + ) + } +} + +impl EventEmitter for LanguageServerPrompt {} + pub mod simple_message_notification { use gpui::{ div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2df22dcea1..3a0d90118c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -59,7 +59,10 @@ use std::{ any::TypeId, borrow::Cow, cell::RefCell, - cmp, env, + cmp, + collections::hash_map::DefaultHasher, + env, + hash::{Hash, Hasher}, path::{Path, PathBuf}, rc::Rc, sync::{atomic::AtomicUsize, Arc, Weak}, @@ -579,24 +582,13 @@ impl Workspace { }), project::Event::LanguageServerPrompt(request) => { - let request = request.clone(); + let mut hasher = DefaultHasher::new(); + request.message.as_str().hash(&mut hasher); + let id = hasher.finish(); - cx.spawn(|_, mut cx| async move { - let messages = request - .actions - .iter() - .map(|action| action.title.as_str()) - .collect::>(); - let index = cx - .update(|cx| { - cx.prompt(request.level, "", Some(&request.message), &messages) - })? - .await?; - request.respond(index).await; - - Result::<(), anyhow::Error>::Ok(()) - }) - .detach() + this.show_notification(id as usize, cx, |cx| { + cx.new_view(|_| notifications::LanguageServerPrompt::new(request.clone())) + }); } _ => {} @@ -2766,7 +2758,7 @@ impl Workspace { .z_index(100) .right_3() .bottom_3() - .w_96() + .w_112() .h_full() .flex() .flex_col()