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            .p_2()
217            .gap_2()
218            .w_full()
219            .child(
220                v_flex()
221                    .overflow_hidden()
222                    .child(
223                        h_flex()
224                            .children(
225                                match request.level {
226                                    PromptLevel::Info => None,
227                                    PromptLevel::Warning => Some(DiagnosticSeverity::WARNING),
228                                    PromptLevel::Critical => Some(DiagnosticSeverity::ERROR),
229                                }
230                                .map(|severity| {
231                                    svg()
232                                        .size(cx.text_style().font_size)
233                                        .flex_none()
234                                        .mr_1()
235                                        .map(|icon| {
236                                            if severity == DiagnosticSeverity::ERROR {
237                                                icon.path(IconName::ExclamationTriangle.path())
238                                                    .text_color(Color::Error.color(cx))
239                                            } else {
240                                                icon.path(IconName::ExclamationTriangle.path())
241                                                    .text_color(Color::Warning.color(cx))
242                                            }
243                                        })
244                                }),
245                            )
246                            .child(
247                                Label::new(format!("{}:", request.lsp_name))
248                                    .size(LabelSize::Default),
249                            ),
250                    )
251                    .child(Label::new(request.message.to_string()))
252                    .children(request.actions.iter().enumerate().map(|(ix, action)| {
253                        let this_handle = cx.view().clone();
254                        ui::Button::new(ix, action.title.clone())
255                            .size(ButtonSize::Large)
256                            .on_click(move |_, cx| {
257                                let this_handle = this_handle.clone();
258                                cx.spawn(|cx| async move {
259                                    LanguageServerPrompt::select_option(this_handle, ix, cx).await
260                                })
261                                .detach()
262                            })
263                    })),
264            )
265            .child(
266                ui::IconButton::new("close", ui::IconName::Close)
267                    .on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
268            )
269    }
270}
271
272impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
273
274pub mod simple_message_notification {
275    use gpui::{
276        div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
277        StatefulInteractiveElement, Styled, ViewContext,
278    };
279    use std::sync::Arc;
280    use ui::prelude::*;
281    use ui::{h_flex, v_flex, Button, Icon, IconName, Label, StyledExt};
282
283    pub struct MessageNotification {
284        message: SharedString,
285        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
286        click_message: Option<SharedString>,
287    }
288
289    impl EventEmitter<DismissEvent> for MessageNotification {}
290
291    impl MessageNotification {
292        pub fn new<S>(message: S) -> MessageNotification
293        where
294            S: Into<SharedString>,
295        {
296            Self {
297                message: message.into(),
298                on_click: None,
299                click_message: None,
300            }
301        }
302
303        pub fn with_click_message<S>(mut self, message: S) -> Self
304        where
305            S: Into<SharedString>,
306        {
307            self.click_message = Some(message.into());
308            self
309        }
310
311        pub fn on_click<F>(mut self, on_click: F) -> Self
312        where
313            F: 'static + Fn(&mut ViewContext<Self>),
314        {
315            self.on_click = Some(Arc::new(on_click));
316            self
317        }
318
319        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
320            cx.emit(DismissEvent);
321        }
322    }
323
324    impl Render for MessageNotification {
325        fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
326            v_flex()
327                .elevation_3(cx)
328                .p_4()
329                .child(
330                    h_flex()
331                        .justify_between()
332                        .child(div().max_w_80().child(Label::new(self.message.clone())))
333                        .child(
334                            div()
335                                .id("cancel")
336                                .child(Icon::new(IconName::Close))
337                                .cursor_pointer()
338                                .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
339                        ),
340                )
341                .children(self.click_message.iter().map(|message| {
342                    Button::new(message.clone(), message.clone()).on_click(cx.listener(
343                        |this, _, cx| {
344                            if let Some(on_click) = this.on_click.as_ref() {
345                                (on_click)(cx)
346                            };
347                            this.dismiss(cx)
348                        },
349                    ))
350                }))
351        }
352    }
353}
354
355pub trait NotifyResultExt {
356    type Ok;
357
358    fn notify_err(
359        self,
360        workspace: &mut Workspace,
361        cx: &mut ViewContext<Workspace>,
362    ) -> Option<Self::Ok>;
363
364    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
365}
366
367impl<T, E> NotifyResultExt for Result<T, E>
368where
369    E: std::fmt::Debug,
370{
371    type Ok = T;
372
373    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
374        match self {
375            Ok(value) => Some(value),
376            Err(err) => {
377                log::error!("TODO {err:?}");
378                workspace.show_error(&err, cx);
379                None
380            }
381        }
382    }
383
384    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
385        match self {
386            Ok(value) => Some(value),
387            Err(err) => {
388                log::error!("TODO {err:?}");
389                cx.update_root(|view, cx| {
390                    if let Ok(workspace) = view.downcast::<Workspace>() {
391                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
392                    }
393                })
394                .ok();
395                None
396            }
397        }
398    }
399}
400
401pub trait NotifyTaskExt {
402    fn detach_and_notify_err(self, cx: &mut WindowContext);
403}
404
405impl<R, E> NotifyTaskExt for Task<Result<R, E>>
406where
407    E: std::fmt::Debug + Sized + 'static,
408    R: 'static,
409{
410    fn detach_and_notify_err(self, cx: &mut WindowContext) {
411        cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
412            .detach();
413    }
414}
415
416pub trait DetachAndPromptErr {
417    fn detach_and_prompt_err(
418        self,
419        msg: &str,
420        cx: &mut WindowContext,
421        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
422    );
423}
424
425impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
426where
427    R: 'static,
428{
429    fn detach_and_prompt_err(
430        self,
431        msg: &str,
432        cx: &mut WindowContext,
433        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
434    ) {
435        let msg = msg.to_owned();
436        cx.spawn(|mut cx| async move {
437            if let Err(err) = self.await {
438                log::error!("{err:?}");
439                if let Ok(prompt) = cx.update(|cx| {
440                    let detail = f(&err, cx)
441                        .unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
442                    cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
443                }) {
444                    prompt.await.ok();
445                }
446            }
447        })
448        .detach();
449    }
450}