1use crate::{Toast, Workspace};
2use anyhow::Context;
3use anyhow::{anyhow, Result};
4use collections::HashMap;
5use gpui::{
6 svg, AnyView, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, Entity, EntityId,
7 EventEmitter, Global, PromptLevel, Render, ScrollHandle, Task, View, ViewContext,
8 VisualContext, WindowContext,
9};
10use std::{any::TypeId, ops::DerefMut, time::Duration};
11use ui::{prelude::*, Tooltip};
12use util::ResultExt;
13
14pub fn init(cx: &mut AppContext) {
15 cx.set_global(NotificationTracker::new());
16}
17
18#[derive(Debug, PartialEq, Clone)]
19pub enum NotificationId {
20 Unique(TypeId),
21 Composite(TypeId, ElementId),
22 Named(SharedString),
23}
24
25impl NotificationId {
26 /// Returns a unique [`NotificationId`] for the given type.
27 pub fn unique<T: 'static>() -> Self {
28 Self::Unique(TypeId::of::<T>())
29 }
30
31 /// Returns a [`NotificationId`] for the given type that is also identified
32 /// by the provided ID.
33 pub fn composite<T: 'static>(id: impl Into<ElementId>) -> Self {
34 Self::Composite(TypeId::of::<T>(), id.into())
35 }
36
37 /// Builds a `NotificationId` out of the given string.
38 pub fn named(id: SharedString) -> Self {
39 Self::Named(id)
40 }
41}
42
43pub trait Notification: EventEmitter<DismissEvent> + Render {}
44
45impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
46
47pub trait NotificationHandle: Send {
48 fn id(&self) -> EntityId;
49 fn to_any(&self) -> AnyView;
50}
51
52impl<T: Notification> NotificationHandle for View<T> {
53 fn id(&self) -> EntityId {
54 self.entity_id()
55 }
56
57 fn to_any(&self) -> AnyView {
58 self.clone().into()
59 }
60}
61
62impl From<&dyn NotificationHandle> for AnyView {
63 fn from(val: &dyn NotificationHandle) -> Self {
64 val.to_any()
65 }
66}
67
68pub(crate) struct NotificationTracker {
69 notifications_sent: HashMap<TypeId, Vec<NotificationId>>,
70}
71
72impl Global for NotificationTracker {}
73
74impl std::ops::Deref for NotificationTracker {
75 type Target = HashMap<TypeId, Vec<NotificationId>>;
76
77 fn deref(&self) -> &Self::Target {
78 &self.notifications_sent
79 }
80}
81
82impl DerefMut for NotificationTracker {
83 fn deref_mut(&mut self) -> &mut Self::Target {
84 &mut self.notifications_sent
85 }
86}
87
88impl NotificationTracker {
89 fn new() -> Self {
90 Self {
91 notifications_sent: Default::default(),
92 }
93 }
94}
95
96impl Workspace {
97 pub fn has_shown_notification_once<V: Notification>(
98 &self,
99 id: &NotificationId,
100 cx: &ViewContext<Self>,
101 ) -> bool {
102 cx.global::<NotificationTracker>()
103 .get(&TypeId::of::<V>())
104 .map(|ids| ids.contains(id))
105 .unwrap_or(false)
106 }
107
108 pub fn show_notification_once<V: Notification>(
109 &mut self,
110 id: NotificationId,
111 cx: &mut ViewContext<Self>,
112 build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
113 ) {
114 if !self.has_shown_notification_once::<V>(&id, cx) {
115 let tracker = cx.global_mut::<NotificationTracker>();
116 let entry = tracker.entry(TypeId::of::<V>()).or_default();
117 entry.push(id.clone());
118 self.show_notification::<V>(id, cx, build_notification)
119 }
120 }
121
122 #[cfg(any(test, feature = "test-support"))]
123 pub fn notification_ids(&self) -> Vec<NotificationId> {
124 self.notifications
125 .iter()
126 .map(|(id, _)| id)
127 .cloned()
128 .collect()
129 }
130
131 pub fn show_notification<V: Notification>(
132 &mut self,
133 id: NotificationId,
134 cx: &mut ViewContext<Self>,
135 build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
136 ) {
137 self.dismiss_notification_internal(&id, cx);
138
139 let notification = build_notification(cx);
140 cx.subscribe(¬ification, {
141 let id = id.clone();
142 move |this, _, _: &DismissEvent, cx| {
143 this.dismiss_notification_internal(&id, cx);
144 }
145 })
146 .detach();
147 self.notifications.push((id, Box::new(notification)));
148 cx.notify();
149 }
150
151 pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
152 where
153 E: std::fmt::Debug + std::fmt::Display,
154 {
155 self.show_notification(workspace_error_notification_id(), cx, |cx| {
156 cx.new_view(|_cx| ErrorMessagePrompt::new(format!("Error: {err}")))
157 });
158 }
159
160 pub fn show_portal_error(&mut self, err: String, cx: &mut ViewContext<Self>) {
161 struct PortalError;
162
163 self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
164 cx.new_view(|_cx| {
165 ErrorMessagePrompt::new(err.to_string()).with_link_button(
166 "See docs",
167 "https://zed.dev/docs/linux#i-cant-open-any-files",
168 )
169 })
170 });
171 }
172
173 pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
174 self.dismiss_notification_internal(id, cx)
175 }
176
177 pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
178 self.dismiss_notification(&toast.id, cx);
179 self.show_notification(toast.id.clone(), cx, |cx| {
180 cx.new_view(|_cx| match toast.on_click.as_ref() {
181 Some((click_msg, on_click)) => {
182 let on_click = on_click.clone();
183 simple_message_notification::MessageNotification::new(toast.msg.clone())
184 .with_click_message(click_msg.clone())
185 .on_click(move |cx| on_click(cx))
186 }
187 None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
188 })
189 });
190 if toast.autohide {
191 cx.spawn(|workspace, mut cx| async move {
192 cx.background_executor()
193 .timer(Duration::from_millis(5000))
194 .await;
195 workspace
196 .update(&mut cx, |workspace, cx| {
197 workspace.dismiss_toast(&toast.id, cx)
198 })
199 .ok();
200 })
201 .detach();
202 }
203 }
204
205 pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
206 self.dismiss_notification(id, cx);
207 }
208
209 pub fn clear_all_notifications(&mut self, cx: &mut ViewContext<Self>) {
210 self.notifications.clear();
211 cx.notify();
212 }
213
214 fn dismiss_notification_internal(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
215 self.notifications.retain(|(existing_id, _)| {
216 if existing_id == id {
217 cx.notify();
218 false
219 } else {
220 true
221 }
222 });
223 }
224}
225
226pub struct LanguageServerPrompt {
227 request: Option<project::LanguageServerPromptRequest>,
228 scroll_handle: ScrollHandle,
229}
230
231impl LanguageServerPrompt {
232 pub fn new(request: project::LanguageServerPromptRequest) -> Self {
233 Self {
234 request: Some(request),
235 scroll_handle: ScrollHandle::new(),
236 }
237 }
238
239 async fn select_option(this: View<Self>, ix: usize, mut cx: AsyncWindowContext) {
240 util::maybe!(async move {
241 let potential_future = this.update(&mut cx, |this, _| {
242 this.request.take().map(|request| request.respond(ix))
243 });
244
245 potential_future? // App Closed
246 .ok_or_else(|| anyhow::anyhow!("Response already sent"))?
247 .await
248 .ok_or_else(|| anyhow::anyhow!("Stream already closed"))?;
249
250 this.update(&mut cx, |_, cx| cx.emit(DismissEvent))?;
251
252 anyhow::Ok(())
253 })
254 .await
255 .log_err();
256 }
257}
258
259impl Render for LanguageServerPrompt {
260 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
261 let Some(request) = &self.request else {
262 return div().id("language_server_prompt_notification");
263 };
264
265 let (icon, color) = match request.level {
266 PromptLevel::Info => (IconName::Info, Color::Accent),
267 PromptLevel::Warning => (IconName::Warning, Color::Warning),
268 PromptLevel::Critical => (IconName::XCircle, Color::Error),
269 };
270
271 div()
272 .id("language_server_prompt_notification")
273 .group("language_server_prompt_notification")
274 .occlude()
275 .w_full()
276 .max_h(vh(0.8, cx))
277 .elevation_3(cx)
278 .overflow_y_scroll()
279 .track_scroll(&self.scroll_handle)
280 .child(
281 v_flex()
282 .p_3()
283 .overflow_hidden()
284 .child(
285 h_flex()
286 .justify_between()
287 .items_start()
288 .child(
289 h_flex()
290 .gap_2()
291 .child(Icon::new(icon).color(color))
292 .child(Label::new(request.lsp_name.clone())),
293 )
294 .child(
295 h_flex()
296 .child(
297 IconButton::new("copy", IconName::Copy)
298 .on_click({
299 let message = request.message.clone();
300 move |_, cx| {
301 cx.write_to_clipboard(
302 ClipboardItem::new_string(message.clone()),
303 )
304 }
305 })
306 .tooltip(|cx| Tooltip::text("Copy Description", cx)),
307 )
308 .child(IconButton::new("close", IconName::Close).on_click(
309 cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent)),
310 )),
311 ),
312 )
313 .child(Label::new(request.message.to_string()).size(LabelSize::Small))
314 .children(request.actions.iter().enumerate().map(|(ix, action)| {
315 let this_handle = cx.view().clone();
316 Button::new(ix, action.title.clone())
317 .size(ButtonSize::Large)
318 .on_click(move |_, cx| {
319 let this_handle = this_handle.clone();
320 cx.spawn(|cx| async move {
321 LanguageServerPrompt::select_option(this_handle, ix, cx).await
322 })
323 .detach()
324 })
325 })),
326 )
327 }
328}
329
330impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
331
332fn workspace_error_notification_id() -> NotificationId {
333 struct WorkspaceErrorNotification;
334 NotificationId::unique::<WorkspaceErrorNotification>()
335}
336
337#[derive(Debug, Clone)]
338pub struct ErrorMessagePrompt {
339 message: SharedString,
340 label_and_url_button: Option<(SharedString, SharedString)>,
341}
342
343impl ErrorMessagePrompt {
344 pub fn new<S>(message: S) -> Self
345 where
346 S: Into<SharedString>,
347 {
348 Self {
349 message: message.into(),
350 label_and_url_button: None,
351 }
352 }
353
354 pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
355 where
356 S: Into<SharedString>,
357 {
358 self.label_and_url_button = Some((label.into(), url.into()));
359 self
360 }
361}
362
363impl Render for ErrorMessagePrompt {
364 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
365 h_flex()
366 .id("error_message_prompt_notification")
367 .occlude()
368 .elevation_3(cx)
369 .items_start()
370 .justify_between()
371 .p_2()
372 .gap_2()
373 .w_full()
374 .child(
375 v_flex()
376 .w_full()
377 .child(
378 h_flex()
379 .w_full()
380 .justify_between()
381 .child(
382 svg()
383 .size(cx.text_style().font_size)
384 .flex_none()
385 .mr_2()
386 .mt(px(-2.0))
387 .map(|icon| {
388 icon.path(IconName::Warning.path())
389 .text_color(Color::Error.color(cx))
390 }),
391 )
392 .child(
393 ui::IconButton::new("close", ui::IconName::Close)
394 .on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
395 ),
396 )
397 .child(
398 div()
399 .id("error_message")
400 .max_w_96()
401 .max_h_40()
402 .overflow_y_scroll()
403 .child(Label::new(self.message.clone()).size(LabelSize::Small)),
404 )
405 .when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
406 elm.child(
407 div().mt_2().child(
408 ui::Button::new("error_message_prompt_notification_button", label)
409 .on_click(move |_, cx| cx.open_url(&url)),
410 ),
411 )
412 }),
413 )
414 }
415}
416
417impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
418
419pub mod simple_message_notification {
420 use std::sync::Arc;
421
422 use gpui::{
423 div, AnyElement, DismissEvent, EventEmitter, ParentElement, Render, SharedString, Styled,
424 ViewContext,
425 };
426 use ui::prelude::*;
427
428 pub struct MessageNotification {
429 content: Box<dyn Fn(&mut ViewContext<Self>) -> AnyElement>,
430 on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
431 click_message: Option<SharedString>,
432 secondary_click_message: Option<SharedString>,
433 secondary_on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
434 }
435
436 impl EventEmitter<DismissEvent> for MessageNotification {}
437
438 impl MessageNotification {
439 pub fn new<S>(message: S) -> MessageNotification
440 where
441 S: Into<SharedString>,
442 {
443 let message = message.into();
444 Self::new_from_builder(move |_| Label::new(message.clone()).into_any_element())
445 }
446
447 pub fn new_from_builder<F>(content: F) -> MessageNotification
448 where
449 F: 'static + Fn(&mut ViewContext<Self>) -> AnyElement,
450 {
451 Self {
452 content: Box::new(content),
453 on_click: None,
454 click_message: None,
455 secondary_on_click: None,
456 secondary_click_message: None,
457 }
458 }
459
460 pub fn with_click_message<S>(mut self, message: S) -> Self
461 where
462 S: Into<SharedString>,
463 {
464 self.click_message = Some(message.into());
465 self
466 }
467
468 pub fn on_click<F>(mut self, on_click: F) -> Self
469 where
470 F: 'static + Fn(&mut ViewContext<Self>),
471 {
472 self.on_click = Some(Arc::new(on_click));
473 self
474 }
475
476 pub fn with_secondary_click_message<S>(mut self, message: S) -> Self
477 where
478 S: Into<SharedString>,
479 {
480 self.secondary_click_message = Some(message.into());
481 self
482 }
483
484 pub fn on_secondary_click<F>(mut self, on_click: F) -> Self
485 where
486 F: 'static + Fn(&mut ViewContext<Self>),
487 {
488 self.secondary_on_click = Some(Arc::new(on_click));
489 self
490 }
491
492 pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
493 cx.emit(DismissEvent);
494 }
495 }
496
497 impl Render for MessageNotification {
498 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
499 v_flex()
500 .p_3()
501 .gap_2()
502 .elevation_3(cx)
503 .child(
504 h_flex()
505 .gap_4()
506 .justify_between()
507 .items_start()
508 .child(div().max_w_96().child((self.content)(cx)))
509 .child(
510 IconButton::new("close", IconName::Close)
511 .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
512 ),
513 )
514 .child(
515 h_flex()
516 .gap_2()
517 .children(self.click_message.iter().map(|message| {
518 Button::new(message.clone(), message.clone())
519 .label_size(LabelSize::Small)
520 .icon(IconName::Check)
521 .icon_position(IconPosition::Start)
522 .icon_size(IconSize::Small)
523 .icon_color(Color::Success)
524 .on_click(cx.listener(|this, _, cx| {
525 if let Some(on_click) = this.on_click.as_ref() {
526 (on_click)(cx)
527 };
528 this.dismiss(cx)
529 }))
530 }))
531 .children(self.secondary_click_message.iter().map(|message| {
532 Button::new(message.clone(), message.clone())
533 .label_size(LabelSize::Small)
534 .icon(IconName::Close)
535 .icon_position(IconPosition::Start)
536 .icon_size(IconSize::Small)
537 .icon_color(Color::Error)
538 .on_click(cx.listener(|this, _, cx| {
539 if let Some(on_click) = this.secondary_on_click.as_ref() {
540 (on_click)(cx)
541 };
542 this.dismiss(cx)
543 }))
544 })),
545 )
546 }
547 }
548}
549
550/// Shows a notification in the active workspace if there is one, otherwise shows it in all workspaces.
551pub fn show_app_notification<V: Notification>(
552 id: NotificationId,
553 cx: &mut AppContext,
554 build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V>,
555) -> Result<()> {
556 let workspaces_to_notify = if let Some(active_workspace_window) = cx
557 .active_window()
558 .and_then(|window| window.downcast::<Workspace>())
559 {
560 vec![active_workspace_window]
561 } else {
562 let mut workspaces_to_notify = Vec::new();
563 for window in cx.windows() {
564 if let Some(workspace_window) = window.downcast::<Workspace>() {
565 workspaces_to_notify.push(workspace_window);
566 }
567 }
568 workspaces_to_notify
569 };
570
571 let mut notified = false;
572 let mut notify_errors = Vec::new();
573
574 for workspace_window in workspaces_to_notify {
575 let notify_result = workspace_window.update(cx, |workspace, cx| {
576 workspace.show_notification(id.clone(), cx, &build_notification);
577 });
578 match notify_result {
579 Ok(()) => notified = true,
580 Err(notify_err) => notify_errors.push(notify_err),
581 }
582 }
583
584 if notified {
585 Ok(())
586 } else {
587 if notify_errors.is_empty() {
588 Err(anyhow!("Found no workspaces to show notification."))
589 } else {
590 Err(anyhow!(
591 "No workspaces were able to show notification. Errors:\n\n{}",
592 notify_errors
593 .iter()
594 .map(|e| e.to_string())
595 .collect::<Vec<_>>()
596 .join("\n\n")
597 ))
598 }
599 }
600}
601
602pub fn dismiss_app_notification(id: &NotificationId, cx: &mut AppContext) {
603 for window in cx.windows() {
604 if let Some(workspace_window) = window.downcast::<Workspace>() {
605 workspace_window
606 .update(cx, |workspace, cx| {
607 workspace.dismiss_notification(&id, cx);
608 })
609 .ok();
610 }
611 }
612}
613
614pub trait NotifyResultExt {
615 type Ok;
616
617 fn notify_err(
618 self,
619 workspace: &mut Workspace,
620 cx: &mut ViewContext<Workspace>,
621 ) -> Option<Self::Ok>;
622
623 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
624
625 /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
626 fn notify_app_err(self, cx: &mut AppContext) -> Option<Self::Ok>;
627}
628
629impl<T, E> NotifyResultExt for std::result::Result<T, E>
630where
631 E: std::fmt::Debug + std::fmt::Display,
632{
633 type Ok = T;
634
635 fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
636 match self {
637 Ok(value) => Some(value),
638 Err(err) => {
639 log::error!("TODO {err:?}");
640 workspace.show_error(&err, cx);
641 None
642 }
643 }
644 }
645
646 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
647 match self {
648 Ok(value) => Some(value),
649 Err(err) => {
650 log::error!("{err:?}");
651 cx.update_root(|view, cx| {
652 if let Ok(workspace) = view.downcast::<Workspace>() {
653 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
654 }
655 })
656 .ok();
657 None
658 }
659 }
660 }
661
662 fn notify_app_err(self, cx: &mut AppContext) -> Option<T> {
663 match self {
664 Ok(value) => Some(value),
665 Err(err) => {
666 let message: SharedString = format!("Error: {err}").into();
667 show_app_notification(workspace_error_notification_id(), cx, |cx| {
668 cx.new_view(|_cx| ErrorMessagePrompt::new(message.clone()))
669 })
670 .with_context(|| format!("Showing error notification: {message}"))
671 .log_err();
672 None
673 }
674 }
675 }
676}
677
678pub trait NotifyTaskExt {
679 fn detach_and_notify_err(self, cx: &mut WindowContext);
680}
681
682impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
683where
684 E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
685 R: 'static,
686{
687 fn detach_and_notify_err(self, cx: &mut WindowContext) {
688 cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
689 .detach();
690 }
691}
692
693pub trait DetachAndPromptErr<R> {
694 fn prompt_err(
695 self,
696 msg: &str,
697 cx: &mut WindowContext,
698 f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
699 ) -> Task<Option<R>>;
700
701 fn detach_and_prompt_err(
702 self,
703 msg: &str,
704 cx: &mut WindowContext,
705 f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
706 );
707}
708
709impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
710where
711 R: 'static,
712{
713 fn prompt_err(
714 self,
715 msg: &str,
716 cx: &mut WindowContext,
717 f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
718 ) -> Task<Option<R>> {
719 let msg = msg.to_owned();
720 cx.spawn(|mut cx| async move {
721 let result = self.await;
722 if let Err(err) = result.as_ref() {
723 log::error!("{err:?}");
724 if let Ok(prompt) = cx.update(|cx| {
725 let detail = f(err, cx).unwrap_or_else(|| format!("{err}. Please try again."));
726 cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
727 }) {
728 prompt.await.ok();
729 }
730 return None;
731 }
732 Some(result.unwrap())
733 })
734 }
735
736 fn detach_and_prompt_err(
737 self,
738 msg: &str,
739 cx: &mut WindowContext,
740 f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
741 ) {
742 self.prompt_err(msg, cx, f).detach();
743 }
744}