Adds a way to dismiss workspace notifications (#30015)

Closes https://github.com/zed-industries/zed/issues/10140

* On `menu::Cancel` action (`ESC`), close notifications, one by one, if
`Workspace` gets to handle this action.
More specific, focused items contexts (e.g. `Editor`) take priority.

* Allows to temporarily suppress notifications of this kind either by
clicking a corresponding button in the UI, or using
`workspace::SuppressNotification` action.

This might not work well out of the box for all notifications and might
require further improvement.


https://github.com/user-attachments/assets/0ea49ee6-cd21-464f-ba74-fc40f7a8dedf


Release Notes:

- Added a way to dismiss workspace notifications
This commit is contained in:
Kirill Bulatov 2025-05-06 18:15:26 +03:00 committed by GitHub
parent 7d361ec97e
commit 007fd0586a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 96 additions and 13 deletions

1
Cargo.lock generated
View File

@ -18158,6 +18158,7 @@ dependencies = [
"itertools 0.14.0",
"language",
"log",
"menu",
"node_runtime",
"parking_lot",
"postage",

View File

@ -22,7 +22,9 @@ use ui::{
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
};
use util::{ResultExt, TryFutureExt};
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
use workspace::notifications::{
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
};
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
@ -823,6 +825,11 @@ impl Render for NotificationToast {
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(
IconButton::new("suppress", IconName::XCircle)
.tooltip(Tooltip::text("Do not show until restart"))
.on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
)
.on_click(cx.listener(|this, _, window, cx| {
this.focus_notification_panel(window, cx);
cx.emit(DismissEvent);
@ -831,3 +838,4 @@ impl Render for NotificationToast {
}
impl EventEmitter<DismissEvent> for NotificationToast {}
impl EventEmitter<SuppressEvent> for NotificationToast {}

View File

@ -43,6 +43,7 @@ http_client.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
postage.workspace = true

View File

@ -29,7 +29,7 @@ impl std::ops::DerefMut for Notifications {
}
}
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum NotificationId {
Unique(TypeId),
Composite(TypeId, ElementId),
@ -54,7 +54,12 @@ impl NotificationId {
}
}
pub trait Notification: EventEmitter<DismissEvent> + Focusable + Render {}
pub trait Notification:
EventEmitter<DismissEvent> + EventEmitter<SuppressEvent> + Focusable + Render
{
}
pub struct SuppressEvent;
impl Workspace {
#[cfg(any(test, feature = "test-support"))]
@ -81,6 +86,13 @@ impl Workspace {
}
})
.detach();
cx.subscribe(&notification, {
let id = id.clone();
move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
workspace.suppress_notification(&id, cx);
}
})
.detach();
notification.into()
});
}
@ -96,6 +108,9 @@ impl Workspace {
cx: &mut Context<Self>,
build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,
) {
if self.suppressed_notifications.contains(id) {
return;
}
self.dismiss_notification(id, cx);
self.notifications
.push((id.clone(), build_notification(cx)));
@ -172,6 +187,11 @@ impl Workspace {
cx.notify();
}
pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
self.dismiss_notification(id, cx);
self.suppressed_notifications.insert(id.clone());
}
pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
// Allow absence of the global so that tests don't need to initialize it.
let app_notifications = GLOBAL_APP_NOTIFICATIONS
@ -268,6 +288,14 @@ impl Render for LanguageServerPrompt {
)
.child(
h_flex()
.gap_2()
.child(
IconButton::new("suppress", IconName::XCircle)
.tooltip(Tooltip::text("Do not show until restart"))
.on_click(
cx.listener(|_, _, _, cx| cx.emit(SuppressEvent)),
),
)
.child(
IconButton::new("copy", IconName::Copy)
.on_click({
@ -305,6 +333,7 @@ impl Render for LanguageServerPrompt {
}
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
impl EventEmitter<SuppressEvent> for LanguageServerPrompt {}
fn workspace_error_notification_id() -> NotificationId {
struct WorkspaceErrorNotification;
@ -401,6 +430,7 @@ impl Focusable for ErrorMessagePrompt {
}
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
impl Notification for ErrorMessagePrompt {}
@ -411,9 +441,9 @@ pub mod simple_message_notification {
AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
SharedString, Styled, div,
};
use ui::prelude::*;
use ui::{Tooltip, prelude::*};
use super::Notification;
use super::{Notification, SuppressEvent};
pub struct MessageNotification {
focus_handle: FocusHandle,
@ -429,6 +459,7 @@ pub mod simple_message_notification {
more_info_message: Option<SharedString>,
more_info_url: Option<Arc<str>>,
show_close_button: bool,
show_suppress_button: bool,
title: Option<SharedString>,
}
@ -439,6 +470,7 @@ pub mod simple_message_notification {
}
impl EventEmitter<DismissEvent> for MessageNotification {}
impl EventEmitter<SuppressEvent> for MessageNotification {}
impl Notification for MessageNotification {}
@ -470,6 +502,7 @@ pub mod simple_message_notification {
more_info_message: None,
more_info_url: None,
show_close_button: true,
show_suppress_button: true,
title: None,
focus_handle: cx.focus_handle(),
}
@ -568,6 +601,11 @@ pub mod simple_message_notification {
self
}
pub fn show_suppress_button(mut self, show: bool) -> Self {
self.show_suppress_button = show;
self
}
pub fn with_title<S>(mut self, title: S) -> Self
where
S: Into<SharedString>,
@ -597,12 +635,26 @@ pub mod simple_message_notification {
})
.child(div().max_w_96().child((self.build_content)(window, cx))),
)
.when(self.show_close_button, |this| {
this.child(
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
)
}),
.child(
h_flex()
.gap_2()
.when(self.show_suppress_button, |this| {
this.child(
IconButton::new("suppress", IconName::XCircle)
.tooltip(Tooltip::text("Do not show until restart"))
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(SuppressEvent);
})),
)
})
.when(self.show_close_button, |this| {
this.child(
IconButton::new("close", IconName::Close).on_click(
cx.listener(|this, _, _, cx| this.dismiss(cx)),
),
)
}),
),
)
.child(
h_flex()

View File

@ -52,7 +52,8 @@ use language::{Buffer, LanguageRegistry, Rope};
pub use modal_layer::*;
use node_runtime::NodeRuntime;
use notifications::{
DetachAndPromptErr, Notifications, simple_message_notification::MessageNotification,
DetachAndPromptErr, Notifications, dismiss_app_notification,
simple_message_notification::MessageNotification,
};
pub use pane::*;
pub use pane_group::*;
@ -179,6 +180,7 @@ actions!(
SaveAs,
SaveWithoutFormat,
ShutdownDebugAdapters,
SuppressNotification,
ToggleBottomDock,
ToggleCenteredLayout,
ToggleLeftDock,
@ -921,6 +923,7 @@ pub struct Workspace {
toast_layer: Entity<ToastLayer>,
titlebar_item: Option<AnyView>,
notifications: Notifications,
suppressed_notifications: HashSet<NotificationId>,
project: Entity<Project>,
follower_states: HashMap<CollaboratorId, FollowerState>,
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
@ -1245,7 +1248,8 @@ impl Workspace {
modal_layer,
toast_layer,
titlebar_item: None,
notifications: Default::default(),
notifications: Notifications::default(),
suppressed_notifications: HashSet::default(),
left_dock,
bottom_dock,
bottom_dock_layout,
@ -5301,12 +5305,20 @@ impl Workspace {
workspace.clear_all_notifications(cx);
},
))
.on_action(cx.listener(
|workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
if let Some((notification_id, _)) = workspace.notifications.pop() {
workspace.suppress_notification(&notification_id, cx);
}
},
))
.on_action(cx.listener(
|workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
workspace.reopen_closed_item(window, cx).detach();
},
))
.on_action(cx.listener(Workspace::toggle_centered_layout))
.on_action(cx.listener(Workspace::cancel))
}
#[cfg(any(test, feature = "test-support"))]
@ -5477,6 +5489,15 @@ impl Workspace {
.update(cx, |_, window, _| window.activate_window())
.ok();
}
pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
if let Some((notification_id, _)) = self.notifications.pop() {
dismiss_app_notification(&notification_id, cx);
return;
}
cx.propagate();
}
}
fn leader_border_for_pane(