activity_indicator.rs

  1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
  2use editor::Editor;
  3use extension_host::ExtensionStore;
  4use futures::StreamExt;
  5use gpui::{
  6    Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
  7    InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
  8    Styled, Transformation, Window, actions, percentage,
  9};
 10use language::{
 11    BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
 12    LanguageServerStatusUpdate, ServerHealth,
 13};
 14use project::{
 15    EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
 16    ProjectEnvironmentEvent,
 17    git_store::{GitStoreEvent, Repository},
 18};
 19use smallvec::SmallVec;
 20use std::{
 21    cmp::Reverse,
 22    collections::HashSet,
 23    fmt::Write,
 24    path::Path,
 25    sync::Arc,
 26    time::{Duration, Instant},
 27};
 28use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
 29use util::truncate_and_trailoff;
 30use workspace::{StatusItemView, Workspace, item::ItemHandle};
 31
 32const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
 33
 34actions!(
 35    activity_indicator,
 36    [
 37        /// Displays error messages from language servers in the status bar.
 38        ShowErrorMessage
 39    ]
 40);
 41
 42pub enum Event {
 43    ShowStatus {
 44        server_name: LanguageServerName,
 45        status: SharedString,
 46    },
 47}
 48
 49pub struct ActivityIndicator {
 50    statuses: Vec<ServerStatus>,
 51    project: Entity<Project>,
 52    auto_updater: Option<Entity<AutoUpdater>>,
 53    context_menu_handle: PopoverMenuHandle<ContextMenu>,
 54}
 55
 56#[derive(Debug)]
 57struct ServerStatus {
 58    name: LanguageServerName,
 59    status: LanguageServerStatusUpdate,
 60}
 61
 62struct PendingWork<'a> {
 63    language_server_id: LanguageServerId,
 64    progress_token: &'a str,
 65    progress: &'a LanguageServerProgress,
 66}
 67
 68struct Content {
 69    icon: Option<gpui::AnyElement>,
 70    message: String,
 71    on_click:
 72        Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
 73    tooltip_message: Option<String>,
 74}
 75
 76impl ActivityIndicator {
 77    pub fn new(
 78        workspace: &mut Workspace,
 79        languages: Arc<LanguageRegistry>,
 80        window: &mut Window,
 81        cx: &mut Context<Workspace>,
 82    ) -> Entity<ActivityIndicator> {
 83        let project = workspace.project().clone();
 84        let auto_updater = AutoUpdater::get(cx);
 85        let workspace_handle = cx.entity();
 86        let this = cx.new(|cx| {
 87            let mut status_events = languages.language_server_binary_statuses();
 88            cx.spawn(async move |this, cx| {
 89                while let Some((name, binary_status)) = status_events.next().await {
 90                    this.update(cx, |this: &mut ActivityIndicator, cx| {
 91                        this.statuses.retain(|s| s.name != name);
 92                        this.statuses.push(ServerStatus {
 93                            name,
 94                            status: LanguageServerStatusUpdate::Binary(binary_status),
 95                        });
 96                        cx.notify();
 97                    })?;
 98                }
 99                anyhow::Ok(())
100            })
101            .detach();
102
103            cx.subscribe_in(
104                &workspace_handle,
105                window,
106                |activity_indicator, _, event, window, cx| match event {
107                    workspace::Event::ClearActivityIndicator { .. } => {
108                        if activity_indicator.statuses.pop().is_some() {
109                            activity_indicator.dismiss_error_message(
110                                &DismissErrorMessage,
111                                window,
112                                cx,
113                            );
114                            cx.notify();
115                        }
116                    }
117                    _ => {}
118                },
119            )
120            .detach();
121
122            cx.subscribe(
123                &project.read(cx).lsp_store(),
124                |activity_indicator, _, event, cx| match event {
125                    LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
126                        if let proto::update_language_server::Variant::StatusUpdate(status_update) =
127                            message
128                        {
129                            let Some(name) = name.clone() else {
130                                return;
131                            };
132                            let status = match &status_update.status {
133                                Some(proto::status_update::Status::Binary(binary_status)) => {
134                                    if let Some(binary_status) =
135                                        proto::ServerBinaryStatus::from_i32(*binary_status)
136                                    {
137                                        let binary_status = match binary_status {
138                                            proto::ServerBinaryStatus::None => BinaryStatus::None,
139                                            proto::ServerBinaryStatus::CheckingForUpdate => {
140                                                BinaryStatus::CheckingForUpdate
141                                            }
142                                            proto::ServerBinaryStatus::Downloading => {
143                                                BinaryStatus::Downloading
144                                            }
145                                            proto::ServerBinaryStatus::Starting => {
146                                                BinaryStatus::Starting
147                                            }
148                                            proto::ServerBinaryStatus::Stopping => {
149                                                BinaryStatus::Stopping
150                                            }
151                                            proto::ServerBinaryStatus::Stopped => {
152                                                BinaryStatus::Stopped
153                                            }
154                                            proto::ServerBinaryStatus::Failed => {
155                                                let Some(error) = status_update.message.clone()
156                                                else {
157                                                    return;
158                                                };
159                                                BinaryStatus::Failed { error }
160                                            }
161                                        };
162                                        LanguageServerStatusUpdate::Binary(binary_status)
163                                    } else {
164                                        return;
165                                    }
166                                }
167                                Some(proto::status_update::Status::Health(health_status)) => {
168                                    if let Some(health) =
169                                        proto::ServerHealth::from_i32(*health_status)
170                                    {
171                                        let health = match health {
172                                            proto::ServerHealth::Ok => ServerHealth::Ok,
173                                            proto::ServerHealth::Warning => ServerHealth::Warning,
174                                            proto::ServerHealth::Error => ServerHealth::Error,
175                                        };
176                                        LanguageServerStatusUpdate::Health(
177                                            health,
178                                            status_update.message.clone().map(SharedString::from),
179                                        )
180                                    } else {
181                                        return;
182                                    }
183                                }
184                                None => return,
185                            };
186
187                            activity_indicator.statuses.retain(|s| s.name != name);
188                            activity_indicator
189                                .statuses
190                                .push(ServerStatus { name, status });
191                        }
192                        cx.notify()
193                    }
194                    _ => {}
195                },
196            )
197            .detach();
198
199            cx.subscribe(
200                &project.read(cx).environment().clone(),
201                |_, _, event, cx| match event {
202                    ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
203                },
204            )
205            .detach();
206
207            cx.subscribe(
208                &project.read(cx).git_store().clone(),
209                |_, _, event: &GitStoreEvent, cx| match event {
210                    project::git_store::GitStoreEvent::JobsUpdated => cx.notify(),
211                    _ => {}
212                },
213            )
214            .detach();
215
216            if let Some(auto_updater) = auto_updater.as_ref() {
217                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
218            }
219
220            Self {
221                statuses: Vec::new(),
222                project: project.clone(),
223                auto_updater,
224                context_menu_handle: Default::default(),
225            }
226        });
227
228        cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
229            Event::ShowStatus {
230                server_name,
231                status,
232            } => {
233                let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
234                let project = project.clone();
235                let status = status.clone();
236                let server_name = server_name.clone();
237                cx.spawn_in(window, async move |workspace, cx| {
238                    let buffer = create_buffer.await?;
239                    buffer.update(cx, |buffer, cx| {
240                        buffer.edit(
241                            [(0..0, format!("Language server {server_name}:\n\n{status}"))],
242                            None,
243                            cx,
244                        );
245                        buffer.set_capability(language::Capability::ReadOnly, cx);
246                    })?;
247                    workspace.update_in(cx, |workspace, window, cx| {
248                        workspace.add_item_to_active_pane(
249                            Box::new(cx.new(|cx| {
250                                let mut editor =
251                                    Editor::for_buffer(buffer, Some(project.clone()), window, cx);
252                                editor.set_read_only(true);
253                                editor
254                            })),
255                            None,
256                            true,
257                            window,
258                            cx,
259                        );
260                    })?;
261
262                    anyhow::Ok(())
263                })
264                .detach();
265            }
266        })
267        .detach();
268        this
269    }
270
271    fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
272        let mut status_message_shown = false;
273        self.statuses.retain(|status| match &status.status {
274            LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
275                if !status_message_shown =>
276            {
277                cx.emit(Event::ShowStatus {
278                    server_name: status.name.clone(),
279                    status: SharedString::from(error),
280                });
281                status_message_shown = true;
282                false
283            }
284            LanguageServerStatusUpdate::Health(
285                ServerHealth::Error | ServerHealth::Warning,
286                status_string,
287            ) if !status_message_shown => match status_string {
288                Some(error) => {
289                    cx.emit(Event::ShowStatus {
290                        server_name: status.name.clone(),
291                        status: error.clone(),
292                    });
293                    status_message_shown = true;
294                    false
295                }
296                None => false,
297            },
298            _ => true,
299        });
300    }
301
302    fn dismiss_error_message(
303        &mut self,
304        _: &DismissErrorMessage,
305        _: &mut Window,
306        cx: &mut Context<Self>,
307    ) {
308        let error_dismissed = if let Some(updater) = &self.auto_updater {
309            updater.update(cx, |updater, cx| updater.dismiss_error(cx))
310        } else {
311            false
312        };
313        if error_dismissed {
314            return;
315        }
316
317        self.project.update(cx, |project, cx| {
318            if project.last_formatting_failure(cx).is_some() {
319                project.reset_last_formatting_failure(cx);
320                true
321            } else {
322                false
323            }
324        });
325    }
326
327    fn pending_language_server_work<'a>(
328        &self,
329        cx: &'a App,
330    ) -> impl Iterator<Item = PendingWork<'a>> {
331        self.project
332            .read(cx)
333            .language_server_statuses(cx)
334            .rev()
335            .filter_map(|(server_id, status)| {
336                if status.pending_work.is_empty() {
337                    None
338                } else {
339                    let mut pending_work = status
340                        .pending_work
341                        .iter()
342                        .map(|(token, progress)| PendingWork {
343                            language_server_id: server_id,
344                            progress_token: token.as_str(),
345                            progress,
346                        })
347                        .collect::<SmallVec<[_; 4]>>();
348                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
349                    Some(pending_work)
350                }
351            })
352            .flatten()
353    }
354
355    fn pending_environment_errors<'a>(
356        &'a self,
357        cx: &'a App,
358    ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
359        self.project.read(cx).shell_environment_errors(cx)
360    }
361
362    fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
363        // Show if any direnv calls failed
364        if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
365            let abs_path = abs_path.clone();
366            return Some(Content {
367                icon: Some(
368                    Icon::new(IconName::Warning)
369                        .size(IconSize::Small)
370                        .into_any_element(),
371                ),
372                message: error.0.clone(),
373                on_click: Some(Arc::new(move |this, window, cx| {
374                    this.project.update(cx, |project, cx| {
375                        project.remove_environment_error(&abs_path, cx);
376                    });
377                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
378                })),
379                tooltip_message: None,
380            });
381        }
382        // Show any language server has pending activity.
383        {
384            let mut pending_work = self.pending_language_server_work(cx);
385            if let Some(PendingWork {
386                progress_token,
387                progress,
388                ..
389            }) = pending_work.next()
390            {
391                let mut message = progress
392                    .title
393                    .as_deref()
394                    .unwrap_or(progress_token)
395                    .to_string();
396
397                if let Some(percentage) = progress.percentage {
398                    write!(&mut message, " ({}%)", percentage).unwrap();
399                }
400
401                if let Some(progress_message) = progress.message.as_ref() {
402                    message.push_str(": ");
403                    message.push_str(progress_message);
404                }
405
406                let additional_work_count = pending_work.count();
407                if additional_work_count > 0 {
408                    write!(&mut message, " + {} more", additional_work_count).unwrap();
409                }
410
411                return Some(Content {
412                    icon: Some(
413                        Icon::new(IconName::ArrowCircle)
414                            .size(IconSize::Small)
415                            .with_animation(
416                                "arrow-circle",
417                                Animation::new(Duration::from_secs(2)).repeat(),
418                                |icon, delta| {
419                                    icon.transform(Transformation::rotate(percentage(delta)))
420                                },
421                            )
422                            .into_any_element(),
423                    ),
424                    message,
425                    on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
426                    tooltip_message: None,
427                });
428            }
429        }
430
431        if let Some(session) = self
432            .project
433            .read(cx)
434            .dap_store()
435            .read(cx)
436            .sessions()
437            .find(|s| !s.read(cx).is_started())
438        {
439            return Some(Content {
440                icon: Some(
441                    Icon::new(IconName::ArrowCircle)
442                        .size(IconSize::Small)
443                        .with_animation(
444                            "arrow-circle",
445                            Animation::new(Duration::from_secs(2)).repeat(),
446                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
447                        )
448                        .into_any_element(),
449                ),
450                message: format!("Debug: {}", session.read(cx).adapter()),
451                tooltip_message: session.read(cx).label().map(|label| label.to_string()),
452                on_click: None,
453            });
454        }
455
456        let current_job = self
457            .project
458            .read(cx)
459            .active_repository(cx)
460            .map(|r| r.read(cx))
461            .and_then(Repository::current_job);
462        // Show any long-running git command
463        if let Some(job_info) = current_job {
464            if Instant::now() - job_info.start >= GIT_OPERATION_DELAY {
465                return Some(Content {
466                    icon: Some(
467                        Icon::new(IconName::ArrowCircle)
468                            .size(IconSize::Small)
469                            .with_animation(
470                                "arrow-circle",
471                                Animation::new(Duration::from_secs(2)).repeat(),
472                                |icon, delta| {
473                                    icon.transform(Transformation::rotate(percentage(delta)))
474                                },
475                            )
476                            .into_any_element(),
477                    ),
478                    message: job_info.message.into(),
479                    on_click: None,
480                    tooltip_message: None,
481                });
482            }
483        }
484
485        // Show any language server installation info.
486        let mut downloading = SmallVec::<[_; 3]>::new();
487        let mut checking_for_update = SmallVec::<[_; 3]>::new();
488        let mut failed = SmallVec::<[_; 3]>::new();
489        let mut health_messages = SmallVec::<[_; 3]>::new();
490        let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
491        for status in &self.statuses {
492            match &status.status {
493                LanguageServerStatusUpdate::Binary(
494                    BinaryStatus::Starting | BinaryStatus::Stopping,
495                ) => {}
496                LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
497                    servers_to_clear_statuses.insert(status.name.clone());
498                }
499                LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
500                    checking_for_update.push(status.name.clone());
501                }
502                LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
503                    downloading.push(status.name.clone());
504                }
505                LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
506                    failed.push(status.name.clone());
507                }
508                LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
509                LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
510                    Some(server_status) => {
511                        health_messages.push((status.name.clone(), *health, server_status.clone()));
512                    }
513                    None => {
514                        servers_to_clear_statuses.insert(status.name.clone());
515                    }
516                },
517            }
518        }
519        self.statuses
520            .retain(|status| !servers_to_clear_statuses.contains(&status.name));
521
522        health_messages.sort_by_key(|(_, health, _)| match health {
523            ServerHealth::Error => 2,
524            ServerHealth::Warning => 1,
525            ServerHealth::Ok => 0,
526        });
527
528        if !downloading.is_empty() {
529            return Some(Content {
530                icon: Some(
531                    Icon::new(IconName::Download)
532                        .size(IconSize::Small)
533                        .into_any_element(),
534                ),
535                message: format!(
536                    "Downloading {}...",
537                    downloading.iter().map(|name| name.as_ref()).fold(
538                        String::new(),
539                        |mut acc, s| {
540                            if !acc.is_empty() {
541                                acc.push_str(", ");
542                            }
543                            acc.push_str(s);
544                            acc
545                        }
546                    )
547                ),
548                on_click: Some(Arc::new(move |this, window, cx| {
549                    this.statuses
550                        .retain(|status| !downloading.contains(&status.name));
551                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
552                })),
553                tooltip_message: None,
554            });
555        }
556
557        if !checking_for_update.is_empty() {
558            return Some(Content {
559                icon: Some(
560                    Icon::new(IconName::Download)
561                        .size(IconSize::Small)
562                        .into_any_element(),
563                ),
564                message: format!(
565                    "Checking for updates to {}...",
566                    checking_for_update.iter().map(|name| name.as_ref()).fold(
567                        String::new(),
568                        |mut acc, s| {
569                            if !acc.is_empty() {
570                                acc.push_str(", ");
571                            }
572                            acc.push_str(s);
573                            acc
574                        }
575                    ),
576                ),
577                on_click: Some(Arc::new(move |this, window, cx| {
578                    this.statuses
579                        .retain(|status| !checking_for_update.contains(&status.name));
580                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
581                })),
582                tooltip_message: None,
583            });
584        }
585
586        if !failed.is_empty() {
587            return Some(Content {
588                icon: Some(
589                    Icon::new(IconName::Warning)
590                        .size(IconSize::Small)
591                        .into_any_element(),
592                ),
593                message: format!(
594                    "Failed to run {}. Click to show error.",
595                    failed
596                        .iter()
597                        .map(|name| name.as_ref())
598                        .fold(String::new(), |mut acc, s| {
599                            if !acc.is_empty() {
600                                acc.push_str(", ");
601                            }
602                            acc.push_str(s);
603                            acc
604                        }),
605                ),
606                on_click: Some(Arc::new(|this, window, cx| {
607                    this.show_error_message(&ShowErrorMessage, window, cx)
608                })),
609                tooltip_message: None,
610            });
611        }
612
613        // Show any formatting failure
614        if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
615            return Some(Content {
616                icon: Some(
617                    Icon::new(IconName::Warning)
618                        .size(IconSize::Small)
619                        .into_any_element(),
620                ),
621                message: format!("Formatting failed: {failure}. Click to see logs."),
622                on_click: Some(Arc::new(|indicator, window, cx| {
623                    indicator.project.update(cx, |project, cx| {
624                        project.reset_last_formatting_failure(cx);
625                    });
626                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
627                })),
628                tooltip_message: None,
629            });
630        }
631
632        // Show any health messages for the language servers
633        if let Some((server_name, health, message)) = health_messages.pop() {
634            let health_str = match health {
635                ServerHealth::Ok => format!("({server_name}) "),
636                ServerHealth::Warning => format!("({server_name}) Warning: "),
637                ServerHealth::Error => format!("({server_name}) Error: "),
638            };
639            let single_line_message = message
640                .lines()
641                .filter_map(|line| {
642                    let line = line.trim();
643                    if line.is_empty() { None } else { Some(line) }
644                })
645                .collect::<Vec<_>>()
646                .join(" ");
647            let mut altered_message = single_line_message != message;
648            let truncated_message = truncate_and_trailoff(
649                &single_line_message,
650                MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
651            );
652            altered_message |= truncated_message != single_line_message;
653            let final_message = format!("{health_str}{truncated_message}");
654
655            let tooltip_message = if altered_message {
656                Some(format!("{health_str}{message}"))
657            } else {
658                None
659            };
660
661            return Some(Content {
662                icon: Some(
663                    Icon::new(IconName::Warning)
664                        .size(IconSize::Small)
665                        .into_any_element(),
666                ),
667                message: final_message,
668                tooltip_message,
669                on_click: Some(Arc::new(move |activity_indicator, window, cx| {
670                    if altered_message {
671                        activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
672                    } else {
673                        activity_indicator
674                            .statuses
675                            .retain(|status| status.name != server_name);
676                        cx.notify();
677                    }
678                })),
679            });
680        }
681
682        // Show any application auto-update info.
683        if let Some(updater) = &self.auto_updater {
684            return match &updater.read(cx).status() {
685                AutoUpdateStatus::Checking => Some(Content {
686                    icon: Some(
687                        Icon::new(IconName::Download)
688                            .size(IconSize::Small)
689                            .into_any_element(),
690                    ),
691                    message: "Checking for Zed updates…".to_string(),
692                    on_click: Some(Arc::new(|this, window, cx| {
693                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
694                    })),
695                    tooltip_message: None,
696                }),
697                AutoUpdateStatus::Downloading { version } => Some(Content {
698                    icon: Some(
699                        Icon::new(IconName::Download)
700                            .size(IconSize::Small)
701                            .into_any_element(),
702                    ),
703                    message: "Downloading Zed update…".to_string(),
704                    on_click: Some(Arc::new(|this, window, cx| {
705                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
706                    })),
707                    tooltip_message: Some(Self::version_tooltip_message(&version)),
708                }),
709                AutoUpdateStatus::Installing { version } => Some(Content {
710                    icon: Some(
711                        Icon::new(IconName::Download)
712                            .size(IconSize::Small)
713                            .into_any_element(),
714                    ),
715                    message: "Installing Zed update…".to_string(),
716                    on_click: Some(Arc::new(|this, window, cx| {
717                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
718                    })),
719                    tooltip_message: Some(Self::version_tooltip_message(&version)),
720                }),
721                AutoUpdateStatus::Updated {
722                    binary_path,
723                    version,
724                } => Some(Content {
725                    icon: None,
726                    message: "Click to restart and update Zed".to_string(),
727                    on_click: Some(Arc::new({
728                        let reload = workspace::Reload {
729                            binary_path: Some(binary_path.clone()),
730                        };
731                        move |_, _, cx| workspace::reload(&reload, cx)
732                    })),
733                    tooltip_message: Some(Self::version_tooltip_message(&version)),
734                }),
735                AutoUpdateStatus::Errored => Some(Content {
736                    icon: Some(
737                        Icon::new(IconName::Warning)
738                            .size(IconSize::Small)
739                            .into_any_element(),
740                    ),
741                    message: "Auto update failed".to_string(),
742                    on_click: Some(Arc::new(|this, window, cx| {
743                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
744                    })),
745                    tooltip_message: None,
746                }),
747                AutoUpdateStatus::Idle => None,
748            };
749        }
750
751        if let Some(extension_store) =
752            ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
753        {
754            if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
755                return Some(Content {
756                    icon: Some(
757                        Icon::new(IconName::Download)
758                            .size(IconSize::Small)
759                            .into_any_element(),
760                    ),
761                    message: format!("Updating {extension_id} extension…"),
762                    on_click: Some(Arc::new(|this, window, cx| {
763                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
764                    })),
765                    tooltip_message: None,
766                });
767            }
768        }
769
770        None
771    }
772
773    fn version_tooltip_message(version: &VersionCheckType) -> String {
774        format!("Version: {}", {
775            match version {
776                auto_update::VersionCheckType::Sha(sha) => format!("{}", sha.short()),
777                auto_update::VersionCheckType::Semantic(semantic_version) => {
778                    semantic_version.to_string()
779                }
780            }
781        })
782    }
783
784    fn toggle_language_server_work_context_menu(
785        &mut self,
786        window: &mut Window,
787        cx: &mut Context<Self>,
788    ) {
789        self.context_menu_handle.toggle(window, cx);
790    }
791}
792
793impl EventEmitter<Event> for ActivityIndicator {}
794
795const MAX_MESSAGE_LEN: usize = 50;
796
797impl Render for ActivityIndicator {
798    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
799        let result = h_flex()
800            .id("activity-indicator")
801            .on_action(cx.listener(Self::show_error_message))
802            .on_action(cx.listener(Self::dismiss_error_message));
803        let Some(content) = self.content_to_render(cx) else {
804            return result;
805        };
806        let this = cx.entity().downgrade();
807        let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
808        result.gap_2().child(
809            PopoverMenu::new("activity-indicator-popover")
810                .trigger(
811                    ButtonLike::new("activity-indicator-trigger").child(
812                        h_flex()
813                            .id("activity-indicator-status")
814                            .gap_2()
815                            .children(content.icon)
816                            .map(|button| {
817                                if truncate_content {
818                                    button
819                                        .child(
820                                            Label::new(truncate_and_trailoff(
821                                                &content.message,
822                                                MAX_MESSAGE_LEN,
823                                            ))
824                                            .size(LabelSize::Small),
825                                        )
826                                        .tooltip(Tooltip::text(content.message))
827                                } else {
828                                    button
829                                        .child(Label::new(content.message).size(LabelSize::Small))
830                                        .when_some(
831                                            content.tooltip_message,
832                                            |this, tooltip_message| {
833                                                this.tooltip(Tooltip::text(tooltip_message))
834                                            },
835                                        )
836                                }
837                            })
838                            .when_some(content.on_click, |this, handler| {
839                                this.on_click(cx.listener(move |this, _, window, cx| {
840                                    handler(this, window, cx);
841                                }))
842                                .cursor(CursorStyle::PointingHand)
843                            }),
844                    ),
845                )
846                .anchor(gpui::Corner::BottomLeft)
847                .menu(move |window, cx| {
848                    let strong_this = this.upgrade()?;
849                    let mut has_work = false;
850                    let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
851                        for work in strong_this.read(cx).pending_language_server_work(cx) {
852                            has_work = true;
853                            let this = this.clone();
854                            let mut title = work
855                                .progress
856                                .title
857                                .as_deref()
858                                .unwrap_or(work.progress_token)
859                                .to_owned();
860
861                            if work.progress.is_cancellable {
862                                let language_server_id = work.language_server_id;
863                                let token = work.progress_token.to_string();
864                                let title = SharedString::from(title);
865                                menu = menu.custom_entry(
866                                    move |_, _| {
867                                        h_flex()
868                                            .w_full()
869                                            .justify_between()
870                                            .child(Label::new(title.clone()))
871                                            .child(Icon::new(IconName::XCircle))
872                                            .into_any_element()
873                                    },
874                                    move |_, cx| {
875                                        this.update(cx, |this, cx| {
876                                            this.project.update(cx, |project, cx| {
877                                                project.cancel_language_server_work(
878                                                    language_server_id,
879                                                    Some(token.clone()),
880                                                    cx,
881                                                );
882                                            });
883                                            this.context_menu_handle.hide(cx);
884                                            cx.notify();
885                                        })
886                                        .ok();
887                                    },
888                                );
889                            } else {
890                                if let Some(progress_message) = work.progress.message.as_ref() {
891                                    title.push_str(": ");
892                                    title.push_str(progress_message);
893                                }
894
895                                menu = menu.label(title);
896                            }
897                        }
898                        menu
899                    });
900                    has_work.then_some(menu)
901                }),
902        )
903    }
904}
905
906impl StatusItemView for ActivityIndicator {
907    fn set_active_pane_item(
908        &mut self,
909        _: Option<&dyn ItemHandle>,
910        _window: &mut Window,
911        _: &mut Context<Self>,
912    ) {
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use gpui::SemanticVersion;
919    use release_channel::AppCommitSha;
920
921    use super::*;
922
923    #[test]
924    fn test_version_tooltip_message() {
925        let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
926            SemanticVersion::new(1, 0, 0),
927        ));
928
929        assert_eq!(message, "Version: 1.0.0");
930
931        let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha(
932            AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()),
933        ));
934
935        assert_eq!(message, "Version: 14d9a41…");
936    }
937}