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    id: LanguageServerId,
 60    status: LanguageServerStatusUpdate,
 61}
 62
 63struct PendingWork<'a> {
 64    language_server_id: LanguageServerId,
 65    progress_token: &'a str,
 66    progress: &'a LanguageServerProgress,
 67}
 68
 69struct Content {
 70    icon: Option<gpui::AnyElement>,
 71    message: String,
 72    on_click:
 73        Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
 74    tooltip_message: Option<String>,
 75}
 76
 77impl ActivityIndicator {
 78    pub fn new(
 79        workspace: &mut Workspace,
 80        languages: Arc<LanguageRegistry>,
 81        window: &mut Window,
 82        cx: &mut Context<Workspace>,
 83    ) -> Entity<ActivityIndicator> {
 84        let project = workspace.project().clone();
 85        let auto_updater = AutoUpdater::get(cx);
 86        let workspace_handle = cx.entity();
 87        let this = cx.new(|cx| {
 88            let mut status_events = languages.language_server_binary_statuses();
 89            cx.spawn(async move |this, cx| {
 90                while let Some((id, name, binary_status)) = status_events.next().await {
 91                    this.update(cx, |this: &mut ActivityIndicator, cx| {
 92                        this.statuses.retain(|s| s.id != id);
 93                        this.statuses.push(ServerStatus {
 94                            name,
 95                            id,
 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 {
123                        language_server_id,
124                        name,
125                        message,
126                        ..
127                    } = event
128                    {
129                        if let proto::update_language_server::Variant::StatusUpdate(status_update) =
130                            message
131                        {
132                            let Some(name) = name.clone() else {
133                                return;
134                            };
135                            let status = match &status_update.status {
136                                Some(proto::status_update::Status::Binary(binary_status)) => {
137                                    if let Some(binary_status) =
138                                        proto::ServerBinaryStatus::from_i32(*binary_status)
139                                    {
140                                        let binary_status = match binary_status {
141                                            proto::ServerBinaryStatus::None => BinaryStatus::None,
142                                            proto::ServerBinaryStatus::CheckingForUpdate => {
143                                                BinaryStatus::CheckingForUpdate
144                                            }
145                                            proto::ServerBinaryStatus::Downloading => {
146                                                BinaryStatus::Downloading
147                                            }
148                                            proto::ServerBinaryStatus::Starting => {
149                                                BinaryStatus::Starting
150                                            }
151                                            proto::ServerBinaryStatus::Stopping => {
152                                                BinaryStatus::Stopping
153                                            }
154                                            proto::ServerBinaryStatus::Stopped => {
155                                                BinaryStatus::Stopped
156                                            }
157                                            proto::ServerBinaryStatus::Failed => {
158                                                let Some(error) = status_update.message.clone()
159                                                else {
160                                                    return;
161                                                };
162                                                BinaryStatus::Failed { error }
163                                            }
164                                        };
165                                        LanguageServerStatusUpdate::Binary(binary_status)
166                                    } else {
167                                        return;
168                                    }
169                                }
170                                Some(proto::status_update::Status::Health(health_status)) => {
171                                    if let Some(health) =
172                                        proto::ServerHealth::from_i32(*health_status)
173                                    {
174                                        let health = match health {
175                                            proto::ServerHealth::Ok => ServerHealth::Ok,
176                                            proto::ServerHealth::Warning => ServerHealth::Warning,
177                                            proto::ServerHealth::Error => ServerHealth::Error,
178                                        };
179                                        LanguageServerStatusUpdate::Health(
180                                            health,
181                                            status_update.message.clone().map(SharedString::from),
182                                        )
183                                    } else {
184                                        return;
185                                    }
186                                }
187                                None => return,
188                            };
189
190                            activity_indicator.statuses.retain(|s| s.name != name);
191                            activity_indicator.statuses.push(ServerStatus {
192                                name,
193                                id: *language_server_id,
194                                status,
195                            });
196                        }
197                        cx.notify()
198                    }
199                },
200            )
201            .detach();
202
203            cx.subscribe(
204                &project.read(cx).environment().clone(),
205                |_, _, event, cx| match event {
206                    ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
207                },
208            )
209            .detach();
210
211            cx.subscribe(
212                &project.read(cx).git_store().clone(),
213                |_, _, event: &GitStoreEvent, cx| {
214                    if let project::git_store::GitStoreEvent::JobsUpdated = event {
215                        cx.notify()
216                    }
217                },
218            )
219            .detach();
220
221            if let Some(auto_updater) = auto_updater.as_ref() {
222                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
223            }
224
225            Self {
226                statuses: Vec::new(),
227                project: project.clone(),
228                auto_updater,
229                context_menu_handle: Default::default(),
230            }
231        });
232
233        cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
234            Event::ShowStatus {
235                server_name,
236                status,
237            } => {
238                let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
239                let status = status.clone();
240                let server_name = server_name.clone();
241                cx.spawn_in(window, async move |workspace, cx| {
242                    let buffer = create_buffer.await?;
243                    buffer.update(cx, |buffer, cx| {
244                        buffer.edit(
245                            [(0..0, format!("Language server {server_name}:\n\n{status}"))],
246                            None,
247                            cx,
248                        );
249                        buffer.set_capability(language::Capability::ReadOnly, cx);
250                    })?;
251                    workspace.update_in(cx, |workspace, window, cx| {
252                        workspace.add_item_to_active_pane(
253                            Box::new(cx.new(|cx| {
254                                let mut editor = Editor::for_buffer(buffer, None, window, cx);
255                                editor.set_read_only(true);
256                                editor
257                            })),
258                            None,
259                            true,
260                            window,
261                            cx,
262                        );
263                    })?;
264
265                    anyhow::Ok(())
266                })
267                .detach();
268            }
269        })
270        .detach();
271        this
272    }
273
274    fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
275        let mut status_message_shown = false;
276        self.statuses.retain(|status| match &status.status {
277            LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
278                if !status_message_shown =>
279            {
280                cx.emit(Event::ShowStatus {
281                    server_name: status.name.clone(),
282                    status: SharedString::from(error),
283                });
284                status_message_shown = true;
285                false
286            }
287            LanguageServerStatusUpdate::Health(
288                ServerHealth::Error | ServerHealth::Warning,
289                status_string,
290            ) if !status_message_shown => match status_string {
291                Some(error) => {
292                    cx.emit(Event::ShowStatus {
293                        server_name: status.name.clone(),
294                        status: error.clone(),
295                    });
296                    status_message_shown = true;
297                    false
298                }
299                None => false,
300            },
301            _ => true,
302        });
303    }
304
305    fn dismiss_error_message(
306        &mut self,
307        _: &DismissErrorMessage,
308        _: &mut Window,
309        cx: &mut Context<Self>,
310    ) {
311        let error_dismissed = if let Some(updater) = &self.auto_updater {
312            updater.update(cx, |updater, cx| updater.dismiss_error(cx))
313        } else {
314            false
315        };
316        if error_dismissed {
317            return;
318        }
319
320        self.project.update(cx, |project, cx| {
321            if project.last_formatting_failure(cx).is_some() {
322                project.reset_last_formatting_failure(cx);
323                true
324            } else {
325                false
326            }
327        });
328    }
329
330    fn pending_language_server_work<'a>(
331        &self,
332        cx: &'a App,
333    ) -> impl Iterator<Item = PendingWork<'a>> {
334        self.project
335            .read(cx)
336            .language_server_statuses(cx)
337            .rev()
338            .filter_map(|(server_id, status)| {
339                if status.pending_work.is_empty() {
340                    None
341                } else {
342                    let mut pending_work = status
343                        .pending_work
344                        .iter()
345                        .map(|(token, progress)| PendingWork {
346                            language_server_id: server_id,
347                            progress_token: token.as_str(),
348                            progress,
349                        })
350                        .collect::<SmallVec<[_; 4]>>();
351                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
352                    Some(pending_work)
353                }
354            })
355            .flatten()
356    }
357
358    fn pending_environment_errors<'a>(
359        &'a self,
360        cx: &'a App,
361    ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
362        self.project.read(cx).shell_environment_errors(cx)
363    }
364
365    fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
366        // Show if any direnv calls failed
367        if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
368            let abs_path = abs_path.clone();
369            return Some(Content {
370                icon: Some(
371                    Icon::new(IconName::Warning)
372                        .size(IconSize::Small)
373                        .into_any_element(),
374                ),
375                message: error.0.clone(),
376                on_click: Some(Arc::new(move |this, window, cx| {
377                    this.project.update(cx, |project, cx| {
378                        project.remove_environment_error(&abs_path, cx);
379                    });
380                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
381                })),
382                tooltip_message: None,
383            });
384        }
385        // Show any language server has pending activity.
386        {
387            let mut pending_work = self.pending_language_server_work(cx);
388            if let Some(PendingWork {
389                progress_token,
390                progress,
391                ..
392            }) = pending_work.next()
393            {
394                let mut message = progress
395                    .title
396                    .as_deref()
397                    .unwrap_or(progress_token)
398                    .to_string();
399
400                if let Some(percentage) = progress.percentage {
401                    write!(&mut message, " ({}%)", percentage).unwrap();
402                }
403
404                if let Some(progress_message) = progress.message.as_ref() {
405                    message.push_str(": ");
406                    message.push_str(progress_message);
407                }
408
409                let additional_work_count = pending_work.count();
410                if additional_work_count > 0 {
411                    write!(&mut message, " + {} more", additional_work_count).unwrap();
412                }
413
414                return Some(Content {
415                    icon: Some(
416                        Icon::new(IconName::ArrowCircle)
417                            .size(IconSize::Small)
418                            .with_animation(
419                                "arrow-circle",
420                                Animation::new(Duration::from_secs(2)).repeat(),
421                                |icon, delta| {
422                                    icon.transform(Transformation::rotate(percentage(delta)))
423                                },
424                            )
425                            .into_any_element(),
426                    ),
427                    message,
428                    on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
429                    tooltip_message: None,
430                });
431            }
432        }
433
434        if let Some(session) = self
435            .project
436            .read(cx)
437            .dap_store()
438            .read(cx)
439            .sessions()
440            .find(|s| !s.read(cx).is_started())
441        {
442            return Some(Content {
443                icon: Some(
444                    Icon::new(IconName::ArrowCircle)
445                        .size(IconSize::Small)
446                        .with_animation(
447                            "arrow-circle",
448                            Animation::new(Duration::from_secs(2)).repeat(),
449                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
450                        )
451                        .into_any_element(),
452                ),
453                message: format!("Debug: {}", session.read(cx).adapter()),
454                tooltip_message: session.read(cx).label().map(|label| label.to_string()),
455                on_click: None,
456            });
457        }
458
459        let current_job = self
460            .project
461            .read(cx)
462            .active_repository(cx)
463            .map(|r| r.read(cx))
464            .and_then(Repository::current_job);
465        // Show any long-running git command
466        if let Some(job_info) = current_job
467            && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
468        {
469            return Some(Content {
470                icon: Some(
471                    Icon::new(IconName::ArrowCircle)
472                        .size(IconSize::Small)
473                        .with_animation(
474                            "arrow-circle",
475                            Animation::new(Duration::from_secs(2)).repeat(),
476                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
477                        )
478                        .into_any_element(),
479                ),
480                message: job_info.message.into(),
481                on_click: None,
482                tooltip_message: None,
483            });
484        }
485
486        // Show any language server installation info.
487        let mut downloading = SmallVec::<[_; 3]>::new();
488        let mut checking_for_update = SmallVec::<[_; 3]>::new();
489        let mut failed = SmallVec::<[_; 3]>::new();
490        let mut health_messages = SmallVec::<[_; 3]>::new();
491        let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
492        for status in &self.statuses {
493            match &status.status {
494                LanguageServerStatusUpdate::Binary(
495                    BinaryStatus::Starting | BinaryStatus::Stopping,
496                ) => {}
497                LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
498                    servers_to_clear_statuses.insert(status.name.clone());
499                }
500                LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
501                    checking_for_update.push(status.name.clone());
502                }
503                LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
504                    downloading.push(status.name.clone());
505                }
506                LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
507                    failed.push(status.name.clone());
508                }
509                LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
510                LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
511                    Some(server_status) => {
512                        health_messages.push((status.name.clone(), *health, server_status.clone()));
513                    }
514                    None => {
515                        servers_to_clear_statuses.insert(status.name.clone());
516                    }
517                },
518            }
519        }
520        self.statuses
521            .retain(|status| !servers_to_clear_statuses.contains(&status.name));
522
523        health_messages.sort_by_key(|(_, health, _)| match health {
524            ServerHealth::Error => 2,
525            ServerHealth::Warning => 1,
526            ServerHealth::Ok => 0,
527        });
528
529        if !downloading.is_empty() {
530            return Some(Content {
531                icon: Some(
532                    Icon::new(IconName::Download)
533                        .size(IconSize::Small)
534                        .into_any_element(),
535                ),
536                message: format!(
537                    "Downloading {}...",
538                    downloading.iter().map(|name| name.as_ref()).fold(
539                        String::new(),
540                        |mut acc, s| {
541                            if !acc.is_empty() {
542                                acc.push_str(", ");
543                            }
544                            acc.push_str(s);
545                            acc
546                        }
547                    )
548                ),
549                on_click: Some(Arc::new(move |this, window, cx| {
550                    this.statuses
551                        .retain(|status| !downloading.contains(&status.name));
552                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
553                })),
554                tooltip_message: None,
555            });
556        }
557
558        if !checking_for_update.is_empty() {
559            return Some(Content {
560                icon: Some(
561                    Icon::new(IconName::Download)
562                        .size(IconSize::Small)
563                        .into_any_element(),
564                ),
565                message: format!(
566                    "Checking for updates to {}...",
567                    checking_for_update.iter().map(|name| name.as_ref()).fold(
568                        String::new(),
569                        |mut acc, s| {
570                            if !acc.is_empty() {
571                                acc.push_str(", ");
572                            }
573                            acc.push_str(s);
574                            acc
575                        }
576                    ),
577                ),
578                on_click: Some(Arc::new(move |this, window, cx| {
579                    this.statuses
580                        .retain(|status| !checking_for_update.contains(&status.name));
581                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
582                })),
583                tooltip_message: None,
584            });
585        }
586
587        if !failed.is_empty() {
588            return Some(Content {
589                icon: Some(
590                    Icon::new(IconName::Warning)
591                        .size(IconSize::Small)
592                        .into_any_element(),
593                ),
594                message: format!(
595                    "Failed to run {}. Click to show error.",
596                    failed
597                        .iter()
598                        .map(|name| name.as_ref())
599                        .fold(String::new(), |mut acc, s| {
600                            if !acc.is_empty() {
601                                acc.push_str(", ");
602                            }
603                            acc.push_str(s);
604                            acc
605                        }),
606                ),
607                on_click: Some(Arc::new(|this, window, cx| {
608                    this.show_error_message(&ShowErrorMessage, window, cx)
609                })),
610                tooltip_message: None,
611            });
612        }
613
614        // Show any formatting failure
615        if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
616            return Some(Content {
617                icon: Some(
618                    Icon::new(IconName::Warning)
619                        .size(IconSize::Small)
620                        .into_any_element(),
621                ),
622                message: format!("Formatting failed: {failure}. Click to see logs."),
623                on_click: Some(Arc::new(|indicator, window, cx| {
624                    indicator.project.update(cx, |project, cx| {
625                        project.reset_last_formatting_failure(cx);
626                    });
627                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
628                })),
629                tooltip_message: None,
630            });
631        }
632
633        // Show any health messages for the language servers
634        if let Some((server_name, health, message)) = health_messages.pop() {
635            let health_str = match health {
636                ServerHealth::Ok => format!("({server_name}) "),
637                ServerHealth::Warning => format!("({server_name}) Warning: "),
638                ServerHealth::Error => format!("({server_name}) Error: "),
639            };
640            let single_line_message = message
641                .lines()
642                .filter_map(|line| {
643                    let line = line.trim();
644                    if line.is_empty() { None } else { Some(line) }
645                })
646                .collect::<Vec<_>>()
647                .join(" ");
648            let mut altered_message = single_line_message != message;
649            let truncated_message = truncate_and_trailoff(
650                &single_line_message,
651                MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
652            );
653            altered_message |= truncated_message != single_line_message;
654            let final_message = format!("{health_str}{truncated_message}");
655
656            let tooltip_message = if altered_message {
657                Some(format!("{health_str}{message}"))
658            } else {
659                None
660            };
661
662            return Some(Content {
663                icon: Some(
664                    Icon::new(IconName::Warning)
665                        .size(IconSize::Small)
666                        .into_any_element(),
667                ),
668                message: final_message,
669                tooltip_message,
670                on_click: Some(Arc::new(move |activity_indicator, window, cx| {
671                    if altered_message {
672                        activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
673                    } else {
674                        activity_indicator
675                            .statuses
676                            .retain(|status| status.name != server_name);
677                        cx.notify();
678                    }
679                })),
680            });
681        }
682
683        // Show any application auto-update info.
684        if let Some(updater) = &self.auto_updater {
685            return match &updater.read(cx).status() {
686                AutoUpdateStatus::Checking => Some(Content {
687                    icon: Some(
688                        Icon::new(IconName::Download)
689                            .size(IconSize::Small)
690                            .into_any_element(),
691                    ),
692                    message: "Checking for Zed updates…".to_string(),
693                    on_click: Some(Arc::new(|this, window, cx| {
694                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
695                    })),
696                    tooltip_message: None,
697                }),
698                AutoUpdateStatus::Downloading { version } => Some(Content {
699                    icon: Some(
700                        Icon::new(IconName::Download)
701                            .size(IconSize::Small)
702                            .into_any_element(),
703                    ),
704                    message: "Downloading Zed update…".to_string(),
705                    on_click: Some(Arc::new(|this, window, cx| {
706                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
707                    })),
708                    tooltip_message: Some(Self::version_tooltip_message(version)),
709                }),
710                AutoUpdateStatus::Installing { version } => Some(Content {
711                    icon: Some(
712                        Icon::new(IconName::Download)
713                            .size(IconSize::Small)
714                            .into_any_element(),
715                    ),
716                    message: "Installing Zed update…".to_string(),
717                    on_click: Some(Arc::new(|this, window, cx| {
718                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
719                    })),
720                    tooltip_message: Some(Self::version_tooltip_message(version)),
721                }),
722                AutoUpdateStatus::Updated { version } => Some(Content {
723                    icon: None,
724                    message: "Click to restart and update Zed".to_string(),
725                    on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
726                    tooltip_message: Some(Self::version_tooltip_message(version)),
727                }),
728                AutoUpdateStatus::Errored => Some(Content {
729                    icon: Some(
730                        Icon::new(IconName::Warning)
731                            .size(IconSize::Small)
732                            .into_any_element(),
733                    ),
734                    message: "Auto update failed".to_string(),
735                    on_click: Some(Arc::new(|this, window, cx| {
736                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
737                    })),
738                    tooltip_message: None,
739                }),
740                AutoUpdateStatus::Idle => None,
741            };
742        }
743
744        if let Some(extension_store) =
745            ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
746            && let Some(extension_id) = extension_store.outstanding_operations().keys().next()
747        {
748            return Some(Content {
749                icon: Some(
750                    Icon::new(IconName::Download)
751                        .size(IconSize::Small)
752                        .into_any_element(),
753                ),
754                message: format!("Updating {extension_id} extension…"),
755                on_click: Some(Arc::new(|this, window, cx| {
756                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
757                })),
758                tooltip_message: None,
759            });
760        }
761
762        None
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}