Switch LSP prompts to use a non-blocking toast (#8312)

Mikayla Maki and Marshall created

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

<img width="583" alt="Screenshot 2024-02-23 at 2 40 05 PM"
src="https://github.com/zed-industries/zed/assets/2280405/1bfc027b-b7a8-4563-88b6-020e47869668">

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

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(-)

Detailed changes

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%"),

crates/project/src/project.rs 🔗

@@ -229,6 +229,7 @@ pub struct LanguageServerPromptRequest {
     pub level: PromptLevel,
     pub message: String,
     pub actions: Vec<MessageActionItem>,
+    pub lsp_name: String,
     response_channel: Sender<MessageActionItem>,
 }
 
@@ -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::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
@@ -3160,8 +3162,10 @@ impl Project {
         language_server
             .on_request::<lsp::request::ShowMessageRequest, _, _>({
                 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)) => {

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(),

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`.

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.),

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<project::LanguageServerPromptRequest>,
+}
+
+impl LanguageServerPrompt {
+    pub fn new(request: project::LanguageServerPromptRequest) -> Self {
+        Self {
+            request: Some(request),
+        }
+    }
+
+    async fn select_option(this: View<Self>, 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<Self>) -> 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<DismissEvent> for LanguageServerPrompt {}
+
 pub mod simple_message_notification {
     use gpui::{
         div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,

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::<Vec<_>>();
-                        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()