1use crate::{SuppressNotification, Toast, Workspace};
2use gpui::{
3 AnyView, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
4 Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, Task, svg,
5};
6use parking_lot::Mutex;
7use std::ops::Deref;
8use std::sync::{Arc, LazyLock};
9use std::{any::TypeId, time::Duration};
10use ui::{Tooltip, prelude::*};
11use util::ResultExt;
12
13#[derive(Default)]
14pub struct Notifications {
15 notifications: Vec<(NotificationId, AnyView)>,
16}
17
18impl Deref for Notifications {
19 type Target = Vec<(NotificationId, AnyView)>;
20
21 fn deref(&self) -> &Self::Target {
22 &self.notifications
23 }
24}
25
26impl std::ops::DerefMut for Notifications {
27 fn deref_mut(&mut self) -> &mut Self::Target {
28 &mut self.notifications
29 }
30}
31
32#[derive(Debug, Eq, PartialEq, Clone, Hash)]
33pub enum NotificationId {
34 Unique(TypeId),
35 Composite(TypeId, ElementId),
36 Named(SharedString),
37}
38
39impl NotificationId {
40 /// Returns a unique [`NotificationId`] for the given type.
41 pub fn unique<T: 'static>() -> Self {
42 Self::Unique(TypeId::of::<T>())
43 }
44
45 /// Returns a [`NotificationId`] for the given type that is also identified
46 /// by the provided ID.
47 pub fn composite<T: 'static>(id: impl Into<ElementId>) -> Self {
48 Self::Composite(TypeId::of::<T>(), id.into())
49 }
50
51 /// Builds a `NotificationId` out of the given string.
52 pub fn named(id: SharedString) -> Self {
53 Self::Named(id)
54 }
55}
56
57pub trait Notification:
58 EventEmitter<DismissEvent> + EventEmitter<SuppressEvent> + Focusable + Render
59{
60}
61
62pub struct SuppressEvent;
63
64impl Workspace {
65 #[cfg(any(test, feature = "test-support"))]
66 pub fn notification_ids(&self) -> Vec<NotificationId> {
67 self.notifications
68 .iter()
69 .map(|(id, _)| id)
70 .cloned()
71 .collect()
72 }
73
74 pub fn show_notification<V: Notification>(
75 &mut self,
76 id: NotificationId,
77 cx: &mut Context<Self>,
78 build_notification: impl FnOnce(&mut Context<Self>) -> Entity<V>,
79 ) {
80 self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
81 let notification = build_notification(cx);
82 cx.subscribe(¬ification, {
83 let id = id.clone();
84 move |this, _, _: &DismissEvent, cx| {
85 this.dismiss_notification(&id, cx);
86 }
87 })
88 .detach();
89 cx.subscribe(¬ification, {
90 let id = id.clone();
91 move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
92 workspace.suppress_notification(&id, cx);
93 }
94 })
95 .detach();
96 notification.into()
97 });
98 }
99
100 /// Shows a notification in this workspace's window. Caller must handle dismiss.
101 ///
102 /// This exists so that the `build_notification` closures stored for app notifications can
103 /// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that
104 /// responsibility is pushed to the caller where the `V` type is known.
105 pub(crate) fn show_notification_without_handling_dismiss_events(
106 &mut self,
107 id: &NotificationId,
108 cx: &mut Context<Self>,
109 build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,
110 ) {
111 if self.suppressed_notifications.contains(id) {
112 return;
113 }
114 self.dismiss_notification(id, cx);
115 self.notifications
116 .push((id.clone(), build_notification(cx)));
117 cx.notify();
118 }
119
120 pub fn show_error<E>(&mut self, err: &E, cx: &mut Context<Self>)
121 where
122 E: std::fmt::Debug + std::fmt::Display,
123 {
124 self.show_notification(workspace_error_notification_id(), cx, |cx| {
125 cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx))
126 });
127 }
128
129 pub fn show_portal_error(&mut self, err: String, cx: &mut Context<Self>) {
130 struct PortalError;
131
132 self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
133 cx.new(|cx| {
134 ErrorMessagePrompt::new(err.to_string(), cx).with_link_button(
135 "See docs",
136 "https://zed.dev/docs/linux#i-cant-open-any-files",
137 )
138 })
139 });
140 }
141
142 pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
143 self.notifications.retain(|(existing_id, _)| {
144 if existing_id == id {
145 cx.notify();
146 false
147 } else {
148 true
149 }
150 });
151 }
152
153 pub fn show_toast(&mut self, toast: Toast, cx: &mut Context<Self>) {
154 self.dismiss_notification(&toast.id, cx);
155 self.show_notification(toast.id.clone(), cx, |cx| {
156 cx.new(|cx| match toast.on_click.as_ref() {
157 Some((click_msg, on_click)) => {
158 let on_click = on_click.clone();
159 simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
160 .primary_message(click_msg.clone())
161 .primary_on_click(move |window, cx| on_click(window, cx))
162 }
163 None => {
164 simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
165 }
166 })
167 });
168 if toast.autohide {
169 cx.spawn(async move |workspace, cx| {
170 cx.background_executor()
171 .timer(Duration::from_millis(5000))
172 .await;
173 workspace
174 .update(cx, |workspace, cx| workspace.dismiss_toast(&toast.id, cx))
175 .ok();
176 })
177 .detach();
178 }
179 }
180
181 pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
182 self.dismiss_notification(id, cx);
183 }
184
185 pub fn clear_all_notifications(&mut self, cx: &mut Context<Self>) {
186 self.notifications.clear();
187 cx.notify();
188 }
189
190 pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
191 self.dismiss_notification(id, cx);
192 self.suppressed_notifications.insert(id.clone());
193 }
194
195 pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
196 // Allow absence of the global so that tests don't need to initialize it.
197 let app_notifications = GLOBAL_APP_NOTIFICATIONS
198 .lock()
199 .app_notifications
200 .iter()
201 .cloned()
202 .collect::<Vec<_>>();
203 for (id, build_notification) in app_notifications {
204 self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
205 build_notification(cx)
206 });
207 }
208 }
209}
210
211pub struct LanguageServerPrompt {
212 focus_handle: FocusHandle,
213 request: Option<project::LanguageServerPromptRequest>,
214 scroll_handle: ScrollHandle,
215}
216
217impl Focusable for LanguageServerPrompt {
218 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
219 self.focus_handle.clone()
220 }
221}
222
223impl Notification for LanguageServerPrompt {}
224
225impl LanguageServerPrompt {
226 pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self {
227 Self {
228 focus_handle: cx.focus_handle(),
229 request: Some(request),
230 scroll_handle: ScrollHandle::new(),
231 }
232 }
233
234 async fn select_option(this: Entity<Self>, ix: usize, cx: &mut AsyncWindowContext) {
235 util::maybe!(async move {
236 let potential_future = this.update(cx, |this, _| {
237 this.request.take().map(|request| request.respond(ix))
238 });
239
240 potential_future? // App Closed
241 .ok_or_else(|| anyhow::anyhow!("Response already sent"))?
242 .await
243 .ok_or_else(|| anyhow::anyhow!("Stream already closed"))?;
244
245 this.update(cx, |_, cx| cx.emit(DismissEvent))?;
246
247 anyhow::Ok(())
248 })
249 .await
250 .log_err();
251 }
252}
253
254impl Render for LanguageServerPrompt {
255 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
256 let Some(request) = &self.request else {
257 return div().id("language_server_prompt_notification");
258 };
259
260 let (icon, color) = match request.level {
261 PromptLevel::Info => (IconName::Info, Color::Accent),
262 PromptLevel::Warning => (IconName::Warning, Color::Warning),
263 PromptLevel::Critical => (IconName::XCircle, Color::Error),
264 };
265
266 div()
267 .id("language_server_prompt_notification")
268 .group("language_server_prompt_notification")
269 .occlude()
270 .w_full()
271 .max_h(vh(0.8, window))
272 .elevation_3(cx)
273 .overflow_y_scroll()
274 .track_scroll(&self.scroll_handle)
275 .child(
276 v_flex()
277 .p_3()
278 .overflow_hidden()
279 .child(
280 h_flex()
281 .justify_between()
282 .items_start()
283 .child(
284 h_flex()
285 .gap_2()
286 .child(Icon::new(icon).color(color))
287 .child(Label::new(request.lsp_name.clone())),
288 )
289 .child(
290 h_flex()
291 .gap_2()
292 .child(
293 IconButton::new("suppress", IconName::SquareMinus)
294 .tooltip(|window, cx| {
295 Tooltip::for_action(
296 "Do not show until restart",
297 &SuppressNotification,
298 window,
299 cx,
300 )
301 })
302 .on_click(
303 cx.listener(|_, _, _, cx| cx.emit(SuppressEvent)),
304 ),
305 )
306 .child(
307 IconButton::new("copy", IconName::Copy)
308 .on_click({
309 let message = request.message.clone();
310 move |_, _, cx| {
311 cx.write_to_clipboard(
312 ClipboardItem::new_string(message.clone()),
313 )
314 }
315 })
316 .tooltip(Tooltip::text("Copy Description")),
317 )
318 .child(
319 IconButton::new("close", IconName::Close)
320 .tooltip(|window, cx| {
321 Tooltip::for_action(
322 "Close",
323 &menu::Cancel,
324 window,
325 cx,
326 )
327 })
328 .on_click(cx.listener(|_, _, _, cx| {
329 cx.emit(gpui::DismissEvent)
330 })),
331 ),
332 ),
333 )
334 .child(Label::new(request.message.to_string()).size(LabelSize::Small))
335 .children(request.actions.iter().enumerate().map(|(ix, action)| {
336 let this_handle = cx.entity().clone();
337 Button::new(ix, action.title.clone())
338 .size(ButtonSize::Large)
339 .on_click(move |_, window, cx| {
340 let this_handle = this_handle.clone();
341 window
342 .spawn(cx, async move |cx| {
343 LanguageServerPrompt::select_option(this_handle, ix, cx)
344 .await
345 })
346 .detach()
347 })
348 })),
349 )
350 }
351}
352
353impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
354impl EventEmitter<SuppressEvent> for LanguageServerPrompt {}
355
356fn workspace_error_notification_id() -> NotificationId {
357 struct WorkspaceErrorNotification;
358 NotificationId::unique::<WorkspaceErrorNotification>()
359}
360
361#[derive(Debug, Clone)]
362pub struct ErrorMessagePrompt {
363 message: SharedString,
364 focus_handle: gpui::FocusHandle,
365 label_and_url_button: Option<(SharedString, SharedString)>,
366}
367
368impl ErrorMessagePrompt {
369 pub fn new<S>(message: S, cx: &mut App) -> Self
370 where
371 S: Into<SharedString>,
372 {
373 Self {
374 message: message.into(),
375 focus_handle: cx.focus_handle(),
376 label_and_url_button: None,
377 }
378 }
379
380 pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
381 where
382 S: Into<SharedString>,
383 {
384 self.label_and_url_button = Some((label.into(), url.into()));
385 self
386 }
387}
388
389impl Render for ErrorMessagePrompt {
390 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
391 h_flex()
392 .id("error_message_prompt_notification")
393 .occlude()
394 .elevation_3(cx)
395 .items_start()
396 .justify_between()
397 .p_2()
398 .gap_2()
399 .w_full()
400 .child(
401 v_flex()
402 .w_full()
403 .child(
404 h_flex()
405 .w_full()
406 .justify_between()
407 .child(
408 svg()
409 .size(window.text_style().font_size)
410 .flex_none()
411 .mr_2()
412 .mt(px(-2.0))
413 .map(|icon| {
414 icon.path(IconName::Warning.path())
415 .text_color(Color::Error.color(cx))
416 }),
417 )
418 .child(
419 ui::IconButton::new("close", ui::IconName::Close).on_click(
420 cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
421 ),
422 ),
423 )
424 .child(
425 div()
426 .id("error_message")
427 .max_w_96()
428 .max_h_40()
429 .overflow_y_scroll()
430 .child(Label::new(self.message.clone()).size(LabelSize::Small)),
431 )
432 .when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
433 elm.child(
434 div().mt_2().child(
435 ui::Button::new("error_message_prompt_notification_button", label)
436 .on_click(move |_, _, cx| cx.open_url(&url)),
437 ),
438 )
439 }),
440 )
441 }
442}
443
444impl Focusable for ErrorMessagePrompt {
445 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
446 self.focus_handle.clone()
447 }
448}
449
450impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
451impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
452
453impl Notification for ErrorMessagePrompt {}
454
455pub mod simple_message_notification {
456 use std::sync::Arc;
457
458 use gpui::{
459 AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
460 SharedString, Styled, div,
461 };
462 use ui::{Tooltip, prelude::*};
463
464 use crate::SuppressNotification;
465
466 use super::{Notification, SuppressEvent};
467
468 pub struct MessageNotification {
469 focus_handle: FocusHandle,
470 build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
471 primary_message: Option<SharedString>,
472 primary_icon: Option<IconName>,
473 primary_icon_color: Option<Color>,
474 primary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
475 secondary_message: Option<SharedString>,
476 secondary_icon: Option<IconName>,
477 secondary_icon_color: Option<Color>,
478 secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
479 more_info_message: Option<SharedString>,
480 more_info_url: Option<Arc<str>>,
481 show_close_button: bool,
482 show_suppress_button: bool,
483 title: Option<SharedString>,
484 }
485
486 impl Focusable for MessageNotification {
487 fn focus_handle(&self, _: &App) -> FocusHandle {
488 self.focus_handle.clone()
489 }
490 }
491
492 impl EventEmitter<DismissEvent> for MessageNotification {}
493 impl EventEmitter<SuppressEvent> for MessageNotification {}
494
495 impl Notification for MessageNotification {}
496
497 impl MessageNotification {
498 pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
499 where
500 S: Into<SharedString>,
501 {
502 let message = message.into();
503 Self::new_from_builder(cx, move |_, _| {
504 Label::new(message.clone()).into_any_element()
505 })
506 }
507
508 pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
509 where
510 F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
511 {
512 Self {
513 build_content: Box::new(content),
514 primary_message: None,
515 primary_icon: None,
516 primary_icon_color: None,
517 primary_on_click: None,
518 secondary_message: None,
519 secondary_icon: None,
520 secondary_icon_color: None,
521 secondary_on_click: None,
522 more_info_message: None,
523 more_info_url: None,
524 show_close_button: true,
525 show_suppress_button: true,
526 title: None,
527 focus_handle: cx.focus_handle(),
528 }
529 }
530
531 pub fn primary_message<S>(mut self, message: S) -> Self
532 where
533 S: Into<SharedString>,
534 {
535 self.primary_message = Some(message.into());
536 self
537 }
538
539 pub fn primary_icon(mut self, icon: IconName) -> Self {
540 self.primary_icon = Some(icon);
541 self
542 }
543
544 pub fn primary_icon_color(mut self, color: Color) -> Self {
545 self.primary_icon_color = Some(color);
546 self
547 }
548
549 pub fn primary_on_click<F>(mut self, on_click: F) -> Self
550 where
551 F: 'static + Fn(&mut Window, &mut Context<Self>),
552 {
553 self.primary_on_click = Some(Arc::new(on_click));
554 self
555 }
556
557 pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
558 where
559 F: 'static + Fn(&mut Window, &mut Context<Self>),
560 {
561 self.primary_on_click = Some(on_click);
562 self
563 }
564
565 pub fn secondary_message<S>(mut self, message: S) -> Self
566 where
567 S: Into<SharedString>,
568 {
569 self.secondary_message = Some(message.into());
570 self
571 }
572
573 pub fn secondary_icon(mut self, icon: IconName) -> Self {
574 self.secondary_icon = Some(icon);
575 self
576 }
577
578 pub fn secondary_icon_color(mut self, color: Color) -> Self {
579 self.secondary_icon_color = Some(color);
580 self
581 }
582
583 pub fn secondary_on_click<F>(mut self, on_click: F) -> Self
584 where
585 F: 'static + Fn(&mut Window, &mut Context<Self>),
586 {
587 self.secondary_on_click = Some(Arc::new(on_click));
588 self
589 }
590
591 pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
592 where
593 F: 'static + Fn(&mut Window, &mut Context<Self>),
594 {
595 self.secondary_on_click = Some(on_click);
596 self
597 }
598
599 pub fn more_info_message<S>(mut self, message: S) -> Self
600 where
601 S: Into<SharedString>,
602 {
603 self.more_info_message = Some(message.into());
604 self
605 }
606
607 pub fn more_info_url<S>(mut self, url: S) -> Self
608 where
609 S: Into<Arc<str>>,
610 {
611 self.more_info_url = Some(url.into());
612 self
613 }
614
615 pub fn dismiss(&mut self, cx: &mut Context<Self>) {
616 cx.emit(DismissEvent);
617 }
618
619 pub fn show_close_button(mut self, show: bool) -> Self {
620 self.show_close_button = show;
621 self
622 }
623
624 pub fn show_suppress_button(mut self, show: bool) -> Self {
625 self.show_suppress_button = show;
626 self
627 }
628
629 pub fn with_title<S>(mut self, title: S) -> Self
630 where
631 S: Into<SharedString>,
632 {
633 self.title = Some(title.into());
634 self
635 }
636 }
637
638 impl Render for MessageNotification {
639 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
640 v_flex()
641 .occlude()
642 .p_3()
643 .gap_2()
644 .elevation_3(cx)
645 .child(
646 h_flex()
647 .gap_4()
648 .justify_between()
649 .items_start()
650 .child(
651 v_flex()
652 .gap_0p5()
653 .when_some(self.title.clone(), |element, title| {
654 element.child(Label::new(title))
655 })
656 .child(div().max_w_96().child((self.build_content)(window, cx))),
657 )
658 .child(
659 h_flex()
660 .gap_2()
661 .when(self.show_suppress_button, |this| {
662 this.child(
663 IconButton::new("suppress", IconName::SquareMinus)
664 .tooltip(|window, cx| {
665 Tooltip::for_action(
666 "Do not show until restart",
667 &SuppressNotification,
668 window,
669 cx,
670 )
671 })
672 .on_click(cx.listener(|_, _, _, cx| {
673 cx.emit(SuppressEvent);
674 })),
675 )
676 })
677 .when(self.show_close_button, |this| {
678 this.child(
679 IconButton::new("close", IconName::Close)
680 .tooltip(|window, cx| {
681 Tooltip::for_action(
682 "Close",
683 &menu::Cancel,
684 window,
685 cx,
686 )
687 })
688 .on_click(
689 cx.listener(|this, _, _, cx| this.dismiss(cx)),
690 ),
691 )
692 }),
693 ),
694 )
695 .child(
696 h_flex()
697 .gap_1()
698 .children(self.primary_message.iter().map(|message| {
699 let mut button = Button::new(message.clone(), message.clone())
700 .label_size(LabelSize::Small)
701 .on_click(cx.listener(|this, _, window, cx| {
702 if let Some(on_click) = this.primary_on_click.as_ref() {
703 (on_click)(window, cx)
704 };
705 this.dismiss(cx)
706 }));
707
708 if let Some(icon) = self.primary_icon {
709 button = button
710 .icon(icon)
711 .icon_color(self.primary_icon_color.unwrap_or(Color::Muted))
712 .icon_position(IconPosition::Start)
713 .icon_size(IconSize::Small);
714 }
715
716 button
717 }))
718 .children(self.secondary_message.iter().map(|message| {
719 let mut button = Button::new(message.clone(), message.clone())
720 .label_size(LabelSize::Small)
721 .on_click(cx.listener(|this, _, window, cx| {
722 if let Some(on_click) = this.secondary_on_click.as_ref() {
723 (on_click)(window, cx)
724 };
725 this.dismiss(cx)
726 }));
727
728 if let Some(icon) = self.secondary_icon {
729 button = button
730 .icon(icon)
731 .icon_position(IconPosition::Start)
732 .icon_size(IconSize::Small)
733 .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted));
734 }
735
736 button
737 }))
738 .child(
739 h_flex().w_full().justify_end().children(
740 self.more_info_message
741 .iter()
742 .zip(self.more_info_url.iter())
743 .map(|(message, url)| {
744 let url = url.clone();
745 Button::new(message.clone(), message.clone())
746 .label_size(LabelSize::Small)
747 .icon(IconName::ArrowUpRight)
748 .icon_size(IconSize::Indicator)
749 .icon_color(Color::Muted)
750 .on_click(cx.listener(move |_, _, _, cx| {
751 cx.open_url(&url);
752 }))
753 }),
754 ),
755 ),
756 )
757 }
758 }
759}
760
761static GLOBAL_APP_NOTIFICATIONS: LazyLock<Mutex<AppNotifications>> = LazyLock::new(|| {
762 Mutex::new(AppNotifications {
763 app_notifications: Vec::new(),
764 })
765});
766
767/// Stores app notifications so that they can be shown in new workspaces.
768struct AppNotifications {
769 app_notifications: Vec<(
770 NotificationId,
771 Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
772 )>,
773}
774
775impl AppNotifications {
776 pub fn insert(
777 &mut self,
778 id: NotificationId,
779 build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
780 ) {
781 self.remove(&id);
782 self.app_notifications.push((id, build_notification))
783 }
784
785 pub fn remove(&mut self, id: &NotificationId) {
786 self.app_notifications
787 .retain(|(existing_id, _)| existing_id != id);
788 }
789}
790
791/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
792/// is particularly to handle notifications that occur on initialization before any workspaces
793/// exist. If the notification is dismissed within any workspace, it will be removed from all.
794pub fn show_app_notification<V: Notification + 'static>(
795 id: NotificationId,
796 cx: &mut App,
797 build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static + Send + Sync,
798) {
799 // Defer notification creation so that windows on the stack can be returned to GPUI
800 cx.defer(move |cx| {
801 // Handle dismiss events by removing the notification from all workspaces.
802 let build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync> =
803 Arc::new({
804 let id = id.clone();
805 move |cx| {
806 let notification = build_notification(cx);
807 cx.subscribe(¬ification, {
808 let id = id.clone();
809 move |_, _, _: &DismissEvent, cx| {
810 dismiss_app_notification(&id, cx);
811 }
812 })
813 .detach();
814 cx.subscribe(¬ification, {
815 let id = id.clone();
816 move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
817 workspace.suppress_notification(&id, cx);
818 }
819 })
820 .detach();
821 notification.into()
822 }
823 });
824
825 // Store the notification so that new workspaces also receive it.
826 GLOBAL_APP_NOTIFICATIONS
827 .lock()
828 .insert(id.clone(), build_notification.clone());
829
830 for window in cx.windows() {
831 if let Some(workspace_window) = window.downcast::<Workspace>() {
832 workspace_window
833 .update(cx, |workspace, _window, cx| {
834 workspace.show_notification_without_handling_dismiss_events(
835 &id,
836 cx,
837 |cx| build_notification(cx),
838 );
839 })
840 .ok(); // Doesn't matter if the windows are dropped
841 }
842 }
843 });
844}
845
846pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
847 let id = id.clone();
848 // Defer notification dismissal so that windows on the stack can be returned to GPUI
849 cx.defer(move |cx| {
850 GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
851 for window in cx.windows() {
852 if let Some(workspace_window) = window.downcast::<Workspace>() {
853 let id = id.clone();
854 workspace_window
855 .update(cx, |workspace, _window, cx| {
856 workspace.dismiss_notification(&id, cx)
857 })
858 .ok();
859 }
860 }
861 });
862}
863
864pub trait NotifyResultExt {
865 type Ok;
866
867 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
868 -> Option<Self::Ok>;
869
870 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
871
872 /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
873 fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
874}
875
876impl<T, E> NotifyResultExt for std::result::Result<T, E>
877where
878 E: std::fmt::Debug + std::fmt::Display,
879{
880 type Ok = T;
881
882 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>) -> Option<T> {
883 match self {
884 Ok(value) => Some(value),
885 Err(err) => {
886 log::error!("Showing error notification in workspace: {err:?}");
887 workspace.show_error(&err, cx);
888 None
889 }
890 }
891 }
892
893 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
894 match self {
895 Ok(value) => Some(value),
896 Err(err) => {
897 log::error!("{err:?}");
898 cx.update_root(|view, _, cx| {
899 if let Ok(workspace) = view.downcast::<Workspace>() {
900 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
901 }
902 })
903 .ok();
904 None
905 }
906 }
907 }
908
909 fn notify_app_err(self, cx: &mut App) -> Option<T> {
910 match self {
911 Ok(value) => Some(value),
912 Err(err) => {
913 let message: SharedString = format!("Error: {err}").into();
914 log::error!("Showing error notification in app: {message}");
915 show_app_notification(workspace_error_notification_id(), cx, {
916 let message = message.clone();
917 move |cx| {
918 cx.new({
919 let message = message.clone();
920 move |cx| ErrorMessagePrompt::new(message, cx)
921 })
922 }
923 });
924
925 None
926 }
927 }
928 }
929}
930
931pub trait NotifyTaskExt {
932 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
933}
934
935impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
936where
937 E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
938 R: 'static,
939{
940 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
941 window
942 .spawn(cx, async move |mut cx| self.await.notify_async_err(&mut cx))
943 .detach();
944 }
945}
946
947pub trait DetachAndPromptErr<R> {
948 fn prompt_err(
949 self,
950 msg: &str,
951 window: &Window,
952 cx: &App,
953 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
954 ) -> Task<Option<R>>;
955
956 fn detach_and_prompt_err(
957 self,
958 msg: &str,
959 window: &Window,
960 cx: &App,
961 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
962 );
963}
964
965impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
966where
967 R: 'static,
968{
969 fn prompt_err(
970 self,
971 msg: &str,
972 window: &Window,
973 cx: &App,
974 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
975 ) -> Task<Option<R>> {
976 let msg = msg.to_owned();
977 window.spawn(cx, async move |cx| {
978 let result = self.await;
979 if let Err(err) = result.as_ref() {
980 log::error!("{err:?}");
981 if let Ok(prompt) = cx.update(|window, cx| {
982 let detail =
983 f(err, window, cx).unwrap_or_else(|| format!("{err}. Please try again."));
984 window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx)
985 }) {
986 prompt.await.ok();
987 }
988 return None;
989 }
990 Some(result.unwrap())
991 })
992 }
993
994 fn detach_and_prompt_err(
995 self,
996 msg: &str,
997 window: &Window,
998 cx: &App,
999 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1000 ) {
1001 self.prompt_err(msg, window, cx, f).detach();
1002 }
1003}