Make app notifications appear in new workspaces + dismiss on all workspaces (#23432)

The keymap error notifications got convoluted to support displaying the
notification on startup. This change addresses it systemically for all
future app notifications.

Reverts most of #20531, while keeping the fix to handle keyboard layout
switching. This is a better fix for #20531

Release Notes:

- N/A
This commit is contained in:
Michael Sloan 2025-01-21 22:39:04 -07:00 committed by GitHub
parent 1e1997c97b
commit 1769bc957b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 200 additions and 190 deletions

View file

@ -2,13 +2,21 @@ use crate::{Toast, Workspace};
use anyhow::Context; use anyhow::Context;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use gpui::{ use gpui::{
svg, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, EventEmitter, PromptLevel, svg, AnyView, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, EventEmitter,
Render, ScrollHandle, Task, View, ViewContext, VisualContext, WindowContext, Global, PromptLevel, Render, ScrollHandle, Task, View, ViewContext, VisualContext,
WindowContext,
}; };
use std::rc::Rc;
use std::{any::TypeId, time::Duration}; use std::{any::TypeId, time::Duration};
use ui::{prelude::*, Tooltip}; use ui::{prelude::*, Tooltip};
use util::ResultExt; use util::ResultExt;
pub fn init(cx: &mut AppContext) {
cx.set_global(GlobalAppNotifications {
app_notifications: Vec::new(),
})
}
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum NotificationId { pub enum NotificationId {
Unique(TypeId), Unique(TypeId),
@ -54,17 +62,33 @@ impl Workspace {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>, build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
) { ) {
self.dismiss_notification_internal(&id, cx); self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
let notification = build_notification(cx);
cx.subscribe(&notification, {
let id = id.clone();
move |this, _, _: &DismissEvent, cx| {
this.dismiss_notification(&id, cx);
}
})
.detach();
notification.into()
});
}
let notification = build_notification(cx); /// Shows a notification in this workspace's window. Caller must handle dismiss.
cx.subscribe(&notification, { ///
let id = id.clone(); /// This exists so that the `build_notification` closures stored for app notifications can
move |this, _, _: &DismissEvent, cx| { /// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that
this.dismiss_notification_internal(&id, cx); /// responsibility is pushed to the caller where the `V` type is known.
} pub(crate) fn show_notification_without_handling_dismiss_events(
}) &mut self,
.detach(); id: &NotificationId,
self.notifications.push((id, notification.into())); cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> AnyView,
) {
self.dismiss_notification(id, cx);
self.notifications
.push((id.clone(), build_notification(cx)));
cx.notify(); cx.notify();
} }
@ -91,7 +115,14 @@ impl Workspace {
} }
pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) { pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.dismiss_notification_internal(id, cx) self.notifications.retain(|(existing_id, _)| {
if existing_id == id {
cx.notify();
false
} else {
true
}
});
} }
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) { pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
@ -131,15 +162,18 @@ impl Workspace {
cx.notify(); cx.notify();
} }
fn dismiss_notification_internal(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) { pub fn show_initial_notifications(&mut self, cx: &mut ViewContext<Self>) {
self.notifications.retain(|(existing_id, _)| { // Allow absence of the global so that tests don't need to initialize it.
if existing_id == id { let app_notifications = cx
cx.notify(); .try_global::<GlobalAppNotifications>()
false .iter()
} else { .flat_map(|global| global.app_notifications.iter().cloned())
true .collect::<Vec<_>>();
} for (id, build_notification) in app_notifications {
}); self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
build_notification(cx)
});
}
} }
} }
@ -467,66 +501,103 @@ pub mod simple_message_notification {
} }
} }
/// Shows a notification in the active workspace if there is one, otherwise shows it in all workspaces. /// Stores app notifications so that they can be shown in new workspaces.
pub fn show_app_notification<V: Notification>( struct GlobalAppNotifications {
app_notifications: Vec<(
NotificationId,
Rc<dyn Fn(&mut ViewContext<Workspace>) -> AnyView>,
)>,
}
impl Global for GlobalAppNotifications {}
impl GlobalAppNotifications {
pub fn insert(
&mut self,
id: NotificationId,
build_notification: Rc<dyn Fn(&mut ViewContext<Workspace>) -> AnyView>,
) {
self.remove(&id);
self.app_notifications.push((id, build_notification))
}
pub fn remove(&mut self, id: &NotificationId) {
self.app_notifications
.retain(|(existing_id, _)| existing_id != id);
}
}
/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
/// is particularly to handle notifications that occur on initialization before any workspaces
/// exist. If the notification is dismissed within any workspace, it will be removed from all.
pub fn show_app_notification<V: Notification + 'static>(
id: NotificationId, id: NotificationId,
cx: &mut AppContext, cx: &mut AppContext,
build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V>, build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V> + 'static,
) -> Result<()> { ) -> Result<()> {
let workspaces_to_notify = if let Some(active_workspace_window) = cx // Handle dismiss events by removing the notification from all workspaces.
.active_window() let build_notification: Rc<dyn Fn(&mut ViewContext<Workspace>) -> AnyView> = Rc::new({
.and_then(|window| window.downcast::<Workspace>()) let id = id.clone();
{ move |cx| {
vec![active_workspace_window] let notification = build_notification(cx);
} else { cx.subscribe(&notification, {
let mut workspaces_to_notify = Vec::new(); let id = id.clone();
for window in cx.windows() { move |_, _, _: &DismissEvent, cx| {
if let Some(workspace_window) = window.downcast::<Workspace>() { dismiss_app_notification(&id, cx);
workspaces_to_notify.push(workspace_window); }
} })
.detach();
notification.into()
} }
workspaces_to_notify });
};
// Store the notification so that new workspaces also receive it.
cx.global_mut::<GlobalAppNotifications>()
.insert(id.clone(), build_notification.clone());
let mut notified = false;
let mut notify_errors = Vec::new(); let mut notify_errors = Vec::new();
for workspace_window in workspaces_to_notify { for window in cx.windows() {
let notify_result = workspace_window.update(cx, |workspace, cx| { if let Some(workspace_window) = window.downcast::<Workspace>() {
workspace.show_notification(id.clone(), cx, &build_notification); let notify_result = workspace_window.update(cx, |workspace, cx| {
}); workspace.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
match notify_result { build_notification(cx)
Ok(()) => notified = true, });
Err(notify_err) => notify_errors.push(notify_err), });
match notify_result {
Ok(()) => {}
Err(notify_err) => notify_errors.push(notify_err),
}
} }
} }
if notified { if notify_errors.is_empty() {
Ok(()) Ok(())
} else { } else {
if notify_errors.is_empty() { Err(anyhow!(
Err(anyhow!("Found no workspaces to show notification.")) "No workspaces were able to show notification. Errors:\n\n{}",
} else { notify_errors
Err(anyhow!( .iter()
"No workspaces were able to show notification. Errors:\n\n{}", .map(|e| e.to_string())
notify_errors .collect::<Vec<_>>()
.iter() .join("\n\n")
.map(|e| e.to_string()) ))
.collect::<Vec<_>>()
.join("\n\n")
))
}
} }
} }
pub fn dismiss_app_notification(id: &NotificationId, cx: &mut AppContext) { pub fn dismiss_app_notification(id: &NotificationId, cx: &mut AppContext) {
cx.global_mut::<GlobalAppNotifications>().remove(id);
for window in cx.windows() { for window in cx.windows() {
if let Some(workspace_window) = window.downcast::<Workspace>() { if let Some(workspace_window) = window.downcast::<Workspace>() {
workspace_window let id = id.clone();
.update(cx, |workspace, cx| { // This spawn is necessary in order to dismiss the notification on which the click
workspace.dismiss_notification(&id, cx); // occurred, because in that case we're already in the middle of an update.
cx.spawn(move |mut cx| async move {
workspace_window.update(&mut cx, |workspace, cx| {
workspace.dismiss_notification(&id, cx)
}) })
.ok(); })
.detach_and_log_err(cx);
} }
} }
} }
@ -556,7 +627,7 @@ where
match self { match self {
Ok(value) => Some(value), Ok(value) => Some(value),
Err(err) => { Err(err) => {
log::error!("TODO {err:?}"); log::error!("Showing error notification in workspace: {err:?}");
workspace.show_error(&err, cx); workspace.show_error(&err, cx);
None None
} }
@ -584,10 +655,17 @@ where
Ok(value) => Some(value), Ok(value) => Some(value),
Err(err) => { Err(err) => {
let message: SharedString = format!("Error: {err}").into(); let message: SharedString = format!("Error: {err}").into();
show_app_notification(workspace_error_notification_id(), cx, |cx| { log::error!("Showing error notification in app: {message}");
cx.new_view(|_cx| ErrorMessagePrompt::new(message.clone())) show_app_notification(workspace_error_notification_id(), cx, {
let message = message.clone();
move |cx| {
cx.new_view({
let message = message.clone();
move |_cx| ErrorMessagePrompt::new(message)
})
}
}) })
.with_context(|| format!("Showing error notification: {message}")) .with_context(|| format!("Error while showing error notification: {message}"))
.log_err(); .log_err();
None None
} }

View file

@ -368,6 +368,7 @@ fn prompt_and_open_paths(
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx); init_settings(cx);
notifications::init(cx);
theme_preview::init(cx); theme_preview::init(cx);
cx.on_action(Workspace::close_global); cx.on_action(Workspace::close_global);
@ -1056,6 +1057,7 @@ impl Workspace {
cx.defer(|this, cx| { cx.defer(|this, cx| {
this.update_window_title(cx); this.update_window_title(cx);
this.show_initial_notifications(cx);
}); });
Workspace { Workspace {
weak_self: weak_handle.clone(), weak_self: weak_handle.clone(),

View file

@ -23,10 +23,10 @@ use feature_flags::FeatureFlagAppExt;
use futures::FutureExt; use futures::FutureExt;
use futures::{channel::mpsc, select_biased, StreamExt}; use futures::{channel::mpsc, select_biased, StreamExt};
use gpui::{ use gpui::{
actions, point, px, Action, AnyWindowHandle, AppContext, AsyncAppContext, Context, actions, point, px, Action, AppContext, AsyncAppContext, Context, DismissEvent, Element,
DismissEvent, Element, FocusableView, KeyBinding, MenuItem, ParentElement, PathPromptOptions, FocusableView, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal,
PromptLevel, ReadGlobal, SharedString, Styled, Task, TitlebarOptions, View, ViewContext, SharedString, Styled, Task, TitlebarOptions, View, ViewContext, VisualContext, WindowKind,
VisualContext, WindowKind, WindowOptions, WindowOptions,
}; };
pub use open_listener::*; pub use open_listener::*;
use outline_panel::OutlinePanel; use outline_panel::OutlinePanel;
@ -1053,16 +1053,6 @@ pub fn handle_keymap_file_changes(
}) })
.detach(); .detach();
// Need to notify about keymap load errors when new workspaces are created, so that initial
// keymap load errors are shown to the user.
let (new_workspace_window_tx, mut new_workspace_window_rx) = mpsc::unbounded();
cx.observe_new_views(move |_: &mut Workspace, cx| {
new_workspace_window_tx
.unbounded_send(cx.window_handle())
.unwrap();
})
.detach();
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout()); let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout());
cx.on_keyboard_layout_change(move |cx| { cx.on_keyboard_layout_change(move |cx| {
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout()); let next_mapping = settings::get_key_equivalents(cx.keyboard_layout());
@ -1080,65 +1070,34 @@ pub fn handle_keymap_file_changes(
cx.spawn(move |cx| async move { cx.spawn(move |cx| async move {
let mut user_keymap_content = String::new(); let mut user_keymap_content = String::new();
enum LastError {
None,
JsonError(anyhow::Error),
LoadError(MarkdownString),
}
let mut last_load_error = LastError::None;
loop { loop {
let new_workspace_window = select_biased! { select_biased! {
_ = base_keymap_rx.next() => None, _ = base_keymap_rx.next() => {},
_ = keyboard_layout_rx.next() => None, _ = keyboard_layout_rx.next() => {},
workspace = new_workspace_window_rx.next() => workspace,
content = user_keymap_file_rx.next() => { content = user_keymap_file_rx.next() => {
if let Some(content) = content { if let Some(content) = content {
user_keymap_content = content; user_keymap_content = content;
} }
None
} }
}; };
cx.update(|cx| { cx.update(|cx| {
// No need to reload keymaps when a new workspace is added, just need to send the notification to it. let load_result = KeymapFile::load(&user_keymap_content, cx);
if new_workspace_window.is_none() { match load_result {
let load_result = KeymapFile::load(&user_keymap_content, cx); KeymapFileLoadResult::Success { key_bindings } => {
match load_result { reload_keymaps(cx, key_bindings);
KeymapFileLoadResult::Success { key_bindings } => { dismiss_app_notification(&notification_id, cx);
}
KeymapFileLoadResult::SomeFailedToLoad {
key_bindings,
error_message,
} => {
if !key_bindings.is_empty() {
reload_keymaps(cx, key_bindings); reload_keymaps(cx, key_bindings);
dismiss_app_notification(&notification_id, cx);
last_load_error = LastError::None;
}
KeymapFileLoadResult::SomeFailedToLoad {
key_bindings,
error_message,
} => {
if !key_bindings.is_empty() {
reload_keymaps(cx, key_bindings);
}
last_load_error = LastError::LoadError(error_message);
}
KeymapFileLoadResult::JsonParseFailure { error } => {
last_load_error = LastError::JsonError(error);
} }
show_keymap_file_load_error(notification_id.clone(), error_message, cx)
} }
} KeymapFileLoadResult::JsonParseFailure { error } => {
match &last_load_error { show_keymap_file_json_error(notification_id.clone(), &error, cx)
LastError::None => {}
LastError::JsonError(err) => {
show_keymap_file_json_error(
new_workspace_window,
notification_id.clone(),
err,
cx,
);
}
LastError::LoadError(message) => {
show_keymap_file_load_error(
new_workspace_window,
notification_id.clone(),
message.clone(),
cx,
);
} }
} }
}) })
@ -1149,32 +1108,26 @@ pub fn handle_keymap_file_changes(
} }
fn show_keymap_file_json_error( fn show_keymap_file_json_error(
new_workspace_window: Option<AnyWindowHandle>,
notification_id: NotificationId, notification_id: NotificationId,
error: &anyhow::Error, error: &anyhow::Error,
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
let message: SharedString = let message: SharedString =
format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into(); format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
show_notification_to_specific_workspace_or_all_workspaces( show_app_notification(notification_id, cx, move |cx| {
new_workspace_window, cx.new_view(|_cx| {
notification_id, MessageNotification::new(message.clone())
cx, .with_click_message("Open keymap file")
move |cx| { .on_click(|cx| {
cx.new_view(|_cx| { cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
MessageNotification::new(message.clone()) cx.emit(DismissEvent);
.with_click_message("Open keymap file") })
.on_click(|cx| { })
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone()); })
cx.emit(DismissEvent); .log_err();
})
})
},
);
} }
fn show_keymap_file_load_error( fn show_keymap_file_load_error(
new_workspace_window: Option<AnyWindowHandle>,
notification_id: NotificationId, notification_id: NotificationId,
markdown_error_message: MarkdownString, markdown_error_message: MarkdownString,
cx: &mut AppContext, cx: &mut AppContext,
@ -1193,57 +1146,34 @@ fn show_keymap_file_load_error(
cx.spawn(move |cx| async move { cx.spawn(move |cx| async move {
let parsed_markdown = Rc::new(parsed_markdown.await); let parsed_markdown = Rc::new(parsed_markdown.await);
cx.update(|cx| { cx.update(|cx| {
show_notification_to_specific_workspace_or_all_workspaces( show_app_notification(notification_id, cx, move |cx| {
new_workspace_window, let workspace_handle = cx.view().downgrade();
notification_id, let parsed_markdown = parsed_markdown.clone();
cx, cx.new_view(move |_cx| {
move |cx| { MessageNotification::new_from_builder(move |cx| {
let workspace_handle = cx.view().downgrade(); gpui::div()
let parsed_markdown = parsed_markdown.clone(); .text_xs()
cx.new_view(move |_cx| { .child(markdown_preview::markdown_renderer::render_parsed_markdown(
MessageNotification::new_from_builder(move |cx| { &parsed_markdown.clone(),
gpui::div() Some(workspace_handle.clone()),
.text_xs() cx,
.child(markdown_preview::markdown_renderer::render_parsed_markdown( ))
&parsed_markdown.clone(), .into_any()
Some(workspace_handle.clone()),
cx,
))
.into_any()
})
.with_click_message("Open keymap file")
.on_click(|cx| {
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
cx.emit(DismissEvent);
})
}) })
}, .with_click_message("Open keymap file")
) .on_click(|cx| {
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
cx.emit(DismissEvent);
})
})
})
.log_err();
}) })
.ok(); .ok();
}) })
.detach(); .detach();
} }
fn show_notification_to_specific_workspace_or_all_workspaces<V>(
new_workspace_window: Option<AnyWindowHandle>,
notification_id: NotificationId,
cx: &mut AppContext,
build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V>,
) where
V: workspace::notifications::Notification,
{
if let Some(workspace_window) = new_workspace_window.and_then(|w| w.downcast::<Workspace>()) {
workspace_window
.update(cx, |workspace, cx| {
workspace.show_notification(notification_id, cx, build_notification);
})
.ok();
} else {
show_app_notification(notification_id, cx, build_notification).ok();
}
}
fn reload_keymaps(cx: &mut AppContext, user_key_bindings: Vec<KeyBinding>) { fn reload_keymaps(cx: &mut AppContext, user_key_bindings: Vec<KeyBinding>) {
cx.clear_key_bindings(); cx.clear_key_bindings();
load_default_keymap(cx); load_default_keymap(cx);