notifications.rs

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