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", "itertools 0.14.0",
"language", "language",
"log", "log",
"menu",
"node_runtime", "node_runtime",
"parking_lot", "parking_lot",
"postage", "postage",

View file

@ -22,7 +22,9 @@ use ui::{
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
}; };
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId}; use workspace::notifications::{
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
};
use workspace::{ use workspace::{
Workspace, Workspace,
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -823,6 +825,11 @@ impl Render for NotificationToast {
IconButton::new("close", IconName::Close) IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), .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| { .on_click(cx.listener(|this, _, window, cx| {
this.focus_notification_panel(window, cx); this.focus_notification_panel(window, cx);
cx.emit(DismissEvent); cx.emit(DismissEvent);
@ -831,3 +838,4 @@ impl Render for NotificationToast {
} }
impl EventEmitter<DismissEvent> 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 itertools.workspace = true
language.workspace = true language.workspace = true
log.workspace = true log.workspace = true
menu.workspace = true
node_runtime.workspace = true node_runtime.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
postage.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 { pub enum NotificationId {
Unique(TypeId), Unique(TypeId),
Composite(TypeId, ElementId), 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 { impl Workspace {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -81,6 +86,13 @@ impl Workspace {
} }
}) })
.detach(); .detach();
cx.subscribe(&notification, {
let id = id.clone();
move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
workspace.suppress_notification(&id, cx);
}
})
.detach();
notification.into() notification.into()
}); });
} }
@ -96,6 +108,9 @@ impl Workspace {
cx: &mut Context<Self>, cx: &mut Context<Self>,
build_notification: impl FnOnce(&mut Context<Self>) -> AnyView, build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,
) { ) {
if self.suppressed_notifications.contains(id) {
return;
}
self.dismiss_notification(id, cx); self.dismiss_notification(id, cx);
self.notifications self.notifications
.push((id.clone(), build_notification(cx))); .push((id.clone(), build_notification(cx)));
@ -172,6 +187,11 @@ impl Workspace {
cx.notify(); 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>) { 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. // Allow absence of the global so that tests don't need to initialize it.
let app_notifications = GLOBAL_APP_NOTIFICATIONS let app_notifications = GLOBAL_APP_NOTIFICATIONS
@ -268,6 +288,14 @@ impl Render for LanguageServerPrompt {
) )
.child( .child(
h_flex() 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( .child(
IconButton::new("copy", IconName::Copy) IconButton::new("copy", IconName::Copy)
.on_click({ .on_click({
@ -305,6 +333,7 @@ impl Render for LanguageServerPrompt {
} }
impl EventEmitter<DismissEvent> for LanguageServerPrompt {} impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
impl EventEmitter<SuppressEvent> for LanguageServerPrompt {}
fn workspace_error_notification_id() -> NotificationId { fn workspace_error_notification_id() -> NotificationId {
struct WorkspaceErrorNotification; struct WorkspaceErrorNotification;
@ -401,6 +430,7 @@ impl Focusable for ErrorMessagePrompt {
} }
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {} impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
impl Notification for ErrorMessagePrompt {} impl Notification for ErrorMessagePrompt {}
@ -411,9 +441,9 @@ pub mod simple_message_notification {
AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render, AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
SharedString, Styled, div, SharedString, Styled, div,
}; };
use ui::prelude::*; use ui::{Tooltip, prelude::*};
use super::Notification; use super::{Notification, SuppressEvent};
pub struct MessageNotification { pub struct MessageNotification {
focus_handle: FocusHandle, focus_handle: FocusHandle,
@ -429,6 +459,7 @@ pub mod simple_message_notification {
more_info_message: Option<SharedString>, more_info_message: Option<SharedString>,
more_info_url: Option<Arc<str>>, more_info_url: Option<Arc<str>>,
show_close_button: bool, show_close_button: bool,
show_suppress_button: bool,
title: Option<SharedString>, title: Option<SharedString>,
} }
@ -439,6 +470,7 @@ pub mod simple_message_notification {
} }
impl EventEmitter<DismissEvent> for MessageNotification {} impl EventEmitter<DismissEvent> for MessageNotification {}
impl EventEmitter<SuppressEvent> for MessageNotification {}
impl Notification for MessageNotification {} impl Notification for MessageNotification {}
@ -470,6 +502,7 @@ pub mod simple_message_notification {
more_info_message: None, more_info_message: None,
more_info_url: None, more_info_url: None,
show_close_button: true, show_close_button: true,
show_suppress_button: true,
title: None, title: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
} }
@ -568,6 +601,11 @@ pub mod simple_message_notification {
self 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 pub fn with_title<S>(mut self, title: S) -> Self
where where
S: Into<SharedString>, S: Into<SharedString>,
@ -597,12 +635,26 @@ pub mod simple_message_notification {
}) })
.child(div().max_w_96().child((self.build_content)(window, cx))), .child(div().max_w_96().child((self.build_content)(window, cx))),
) )
.when(self.show_close_button, |this| { .child(
this.child( h_flex()
IconButton::new("close", IconName::Close) .gap_2()
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))), .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( .child(
h_flex() h_flex()

View file

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