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