From 007fd0586a1f35d8ae2e0ac14800007558250c45 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 6 May 2025 18:15:26 +0300 Subject: [PATCH] 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 --- Cargo.lock | 1 + crates/collab_ui/src/notification_panel.rs | 10 ++- crates/workspace/Cargo.toml | 1 + crates/workspace/src/notifications.rs | 72 +++++++++++++++++++--- crates/workspace/src/workspace.rs | 25 +++++++- 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d8f2d9461..05359e1a52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18158,6 +18158,7 @@ dependencies = [ "itertools 0.14.0", "language", "log", + "menu", "node_runtime", "parking_lot", "postage", diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 58df3cead3..883e17fbac 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -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 for NotificationToast {} +impl EventEmitter for NotificationToast {} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index fb575cdc64..22ec62a2be 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -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 diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 3f17b50c39..eaf3085bc8 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -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 + Focusable + Render {} +pub trait Notification: + EventEmitter + EventEmitter + Focusable + Render +{ +} + +pub struct SuppressEvent; impl Workspace { #[cfg(any(test, feature = "test-support"))] @@ -81,6 +86,13 @@ impl Workspace { } }) .detach(); + cx.subscribe(¬ification, { + 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, build_notification: impl FnOnce(&mut Context) -> 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.dismiss_notification(id, cx); + self.suppressed_notifications.insert(id.clone()); + } + pub fn show_initial_notifications(&mut self, cx: &mut Context) { // 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 for LanguageServerPrompt {} +impl EventEmitter for LanguageServerPrompt {} fn workspace_error_notification_id() -> NotificationId { struct WorkspaceErrorNotification; @@ -401,6 +430,7 @@ impl Focusable for ErrorMessagePrompt { } impl EventEmitter for ErrorMessagePrompt {} +impl EventEmitter 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, more_info_url: Option>, show_close_button: bool, + show_suppress_button: bool, title: Option, } @@ -439,6 +470,7 @@ pub mod simple_message_notification { } impl EventEmitter for MessageNotification {} + impl EventEmitter 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(mut self, title: S) -> Self where S: Into, @@ -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() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a0c1b2df88..cf32d8cf50 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, titlebar_item: Option, notifications: Notifications, + suppressed_notifications: HashSet, project: Entity, follower_states: HashMap, last_leaders_by_pane: HashMap, 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(¬ification_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) { + if let Some((notification_id, _)) = self.notifications.pop() { + dismiss_app_notification(¬ification_id, cx); + return; + } + + cx.propagate(); + } } fn leader_border_for_pane(