notifications.rs

  1use crate::{Toast, Workspace};
  2use collections::HashMap;
  3use gpui::{
  4    svg, AnyView, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, Entity, EntityId,
  5    EventEmitter, Global, PromptLevel, Render, ScrollHandle, Task, View, ViewContext,
  6    VisualContext, WindowContext,
  7};
  8use language::DiagnosticSeverity;
  9
 10use std::{any::TypeId, ops::DerefMut};
 11use ui::{prelude::*, Tooltip};
 12use util::ResultExt;
 13
 14pub fn init(cx: &mut AppContext) {
 15    cx.set_global(NotificationTracker::new());
 16}
 17
 18pub trait Notification: EventEmitter<DismissEvent> + Render {}
 19
 20impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
 21
 22pub trait NotificationHandle: Send {
 23    fn id(&self) -> EntityId;
 24    fn to_any(&self) -> AnyView;
 25}
 26
 27impl<T: Notification> NotificationHandle for View<T> {
 28    fn id(&self) -> EntityId {
 29        self.entity_id()
 30    }
 31
 32    fn to_any(&self) -> AnyView {
 33        self.clone().into()
 34    }
 35}
 36
 37impl From<&dyn NotificationHandle> for AnyView {
 38    fn from(val: &dyn NotificationHandle) -> Self {
 39        val.to_any()
 40    }
 41}
 42
 43pub(crate) struct NotificationTracker {
 44    notifications_sent: HashMap<TypeId, Vec<usize>>,
 45}
 46
 47impl Global for NotificationTracker {}
 48
 49impl std::ops::Deref for NotificationTracker {
 50    type Target = HashMap<TypeId, Vec<usize>>;
 51
 52    fn deref(&self) -> &Self::Target {
 53        &self.notifications_sent
 54    }
 55}
 56
 57impl DerefMut for NotificationTracker {
 58    fn deref_mut(&mut self) -> &mut Self::Target {
 59        &mut self.notifications_sent
 60    }
 61}
 62
 63impl NotificationTracker {
 64    fn new() -> Self {
 65        Self {
 66            notifications_sent: Default::default(),
 67        }
 68    }
 69}
 70
 71impl Workspace {
 72    pub fn has_shown_notification_once<V: Notification>(
 73        &self,
 74        id: usize,
 75        cx: &ViewContext<Self>,
 76    ) -> bool {
 77        cx.global::<NotificationTracker>()
 78            .get(&TypeId::of::<V>())
 79            .map(|ids| ids.contains(&id))
 80            .unwrap_or(false)
 81    }
 82
 83    pub fn show_notification_once<V: Notification>(
 84        &mut self,
 85        id: usize,
 86        cx: &mut ViewContext<Self>,
 87        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
 88    ) {
 89        if !self.has_shown_notification_once::<V>(id, cx) {
 90            let tracker = cx.global_mut::<NotificationTracker>();
 91            let entry = tracker.entry(TypeId::of::<V>()).or_default();
 92            entry.push(id);
 93            self.show_notification::<V>(id, cx, build_notification)
 94        }
 95    }
 96
 97    pub fn show_notification<V: Notification>(
 98        &mut self,
 99        id: usize,
100        cx: &mut ViewContext<Self>,
101        build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
102    ) {
103        let type_id = TypeId::of::<V>();
104        self.dismiss_notification_internal(type_id, id, cx);
105
106        let notification = build_notification(cx);
107        cx.subscribe(&notification, move |this, _, _: &DismissEvent, cx| {
108            this.dismiss_notification_internal(type_id, id, cx);
109        })
110        .detach();
111        self.notifications
112            .push((type_id, id, Box::new(notification)));
113        cx.notify();
114    }
115
116    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
117    where
118        E: std::fmt::Debug,
119    {
120        self.show_notification(0, cx, |cx| {
121            cx.new_view(|_cx| {
122                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
123            })
124        });
125    }
126
127    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
128        let type_id = TypeId::of::<V>();
129
130        self.dismiss_notification_internal(type_id, id, cx)
131    }
132
133    pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
134        self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
135        self.show_notification(toast.id, cx, |cx| {
136            cx.new_view(|_cx| match toast.on_click.as_ref() {
137                Some((click_msg, on_click)) => {
138                    let on_click = on_click.clone();
139                    simple_message_notification::MessageNotification::new(toast.msg.clone())
140                        .with_click_message(click_msg.clone())
141                        .on_click(move |cx| on_click(cx))
142                }
143                None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
144            })
145        })
146    }
147
148    pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
149        self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
150    }
151
152    fn dismiss_notification_internal(
153        &mut self,
154        type_id: TypeId,
155        id: usize,
156        cx: &mut ViewContext<Self>,
157    ) {
158        self.notifications
159            .retain(|(existing_type_id, existing_id, _)| {
160                if (*existing_type_id, *existing_id) == (type_id, id) {
161                    cx.notify();
162                    false
163                } else {
164                    true
165                }
166            });
167    }
168}
169
170pub struct LanguageServerPrompt {
171    request: Option<project::LanguageServerPromptRequest>,
172    scroll_handle: ScrollHandle,
173}
174
175impl LanguageServerPrompt {
176    pub fn new(request: project::LanguageServerPromptRequest) -> Self {
177        Self {
178            request: Some(request),
179            scroll_handle: ScrollHandle::new(),
180        }
181    }
182
183    async fn select_option(this: View<Self>, ix: usize, mut cx: AsyncWindowContext) {
184        util::maybe!(async move {
185            let potential_future = this.update(&mut cx, |this, _| {
186                this.request.take().map(|request| request.respond(ix))
187            });
188
189            potential_future? // App Closed
190                .ok_or_else(|| anyhow::anyhow!("Response already sent"))?
191                .await
192                .ok_or_else(|| anyhow::anyhow!("Stream already closed"))?;
193
194            this.update(&mut cx, |_, cx| cx.emit(DismissEvent))?;
195
196            anyhow::Ok(())
197        })
198        .await
199        .log_err();
200    }
201}
202
203impl Render for LanguageServerPrompt {
204    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
205        let Some(request) = &self.request else {
206            return div().id("language_server_prompt_notification");
207        };
208
209        h_flex()
210            .id("language_server_prompt_notification")
211            .occlude()
212            .elevation_3(cx)
213            .items_start()
214            .justify_between()
215            .p_2()
216            .gap_2()
217            .w_full()
218            .max_h(vh(0.8, cx))
219            .overflow_y_scroll()
220            .track_scroll(&self.scroll_handle)
221            .group("")
222            .child(
223                v_flex()
224                    .w_full()
225                    .overflow_hidden()
226                    .child(
227                        h_flex()
228                            .w_full()
229                            .justify_between()
230                            .child(
231                                h_flex()
232                                    .flex_grow()
233                                    .children(
234                                        match request.level {
235                                            PromptLevel::Info => None,
236                                            PromptLevel::Warning => {
237                                                Some(DiagnosticSeverity::WARNING)
238                                            }
239                                            PromptLevel::Critical => {
240                                                Some(DiagnosticSeverity::ERROR)
241                                            }
242                                        }
243                                        .map(|severity| {
244                                            svg()
245                                                .size(cx.text_style().font_size)
246                                                .flex_none()
247                                                .mr_1()
248                                                .mt(px(-2.0))
249                                                .map(|icon| {
250                                                    if severity == DiagnosticSeverity::ERROR {
251                                                        icon.path(
252                                                            IconName::ExclamationTriangle.path(),
253                                                        )
254                                                        .text_color(Color::Error.color(cx))
255                                                    } else {
256                                                        icon.path(
257                                                            IconName::ExclamationTriangle.path(),
258                                                        )
259                                                        .text_color(Color::Warning.color(cx))
260                                                    }
261                                                })
262                                        }),
263                                    )
264                                    .child(
265                                        Label::new(request.lsp_name.clone())
266                                            .size(LabelSize::Default),
267                                    ),
268                            )
269                            .child(
270                                ui::IconButton::new("close", ui::IconName::Close)
271                                    .on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
272                            ),
273                    )
274                    .child(
275                        v_flex()
276                            .child(
277                                h_flex().absolute().right_0().rounded_md().child(
278                                    ui::IconButton::new("copy", ui::IconName::Copy)
279                                        .on_click({
280                                            let message = request.message.clone();
281                                            move |_, cx| {
282                                                cx.write_to_clipboard(ClipboardItem::new(
283                                                    message.clone(),
284                                                ))
285                                            }
286                                        })
287                                        .tooltip(|cx| Tooltip::text("Copy", cx))
288                                        .visible_on_hover(""),
289                                ),
290                            )
291                            .child(Label::new(request.message.to_string()).size(LabelSize::Small)),
292                    )
293                    .children(request.actions.iter().enumerate().map(|(ix, action)| {
294                        let this_handle = cx.view().clone();
295                        ui::Button::new(ix, action.title.clone())
296                            .size(ButtonSize::Large)
297                            .on_click(move |_, cx| {
298                                let this_handle = this_handle.clone();
299                                cx.spawn(|cx| async move {
300                                    LanguageServerPrompt::select_option(this_handle, ix, cx).await
301                                })
302                                .detach()
303                            })
304                    })),
305            )
306    }
307}
308
309impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
310
311pub mod simple_message_notification {
312    use gpui::{
313        div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
314        StatefulInteractiveElement, Styled, ViewContext,
315    };
316    use std::sync::Arc;
317    use ui::prelude::*;
318    use ui::{h_flex, v_flex, Button, Icon, IconName, Label, StyledExt};
319
320    pub struct MessageNotification {
321        message: SharedString,
322        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
323        click_message: Option<SharedString>,
324        secondary_click_message: Option<SharedString>,
325        secondary_on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
326    }
327
328    impl EventEmitter<DismissEvent> for MessageNotification {}
329
330    impl MessageNotification {
331        pub fn new<S>(message: S) -> MessageNotification
332        where
333            S: Into<SharedString>,
334        {
335            Self {
336                message: message.into(),
337                on_click: None,
338                click_message: None,
339                secondary_on_click: None,
340                secondary_click_message: None,
341            }
342        }
343
344        pub fn with_click_message<S>(mut self, message: S) -> Self
345        where
346            S: Into<SharedString>,
347        {
348            self.click_message = Some(message.into());
349            self
350        }
351
352        pub fn on_click<F>(mut self, on_click: F) -> Self
353        where
354            F: 'static + Fn(&mut ViewContext<Self>),
355        {
356            self.on_click = Some(Arc::new(on_click));
357            self
358        }
359
360        pub fn with_secondary_click_message<S>(mut self, message: S) -> Self
361        where
362            S: Into<SharedString>,
363        {
364            self.secondary_click_message = Some(message.into());
365            self
366        }
367
368        pub fn on_secondary_click<F>(mut self, on_click: F) -> Self
369        where
370            F: 'static + Fn(&mut ViewContext<Self>),
371        {
372            self.secondary_on_click = Some(Arc::new(on_click));
373            self
374        }
375
376        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
377            cx.emit(DismissEvent);
378        }
379    }
380
381    impl Render for MessageNotification {
382        fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
383            v_flex()
384                .elevation_3(cx)
385                .p_4()
386                .child(
387                    h_flex()
388                        .justify_between()
389                        .child(div().max_w_80().child(Label::new(self.message.clone())))
390                        .child(
391                            div()
392                                .id("cancel")
393                                .child(Icon::new(IconName::Close))
394                                .cursor_pointer()
395                                .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
396                        ),
397                )
398                .child(
399                    h_flex()
400                        .gap_3()
401                        .children(self.click_message.iter().map(|message| {
402                            Button::new(message.clone(), message.clone()).on_click(cx.listener(
403                                |this, _, cx| {
404                                    if let Some(on_click) = this.on_click.as_ref() {
405                                        (on_click)(cx)
406                                    };
407                                    this.dismiss(cx)
408                                },
409                            ))
410                        }))
411                        .children(self.secondary_click_message.iter().map(|message| {
412                            Button::new(message.clone(), message.clone())
413                                .style(ButtonStyle::Filled)
414                                .on_click(cx.listener(|this, _, cx| {
415                                    if let Some(on_click) = this.secondary_on_click.as_ref() {
416                                        (on_click)(cx)
417                                    };
418                                    this.dismiss(cx)
419                                }))
420                        })),
421                )
422        }
423    }
424}
425
426pub trait NotifyResultExt {
427    type Ok;
428
429    fn notify_err(
430        self,
431        workspace: &mut Workspace,
432        cx: &mut ViewContext<Workspace>,
433    ) -> Option<Self::Ok>;
434
435    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
436}
437
438impl<T, E> NotifyResultExt for Result<T, E>
439where
440    E: std::fmt::Debug,
441{
442    type Ok = T;
443
444    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
445        match self {
446            Ok(value) => Some(value),
447            Err(err) => {
448                log::error!("TODO {err:?}");
449                workspace.show_error(&err, cx);
450                None
451            }
452        }
453    }
454
455    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
456        match self {
457            Ok(value) => Some(value),
458            Err(err) => {
459                log::error!("TODO {err:?}");
460                cx.update_root(|view, cx| {
461                    if let Ok(workspace) = view.downcast::<Workspace>() {
462                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
463                    }
464                })
465                .ok();
466                None
467            }
468        }
469    }
470}
471
472pub trait NotifyTaskExt {
473    fn detach_and_notify_err(self, cx: &mut WindowContext);
474}
475
476impl<R, E> NotifyTaskExt for Task<Result<R, E>>
477where
478    E: std::fmt::Debug + Sized + 'static,
479    R: 'static,
480{
481    fn detach_and_notify_err(self, cx: &mut WindowContext) {
482        cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
483            .detach();
484    }
485}
486
487pub trait DetachAndPromptErr {
488    fn detach_and_prompt_err(
489        self,
490        msg: &str,
491        cx: &mut WindowContext,
492        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
493    );
494}
495
496impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
497where
498    R: 'static,
499{
500    fn detach_and_prompt_err(
501        self,
502        msg: &str,
503        cx: &mut WindowContext,
504        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
505    ) {
506        let msg = msg.to_owned();
507        cx.spawn(|mut cx| async move {
508            if let Err(err) = self.await {
509                log::error!("{err:?}");
510                if let Ok(prompt) = cx.update(|cx| {
511                    let detail = f(&err, cx)
512                        .unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
513                    cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
514                }) {
515                    prompt.await.ok();
516                }
517            }
518        })
519        .detach();
520    }
521}