activity_indicator.rs

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