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