activity_indicator.rs

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