activity_indicator.rs

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