activity_indicator.rs

  1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
  2use editor::Editor;
  3use extension::ExtensionStore;
  4use futures::StreamExt;
  5use gpui::{
  6    actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
  7    DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
  8    SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
  9    VisualContext as _,
 10};
 11use language::{
 12    LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
 13};
 14use project::{LanguageServerProgress, Project};
 15use smallvec::SmallVec;
 16use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
 17use ui::{prelude::*, ContextMenu};
 18use workspace::{item::ItemHandle, StatusItemView, Workspace};
 19
 20actions!(activity_indicator, [ShowErrorMessage]);
 21
 22pub enum Event {
 23    ShowError { lsp_name: Arc<str>, error: String },
 24}
 25
 26pub struct ActivityIndicator {
 27    statuses: Vec<LspStatus>,
 28    project: Model<Project>,
 29    auto_updater: Option<Model<AutoUpdater>>,
 30    context_menu: Option<View<ContextMenu>>,
 31}
 32
 33struct LspStatus {
 34    name: LanguageServerName,
 35    status: LanguageServerBinaryStatus,
 36}
 37
 38struct PendingWork<'a> {
 39    language_server_id: LanguageServerId,
 40    progress_token: &'a str,
 41    progress: &'a LanguageServerProgress,
 42}
 43
 44#[derive(Default)]
 45struct Content {
 46    icon: Option<gpui::AnyElement>,
 47    message: String,
 48    on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
 49}
 50
 51impl ActivityIndicator {
 52    pub fn new(
 53        workspace: &mut Workspace,
 54        languages: Arc<LanguageRegistry>,
 55        cx: &mut ViewContext<Workspace>,
 56    ) -> View<ActivityIndicator> {
 57        let project = workspace.project().clone();
 58        let auto_updater = AutoUpdater::get(cx);
 59        let this = cx.new_view(|cx: &mut ViewContext<Self>| {
 60            let mut status_events = languages.language_server_binary_statuses();
 61            cx.spawn(|this, mut cx| async move {
 62                while let Some((name, status)) = status_events.next().await {
 63                    this.update(&mut cx, |this, cx| {
 64                        this.statuses.retain(|s| s.name != name);
 65                        this.statuses.push(LspStatus { name, status });
 66                        cx.notify();
 67                    })?;
 68                }
 69                anyhow::Ok(())
 70            })
 71            .detach();
 72            cx.observe(&project, |_, _, cx| cx.notify()).detach();
 73
 74            if let Some(auto_updater) = auto_updater.as_ref() {
 75                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
 76            }
 77
 78            Self {
 79                statuses: Default::default(),
 80                project: project.clone(),
 81                auto_updater,
 82                context_menu: None,
 83            }
 84        });
 85
 86        cx.subscribe(&this, move |_, _, event, cx| match event {
 87            Event::ShowError { lsp_name, error } => {
 88                let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
 89                let project = project.clone();
 90                let error = error.clone();
 91                let lsp_name = lsp_name.clone();
 92                cx.spawn(|workspace, mut cx| async move {
 93                    let buffer = create_buffer.await?;
 94                    buffer.update(&mut cx, |buffer, cx| {
 95                        buffer.edit(
 96                            [(
 97                                0..0,
 98                                format!("Language server error: {}\n\n{}", lsp_name, error),
 99                            )],
100                            None,
101                            cx,
102                        );
103                    })?;
104                    workspace.update(&mut cx, |workspace, cx| {
105                        workspace.add_item_to_active_pane(
106                            Box::new(cx.new_view(|cx| {
107                                Editor::for_buffer(buffer, Some(project.clone()), cx)
108                            })),
109                            None,
110                            true,
111                            cx,
112                        );
113                    })?;
114
115                    anyhow::Ok(())
116                })
117                .detach();
118            }
119        })
120        .detach();
121        this
122    }
123
124    fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
125        self.statuses.retain(|status| {
126            if let LanguageServerBinaryStatus::Failed { error } = &status.status {
127                cx.emit(Event::ShowError {
128                    lsp_name: status.name.0.clone(),
129                    error: error.clone(),
130                });
131                false
132            } else {
133                true
134            }
135        });
136
137        cx.notify();
138    }
139
140    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
141        if let Some(updater) = &self.auto_updater {
142            updater.update(cx, |updater, cx| {
143                updater.dismiss_error(cx);
144            });
145        }
146        cx.notify();
147    }
148
149    fn pending_language_server_work<'a>(
150        &self,
151        cx: &'a AppContext,
152    ) -> impl Iterator<Item = PendingWork<'a>> {
153        self.project
154            .read(cx)
155            .language_server_statuses()
156            .rev()
157            .filter_map(|(server_id, status)| {
158                if status.pending_work.is_empty() {
159                    None
160                } else {
161                    let mut pending_work = status
162                        .pending_work
163                        .iter()
164                        .map(|(token, progress)| PendingWork {
165                            language_server_id: server_id,
166                            progress_token: token.as_str(),
167                            progress,
168                        })
169                        .collect::<SmallVec<[_; 4]>>();
170                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
171                    Some(pending_work)
172                }
173            })
174            .flatten()
175    }
176
177    fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
178        // Show any language server has pending activity.
179        let mut pending_work = self.pending_language_server_work(cx);
180        if let Some(PendingWork {
181            progress_token,
182            progress,
183            ..
184        }) = pending_work.next()
185        {
186            let mut message = progress
187                .title
188                .as_deref()
189                .unwrap_or(progress_token)
190                .to_string();
191
192            if let Some(percentage) = progress.percentage {
193                write!(&mut message, " ({}%)", percentage).unwrap();
194            }
195
196            if let Some(progress_message) = progress.message.as_ref() {
197                message.push_str(": ");
198                message.push_str(progress_message);
199            }
200
201            let additional_work_count = pending_work.count();
202            if additional_work_count > 0 {
203                write!(&mut message, " + {} more", additional_work_count).unwrap();
204            }
205
206            return Content {
207                icon: Some(
208                    Icon::new(IconName::ArrowCircle)
209                        .size(IconSize::Small)
210                        .with_animation(
211                            "arrow-circle",
212                            Animation::new(Duration::from_secs(2)).repeat(),
213                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
214                        )
215                        .into_any_element(),
216                ),
217                message,
218                on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
219            };
220        }
221
222        // Show any language server installation info.
223        let mut downloading = SmallVec::<[_; 3]>::new();
224        let mut checking_for_update = SmallVec::<[_; 3]>::new();
225        let mut failed = SmallVec::<[_; 3]>::new();
226        for status in &self.statuses {
227            match status.status {
228                LanguageServerBinaryStatus::CheckingForUpdate => {
229                    checking_for_update.push(status.name.0.as_ref())
230                }
231                LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
232                LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
233                LanguageServerBinaryStatus::None => {}
234            }
235        }
236
237        if !downloading.is_empty() {
238            return Content {
239                icon: Some(
240                    Icon::new(IconName::Download)
241                        .size(IconSize::Small)
242                        .into_any_element(),
243                ),
244                message: format!("Downloading {}...", downloading.join(", "),),
245                on_click: None,
246            };
247        }
248
249        if !checking_for_update.is_empty() {
250            return Content {
251                icon: Some(
252                    Icon::new(IconName::Download)
253                        .size(IconSize::Small)
254                        .into_any_element(),
255                ),
256                message: format!(
257                    "Checking for updates to {}...",
258                    checking_for_update.join(", "),
259                ),
260                on_click: None,
261            };
262        }
263
264        if !failed.is_empty() {
265            return Content {
266                icon: Some(
267                    Icon::new(IconName::ExclamationTriangle)
268                        .size(IconSize::Small)
269                        .into_any_element(),
270                ),
271                message: format!(
272                    "Failed to download {}. Click to show error.",
273                    failed.join(", "),
274                ),
275                on_click: Some(Arc::new(|this, cx| {
276                    this.show_error_message(&Default::default(), cx)
277                })),
278            };
279        }
280
281        // Show any formatting failure
282        if let Some(failure) = self.project.read(cx).last_formatting_failure() {
283            return Content {
284                icon: Some(
285                    Icon::new(IconName::ExclamationTriangle)
286                        .size(IconSize::Small)
287                        .into_any_element(),
288                ),
289                message: format!("Formatting failed: {}. Click to see logs.", failure),
290                on_click: Some(Arc::new(|_, cx| {
291                    cx.dispatch_action(Box::new(workspace::OpenLog));
292                })),
293            };
294        }
295
296        // Show any application auto-update info.
297        if let Some(updater) = &self.auto_updater {
298            return match &updater.read(cx).status() {
299                AutoUpdateStatus::Checking => Content {
300                    icon: Some(
301                        Icon::new(IconName::Download)
302                            .size(IconSize::Small)
303                            .into_any_element(),
304                    ),
305                    message: "Checking for Zed updates…".to_string(),
306                    on_click: None,
307                },
308                AutoUpdateStatus::Downloading => Content {
309                    icon: Some(
310                        Icon::new(IconName::Download)
311                            .size(IconSize::Small)
312                            .into_any_element(),
313                    ),
314                    message: "Downloading Zed update…".to_string(),
315                    on_click: None,
316                },
317                AutoUpdateStatus::Installing => Content {
318                    icon: Some(
319                        Icon::new(IconName::Download)
320                            .size(IconSize::Small)
321                            .into_any_element(),
322                    ),
323                    message: "Installing Zed update…".to_string(),
324                    on_click: None,
325                },
326                AutoUpdateStatus::Updated { binary_path } => Content {
327                    icon: None,
328                    message: "Click to restart and update Zed".to_string(),
329                    on_click: Some(Arc::new({
330                        let reload = workspace::Reload {
331                            binary_path: Some(binary_path.clone()),
332                        };
333                        move |_, cx| workspace::reload(&reload, cx)
334                    })),
335                },
336                AutoUpdateStatus::Errored => Content {
337                    icon: Some(
338                        Icon::new(IconName::ExclamationTriangle)
339                            .size(IconSize::Small)
340                            .into_any_element(),
341                    ),
342                    message: "Auto update failed".to_string(),
343                    on_click: Some(Arc::new(|this, cx| {
344                        this.dismiss_error_message(&Default::default(), cx)
345                    })),
346                },
347                AutoUpdateStatus::Idle => Default::default(),
348            };
349        }
350
351        if let Some(extension_store) =
352            ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
353        {
354            if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
355                return Content {
356                    icon: Some(
357                        Icon::new(IconName::Download)
358                            .size(IconSize::Small)
359                            .into_any_element(),
360                    ),
361                    message: format!("Updating {extension_id} extension…"),
362                    on_click: None,
363                };
364            }
365        }
366
367        Default::default()
368    }
369
370    fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
371        if self.context_menu.take().is_some() {
372            return;
373        }
374
375        self.build_lsp_work_context_menu(cx);
376        cx.notify();
377    }
378
379    fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
380        let mut has_work = false;
381        let this = cx.view().downgrade();
382        let context_menu = ContextMenu::build(cx, |mut menu, cx| {
383            for work in self.pending_language_server_work(cx) {
384                has_work = true;
385
386                let this = this.clone();
387                let title = SharedString::from(
388                    work.progress
389                        .title
390                        .as_deref()
391                        .unwrap_or(work.progress_token)
392                        .to_string(),
393                );
394                if work.progress.is_cancellable {
395                    let language_server_id = work.language_server_id;
396                    let token = work.progress_token.to_string();
397                    menu = menu.custom_entry(
398                        move |_| {
399                            h_flex()
400                                .w_full()
401                                .justify_between()
402                                .child(Label::new(title.clone()))
403                                .child(Icon::new(IconName::XCircle))
404                                .into_any_element()
405                        },
406                        move |cx| {
407                            this.update(cx, |this, cx| {
408                                this.project.update(cx, |project, cx| {
409                                    project.cancel_language_server_work(
410                                        language_server_id,
411                                        Some(token.clone()),
412                                        cx,
413                                    );
414                                });
415                                this.context_menu.take();
416                            })
417                            .ok();
418                        },
419                    );
420                } else {
421                    menu = menu.label(title.clone());
422                }
423            }
424            menu
425        });
426
427        if has_work {
428            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
429                this.context_menu.take();
430                cx.notify();
431            })
432            .detach();
433            cx.focus_view(&context_menu);
434            self.context_menu = Some(context_menu);
435            cx.notify();
436        }
437    }
438}
439
440impl EventEmitter<Event> for ActivityIndicator {}
441
442impl Render for ActivityIndicator {
443    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
444        let content = self.content_to_render(cx);
445
446        let mut result = h_flex()
447            .id("activity-indicator")
448            .on_action(cx.listener(Self::show_error_message))
449            .on_action(cx.listener(Self::dismiss_error_message));
450
451        if let Some(on_click) = content.on_click {
452            result = result
453                .cursor(CursorStyle::PointingHand)
454                .on_click(cx.listener(move |this, _, cx| {
455                    on_click(this, cx);
456                }))
457        }
458
459        result
460            .gap_2()
461            .children(content.icon)
462            .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
463            .children(self.context_menu.as_ref().map(|menu| {
464                deferred(
465                    anchored()
466                        .anchor(gpui::AnchorCorner::BottomLeft)
467                        .child(menu.clone()),
468                )
469                .with_priority(1)
470            }))
471    }
472}
473
474impl StatusItemView for ActivityIndicator {
475    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
476}