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