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