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(¬ification, 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}