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