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