notifications.rs

  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(&notification, {
 83                let id = id.clone();
 84                move |this, _, _: &DismissEvent, cx| {
 85                    this.dismiss_notification(&id, cx);
 86                }
 87            })
 88            .detach();
 89            cx.subscribe(&notification, {
 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(&notification, {
808                        let id = id.clone();
809                        move |_, _, _: &DismissEvent, cx| {
810                            dismiss_app_notification(&id, cx);
811                        }
812                    })
813                    .detach();
814                    notification.into()
815                }
816            });
817
818        // Store the notification so that new workspaces also receive it.
819        GLOBAL_APP_NOTIFICATIONS
820            .lock()
821            .insert(id.clone(), build_notification.clone());
822
823        for window in cx.windows() {
824            if let Some(workspace_window) = window.downcast::<Workspace>() {
825                workspace_window
826                    .update(cx, |workspace, _window, cx| {
827                        workspace.show_notification_without_handling_dismiss_events(
828                            &id,
829                            cx,
830                            |cx| build_notification(cx),
831                        );
832                    })
833                    .ok(); // Doesn't matter if the windows are dropped
834            }
835        }
836    });
837}
838
839pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
840    let id = id.clone();
841    // Defer notification dismissal so that windows on the stack can be returned to GPUI
842    cx.defer(move |cx| {
843        GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
844        for window in cx.windows() {
845            if let Some(workspace_window) = window.downcast::<Workspace>() {
846                let id = id.clone();
847                workspace_window
848                    .update(cx, |workspace, _window, cx| {
849                        workspace.dismiss_notification(&id, cx)
850                    })
851                    .ok();
852            }
853        }
854    });
855}
856
857pub trait NotifyResultExt {
858    type Ok;
859
860    fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
861    -> Option<Self::Ok>;
862
863    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
864
865    /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
866    fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
867}
868
869impl<T, E> NotifyResultExt for std::result::Result<T, E>
870where
871    E: std::fmt::Debug + std::fmt::Display,
872{
873    type Ok = T;
874
875    fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>) -> Option<T> {
876        match self {
877            Ok(value) => Some(value),
878            Err(err) => {
879                log::error!("Showing error notification in workspace: {err:?}");
880                workspace.show_error(&err, cx);
881                None
882            }
883        }
884    }
885
886    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
887        match self {
888            Ok(value) => Some(value),
889            Err(err) => {
890                log::error!("{err:?}");
891                cx.update_root(|view, _, cx| {
892                    if let Ok(workspace) = view.downcast::<Workspace>() {
893                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
894                    }
895                })
896                .ok();
897                None
898            }
899        }
900    }
901
902    fn notify_app_err(self, cx: &mut App) -> Option<T> {
903        match self {
904            Ok(value) => Some(value),
905            Err(err) => {
906                let message: SharedString = format!("Error: {err}").into();
907                log::error!("Showing error notification in app: {message}");
908                show_app_notification(workspace_error_notification_id(), cx, {
909                    let message = message.clone();
910                    move |cx| {
911                        cx.new({
912                            let message = message.clone();
913                            move |cx| ErrorMessagePrompt::new(message, cx)
914                        })
915                    }
916                });
917
918                None
919            }
920        }
921    }
922}
923
924pub trait NotifyTaskExt {
925    fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
926}
927
928impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
929where
930    E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
931    R: 'static,
932{
933    fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
934        window
935            .spawn(cx, async move |mut cx| self.await.notify_async_err(&mut cx))
936            .detach();
937    }
938}
939
940pub trait DetachAndPromptErr<R> {
941    fn prompt_err(
942        self,
943        msg: &str,
944        window: &Window,
945        cx: &App,
946        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
947    ) -> Task<Option<R>>;
948
949    fn detach_and_prompt_err(
950        self,
951        msg: &str,
952        window: &Window,
953        cx: &App,
954        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
955    );
956}
957
958impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
959where
960    R: 'static,
961{
962    fn prompt_err(
963        self,
964        msg: &str,
965        window: &Window,
966        cx: &App,
967        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
968    ) -> Task<Option<R>> {
969        let msg = msg.to_owned();
970        window.spawn(cx, async move |cx| {
971            let result = self.await;
972            if let Err(err) = result.as_ref() {
973                log::error!("{err:?}");
974                if let Ok(prompt) = cx.update(|window, cx| {
975                    let detail =
976                        f(err, window, cx).unwrap_or_else(|| format!("{err}. Please try again."));
977                    window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx)
978                }) {
979                    prompt.await.ok();
980                }
981                return None;
982            }
983            Some(result.unwrap())
984        })
985    }
986
987    fn detach_and_prompt_err(
988        self,
989        msg: &str,
990        window: &Window,
991        cx: &App,
992        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
993    ) {
994        self.prompt_err(msg, window, cx, f).detach();
995    }
996}