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    }
289
290    impl EventEmitter<DismissEvent> for MessageNotification {}
291
292    impl MessageNotification {
293        pub fn new<S>(message: S) -> MessageNotification
294        where
295            S: Into<SharedString>,
296        {
297            Self {
298                message: message.into(),
299                on_click: None,
300                click_message: None,
301            }
302        }
303
304        pub fn with_click_message<S>(mut self, message: S) -> Self
305        where
306            S: Into<SharedString>,
307        {
308            self.click_message = Some(message.into());
309            self
310        }
311
312        pub fn on_click<F>(mut self, on_click: F) -> Self
313        where
314            F: 'static + Fn(&mut ViewContext<Self>),
315        {
316            self.on_click = Some(Arc::new(on_click));
317            self
318        }
319
320        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
321            cx.emit(DismissEvent);
322        }
323    }
324
325    impl Render for MessageNotification {
326        fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
327            v_flex()
328                .elevation_3(cx)
329                .p_4()
330                .child(
331                    h_flex()
332                        .justify_between()
333                        .child(div().max_w_80().child(Label::new(self.message.clone())))
334                        .child(
335                            div()
336                                .id("cancel")
337                                .child(Icon::new(IconName::Close))
338                                .cursor_pointer()
339                                .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
340                        ),
341                )
342                .children(self.click_message.iter().map(|message| {
343                    Button::new(message.clone(), message.clone()).on_click(cx.listener(
344                        |this, _, cx| {
345                            if let Some(on_click) = this.on_click.as_ref() {
346                                (on_click)(cx)
347                            };
348                            this.dismiss(cx)
349                        },
350                    ))
351                }))
352        }
353    }
354}
355
356pub trait NotifyResultExt {
357    type Ok;
358
359    fn notify_err(
360        self,
361        workspace: &mut Workspace,
362        cx: &mut ViewContext<Workspace>,
363    ) -> Option<Self::Ok>;
364
365    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
366}
367
368impl<T, E> NotifyResultExt for Result<T, E>
369where
370    E: std::fmt::Debug,
371{
372    type Ok = T;
373
374    fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
375        match self {
376            Ok(value) => Some(value),
377            Err(err) => {
378                log::error!("TODO {err:?}");
379                workspace.show_error(&err, cx);
380                None
381            }
382        }
383    }
384
385    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
386        match self {
387            Ok(value) => Some(value),
388            Err(err) => {
389                log::error!("TODO {err:?}");
390                cx.update_root(|view, cx| {
391                    if let Ok(workspace) = view.downcast::<Workspace>() {
392                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
393                    }
394                })
395                .ok();
396                None
397            }
398        }
399    }
400}
401
402pub trait NotifyTaskExt {
403    fn detach_and_notify_err(self, cx: &mut WindowContext);
404}
405
406impl<R, E> NotifyTaskExt for Task<Result<R, E>>
407where
408    E: std::fmt::Debug + Sized + 'static,
409    R: 'static,
410{
411    fn detach_and_notify_err(self, cx: &mut WindowContext) {
412        cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
413            .detach();
414    }
415}
416
417pub trait DetachAndPromptErr {
418    fn detach_and_prompt_err(
419        self,
420        msg: &str,
421        cx: &mut WindowContext,
422        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
423    );
424}
425
426impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
427where
428    R: 'static,
429{
430    fn detach_and_prompt_err(
431        self,
432        msg: &str,
433        cx: &mut WindowContext,
434        f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
435    ) {
436        let msg = msg.to_owned();
437        cx.spawn(|mut cx| async move {
438            if let Err(err) = self.await {
439                log::error!("{err:?}");
440                if let Ok(prompt) = cx.update(|cx| {
441                    let detail = f(&err, cx)
442                        .unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
443                    cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
444                }) {
445                    prompt.await.ok();
446                }
447            }
448        })
449        .detach();
450    }
451}