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

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>
This commit is contained in:
Mikayla Maki 2024-02-23 15:18:32 -08:00 committed by GitHub
parent d993dd3b2c
commit cab8b5a9a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 145 additions and 21 deletions

View File

@ -399,6 +399,8 @@ fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
("72", quote! { rems(18.) }, "288px (18rem)"), ("72", quote! { rems(18.) }, "288px (18rem)"),
("80", quote! { rems(20.) }, "320px (20rem)"), ("80", quote! { rems(20.) }, "320px (20rem)"),
("96", quote! { rems(24.) }, "384px (24rem)"), ("96", quote! { rems(24.) }, "384px (24rem)"),
("112", quote! { rems(28.) }, "448px (28rem)"),
("128", quote! { rems(32.) }, "512px (32rem)"),
("auto", quote! { auto() }, "Auto"), ("auto", quote! { auto() }, "Auto"),
("px", quote! { px(1.) }, "1px"), ("px", quote! { px(1.) }, "1px"),
("full", quote! { relative(1.) }, "100%"), ("full", quote! { relative(1.) }, "100%"),

View File

@ -229,6 +229,7 @@ pub struct LanguageServerPromptRequest {
pub level: PromptLevel, pub level: PromptLevel,
pub message: String, pub message: String,
pub actions: Vec<MessageActionItem>, pub actions: Vec<MessageActionItem>,
pub lsp_name: String,
response_channel: Sender<MessageActionItem>, response_channel: Sender<MessageActionItem>,
} }
@ -3022,6 +3023,7 @@ impl Project {
cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?; cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?;
let language_server = pending_server.task.await?; let language_server = pending_server.task.await?;
let name = language_server.name();
language_server language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({ .on_notification::<lsp::notification::PublishDiagnostics, _>({
let adapter = adapter.clone(); let adapter = adapter.clone();
@ -3160,8 +3162,10 @@ impl Project {
language_server language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({ .on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = this.clone(); let this = this.clone();
let name = name.to_string();
move |params, mut cx| { move |params, mut cx| {
let this = this.clone(); let this = this.clone();
let name = name.to_string();
async move { async move {
if let Some(actions) = params.actions { if let Some(actions) = params.actions {
let (tx, mut rx) = smol::channel::bounded(1); let (tx, mut rx) = smol::channel::bounded(1);
@ -3174,6 +3178,7 @@ impl Project {
message: params.message, message: params.message,
actions, actions,
response_channel: tx, response_channel: tx,
lsp_name: name.clone(),
}; };
if let Ok(_) = this.update(&mut cx, |_, cx| { if let Ok(_) = this.update(&mut cx, |_, cx| {
@ -3211,6 +3216,7 @@ impl Project {
} }
}) })
.detach(); .detach();
let mut initialization_options = adapter.adapter.initialization_options(); let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) { match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => { (Some(initialization_options), Some(override_options)) => {

View File

@ -7,6 +7,7 @@ use crate::prelude::*;
pub enum LabelSize { pub enum LabelSize {
#[default] #[default]
Default, Default,
Large,
Small, Small,
XSmall, XSmall,
} }
@ -97,6 +98,7 @@ impl RenderOnce for LabelLike {
) )
}) })
.map(|this| match self.size { .map(|this| match self.size {
LabelSize::Large => this.text_ui_lg(),
LabelSize::Default => this.text_ui(), LabelSize::Default => this.text_ui(),
LabelSize::Small => this.text_ui_sm(), LabelSize::Small => this.text_ui_sm(),
LabelSize::XSmall => this.text_ui_xs(), LabelSize::XSmall => this.text_ui_xs(),

View File

@ -35,6 +35,17 @@ pub trait StyledExt: Styled + Sized {
self.text_size(size.rems()) 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. /// The default size for UI text.
/// ///
/// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.

View File

@ -13,6 +13,13 @@ pub enum UiTextSize {
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
#[default] #[default]
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. /// The small size for UI text.
/// ///
/// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
@ -31,6 +38,7 @@ pub enum UiTextSize {
impl UiTextSize { impl UiTextSize {
pub fn rems(self) -> Rems { pub fn rems(self) -> Rems {
match self { match self {
Self::Large => rems(16. / 16.),
Self::Default => rems(14. / 16.), Self::Default => rems(14. / 16.),
Self::Small => rems(12. / 16.), Self::Small => rems(12. / 16.),
Self::XSmall => rems(10. / 16.), Self::XSmall => rems(10. / 16.),

View File

@ -1,10 +1,14 @@
use crate::{Toast, Workspace}; use crate::{Toast, Workspace};
use collections::HashMap; use collections::HashMap;
use gpui::{ use gpui::{
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Global, svg, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext, Global, PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
}; };
use language::DiagnosticSeverity;
use std::{any::TypeId, ops::DerefMut}; use std::{any::TypeId, ops::DerefMut};
use ui::prelude::*;
use util::ResultExt;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.set_global(NotificationTracker::new()); 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 { pub mod simple_message_notification {
use gpui::{ use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString, div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,

View File

@ -59,7 +59,10 @@ use std::{
any::TypeId, any::TypeId,
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
cmp, env, cmp,
collections::hash_map::DefaultHasher,
env,
hash::{Hash, Hasher},
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{atomic::AtomicUsize, Arc, Weak}, sync::{atomic::AtomicUsize, Arc, Weak},
@ -579,24 +582,13 @@ impl Workspace {
}), }),
project::Event::LanguageServerPrompt(request) => { 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 { this.show_notification(id as usize, cx, |cx| {
let messages = request cx.new_view(|_| notifications::LanguageServerPrompt::new(request.clone()))
.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()
} }
_ => {} _ => {}
@ -2766,7 +2758,7 @@ impl Workspace {
.z_index(100) .z_index(100)
.right_3() .right_3()
.bottom_3() .bottom_3()
.w_96() .w_112()
.h_full() .h_full()
.flex() .flex()
.flex_col() .flex_col()