activity_indicator.rs

  1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, 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 = project.update(cx, |project, cx| project.create_buffer(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_error_message(
283        &mut self,
284        _: &DismissErrorMessage,
285        _: &mut Window,
286        cx: &mut Context<Self>,
287    ) {
288        let error_dismissed = if let Some(updater) = &self.auto_updater {
289            updater.update(cx, |updater, cx| updater.dismiss_error(cx))
290        } else {
291            false
292        };
293        if error_dismissed {
294            return;
295        }
296
297        self.project.update(cx, |project, cx| {
298            if project.last_formatting_failure(cx).is_some() {
299                project.reset_last_formatting_failure(cx);
300                true
301            } else {
302                false
303            }
304        });
305    }
306
307    fn pending_language_server_work<'a>(
308        &self,
309        cx: &'a App,
310    ) -> impl Iterator<Item = PendingWork<'a>> {
311        self.project
312            .read(cx)
313            .language_server_statuses(cx)
314            .rev()
315            .filter_map(|(server_id, status)| {
316                if status.pending_work.is_empty() {
317                    None
318                } else {
319                    let mut pending_work = status
320                        .pending_work
321                        .iter()
322                        .map(|(token, progress)| PendingWork {
323                            language_server_id: server_id,
324                            progress_token: token.as_str(),
325                            progress,
326                        })
327                        .collect::<SmallVec<[_; 4]>>();
328                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
329                    Some(pending_work)
330                }
331            })
332            .flatten()
333    }
334
335    fn pending_environment_errors<'a>(
336        &'a self,
337        cx: &'a App,
338    ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
339        self.project.read(cx).shell_environment_errors(cx)
340    }
341
342    fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
343        // Show if any direnv calls failed
344        if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
345            let abs_path = abs_path.clone();
346            return Some(Content {
347                icon: Some(
348                    Icon::new(IconName::Warning)
349                        .size(IconSize::Small)
350                        .into_any_element(),
351                ),
352                message: error.0.clone(),
353                on_click: Some(Arc::new(move |this, window, cx| {
354                    this.project.update(cx, |project, cx| {
355                        project.remove_environment_error(&abs_path, cx);
356                    });
357                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
358                })),
359                tooltip_message: None,
360            });
361        }
362        // Show any language server has pending activity.
363        {
364            let mut pending_work = self.pending_language_server_work(cx);
365            if let Some(PendingWork {
366                progress_token,
367                progress,
368                ..
369            }) = pending_work.next()
370            {
371                let mut message = progress
372                    .title
373                    .as_deref()
374                    .unwrap_or(progress_token)
375                    .to_string();
376
377                if let Some(percentage) = progress.percentage {
378                    write!(&mut message, " ({}%)", percentage).unwrap();
379                }
380
381                if let Some(progress_message) = progress.message.as_ref() {
382                    message.push_str(": ");
383                    message.push_str(progress_message);
384                }
385
386                let additional_work_count = pending_work.count();
387                if additional_work_count > 0 {
388                    write!(&mut message, " + {} more", additional_work_count).unwrap();
389                }
390
391                return Some(Content {
392                    icon: Some(
393                        Icon::new(IconName::ArrowCircle)
394                            .size(IconSize::Small)
395                            .with_rotate_animation(2)
396                            .into_any_element(),
397                    ),
398                    message,
399                    on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
400                    tooltip_message: None,
401                });
402            }
403        }
404
405        if let Some(session) = self
406            .project
407            .read(cx)
408            .dap_store()
409            .read(cx)
410            .sessions()
411            .find(|s| !s.read(cx).is_started())
412        {
413            return Some(Content {
414                icon: Some(
415                    Icon::new(IconName::ArrowCircle)
416                        .size(IconSize::Small)
417                        .with_rotate_animation(2)
418                        .into_any_element(),
419                ),
420                message: format!("Debug: {}", session.read(cx).adapter()),
421                tooltip_message: session.read(cx).label().map(|label| label.to_string()),
422                on_click: None,
423            });
424        }
425
426        let current_job = self
427            .project
428            .read(cx)
429            .active_repository(cx)
430            .map(|r| r.read(cx))
431            .and_then(Repository::current_job);
432        // Show any long-running git command
433        if let Some(job_info) = current_job
434            && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
435        {
436            return Some(Content {
437                icon: Some(
438                    Icon::new(IconName::ArrowCircle)
439                        .size(IconSize::Small)
440                        .with_rotate_animation(2)
441                        .into_any_element(),
442                ),
443                message: job_info.message.into(),
444                on_click: None,
445                tooltip_message: None,
446            });
447        }
448
449        // Show any language server installation info.
450        let mut downloading = SmallVec::<[_; 3]>::new();
451        let mut checking_for_update = SmallVec::<[_; 3]>::new();
452        let mut failed = SmallVec::<[_; 3]>::new();
453        let mut health_messages = SmallVec::<[_; 3]>::new();
454        let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
455        for status in &self.statuses {
456            match &status.status {
457                LanguageServerStatusUpdate::Binary(
458                    BinaryStatus::Starting | BinaryStatus::Stopping,
459                ) => {}
460                LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
461                    servers_to_clear_statuses.insert(status.name.clone());
462                }
463                LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
464                    checking_for_update.push(status.name.clone());
465                }
466                LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
467                    downloading.push(status.name.clone());
468                }
469                LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
470                    failed.push(status.name.clone());
471                }
472                LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
473                LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
474                    Some(server_status) => {
475                        health_messages.push((status.name.clone(), *health, server_status.clone()));
476                    }
477                    None => {
478                        servers_to_clear_statuses.insert(status.name.clone());
479                    }
480                },
481            }
482        }
483        self.statuses
484            .retain(|status| !servers_to_clear_statuses.contains(&status.name));
485
486        health_messages.sort_by_key(|(_, health, _)| match health {
487            ServerHealth::Error => 2,
488            ServerHealth::Warning => 1,
489            ServerHealth::Ok => 0,
490        });
491
492        if !downloading.is_empty() {
493            return Some(Content {
494                icon: Some(
495                    Icon::new(IconName::Download)
496                        .size(IconSize::Small)
497                        .into_any_element(),
498                ),
499                message: format!(
500                    "Downloading {}...",
501                    downloading.iter().map(|name| name.as_ref()).fold(
502                        String::new(),
503                        |mut acc, s| {
504                            if !acc.is_empty() {
505                                acc.push_str(", ");
506                            }
507                            acc.push_str(s);
508                            acc
509                        }
510                    )
511                ),
512                on_click: Some(Arc::new(move |this, window, cx| {
513                    this.statuses
514                        .retain(|status| !downloading.contains(&status.name));
515                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
516                })),
517                tooltip_message: None,
518            });
519        }
520
521        if !checking_for_update.is_empty() {
522            return Some(Content {
523                icon: Some(
524                    Icon::new(IconName::Download)
525                        .size(IconSize::Small)
526                        .into_any_element(),
527                ),
528                message: format!(
529                    "Checking for updates to {}...",
530                    checking_for_update.iter().map(|name| name.as_ref()).fold(
531                        String::new(),
532                        |mut acc, s| {
533                            if !acc.is_empty() {
534                                acc.push_str(", ");
535                            }
536                            acc.push_str(s);
537                            acc
538                        }
539                    ),
540                ),
541                on_click: Some(Arc::new(move |this, window, cx| {
542                    this.statuses
543                        .retain(|status| !checking_for_update.contains(&status.name));
544                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
545                })),
546                tooltip_message: None,
547            });
548        }
549
550        if !failed.is_empty() {
551            return Some(Content {
552                icon: Some(
553                    Icon::new(IconName::Warning)
554                        .size(IconSize::Small)
555                        .into_any_element(),
556                ),
557                message: format!(
558                    "Failed to run {}. Click to show error.",
559                    failed
560                        .iter()
561                        .map(|name| name.as_ref())
562                        .fold(String::new(), |mut acc, s| {
563                            if !acc.is_empty() {
564                                acc.push_str(", ");
565                            }
566                            acc.push_str(s);
567                            acc
568                        }),
569                ),
570                on_click: Some(Arc::new(|this, window, cx| {
571                    this.show_error_message(&ShowErrorMessage, window, cx)
572                })),
573                tooltip_message: None,
574            });
575        }
576
577        // Show any formatting failure
578        if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
579            return Some(Content {
580                icon: Some(
581                    Icon::new(IconName::Warning)
582                        .size(IconSize::Small)
583                        .into_any_element(),
584                ),
585                message: format!("Formatting failed: {failure}. Click to see logs."),
586                on_click: Some(Arc::new(|indicator, window, cx| {
587                    indicator.project.update(cx, |project, cx| {
588                        project.reset_last_formatting_failure(cx);
589                    });
590                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
591                })),
592                tooltip_message: None,
593            });
594        }
595
596        // Show any health messages for the language servers
597        if let Some((server_name, health, message)) = health_messages.pop() {
598            let health_str = match health {
599                ServerHealth::Ok => format!("({server_name}) "),
600                ServerHealth::Warning => format!("({server_name}) Warning: "),
601                ServerHealth::Error => format!("({server_name}) Error: "),
602            };
603            let single_line_message = message
604                .lines()
605                .filter_map(|line| {
606                    let line = line.trim();
607                    if line.is_empty() { None } else { Some(line) }
608                })
609                .collect::<Vec<_>>()
610                .join(" ");
611            let mut altered_message = single_line_message != message;
612            let truncated_message = truncate_and_trailoff(
613                &single_line_message,
614                MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
615            );
616            altered_message |= truncated_message != single_line_message;
617            let final_message = format!("{health_str}{truncated_message}");
618
619            let tooltip_message = if altered_message {
620                Some(format!("{health_str}{message}"))
621            } else {
622                None
623            };
624
625            return Some(Content {
626                icon: Some(
627                    Icon::new(IconName::Warning)
628                        .size(IconSize::Small)
629                        .into_any_element(),
630                ),
631                message: final_message,
632                tooltip_message,
633                on_click: Some(Arc::new(move |activity_indicator, window, cx| {
634                    if altered_message {
635                        activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
636                    } else {
637                        activity_indicator
638                            .statuses
639                            .retain(|status| status.name != server_name);
640                        cx.notify();
641                    }
642                })),
643            });
644        }
645
646        // Show any application auto-update info.
647        self.auto_updater
648            .as_ref()
649            .and_then(|updater| match &updater.read(cx).status() {
650                AutoUpdateStatus::Checking => Some(Content {
651                    icon: Some(
652                        Icon::new(IconName::Download)
653                            .size(IconSize::Small)
654                            .into_any_element(),
655                    ),
656                    message: "Checking for Zed updates…".to_string(),
657                    on_click: Some(Arc::new(|this, window, cx| {
658                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
659                    })),
660                    tooltip_message: None,
661                }),
662                AutoUpdateStatus::Downloading { version } => Some(Content {
663                    icon: Some(
664                        Icon::new(IconName::Download)
665                            .size(IconSize::Small)
666                            .into_any_element(),
667                    ),
668                    message: "Downloading Zed update…".to_string(),
669                    on_click: Some(Arc::new(|this, window, cx| {
670                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
671                    })),
672                    tooltip_message: Some(Self::version_tooltip_message(version)),
673                }),
674                AutoUpdateStatus::Installing { version } => Some(Content {
675                    icon: Some(
676                        Icon::new(IconName::Download)
677                            .size(IconSize::Small)
678                            .into_any_element(),
679                    ),
680                    message: "Installing Zed update…".to_string(),
681                    on_click: Some(Arc::new(|this, window, cx| {
682                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
683                    })),
684                    tooltip_message: Some(Self::version_tooltip_message(version)),
685                }),
686                AutoUpdateStatus::Updated { version } => Some(Content {
687                    icon: None,
688                    message: "Click to restart and update Zed".to_string(),
689                    on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
690                    tooltip_message: Some(Self::version_tooltip_message(version)),
691                }),
692                AutoUpdateStatus::Errored => Some(Content {
693                    icon: Some(
694                        Icon::new(IconName::Warning)
695                            .size(IconSize::Small)
696                            .into_any_element(),
697                    ),
698                    message: "Auto update failed".to_string(),
699                    on_click: Some(Arc::new(|this, window, cx| {
700                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
701                    })),
702                    tooltip_message: None,
703                }),
704                AutoUpdateStatus::Idle => None,
705            })
706            .or_else(|| {
707                if let Some(extension_store) =
708                    ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
709                    && let Some((extension_id, operation)) =
710                        extension_store.outstanding_operations().iter().next()
711                {
712                    let (message, icon, rotate) = match operation {
713                        ExtensionOperation::Install => (
714                            format!("Installing {extension_id} extension…"),
715                            IconName::LoadCircle,
716                            true,
717                        ),
718                        ExtensionOperation::Upgrade => (
719                            format!("Updating {extension_id} extension…"),
720                            IconName::Download,
721                            false,
722                        ),
723                        ExtensionOperation::Remove => (
724                            format!("Removing {extension_id} extension…"),
725                            IconName::LoadCircle,
726                            true,
727                        ),
728                    };
729
730                    Some(Content {
731                        icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
732                            if rotate {
733                                this.with_rotate_animation(3).into_any_element()
734                            } else {
735                                this.into_any_element()
736                            }
737                        })),
738                        message,
739                        on_click: Some(Arc::new(|this, window, cx| {
740                            this.dismiss_error_message(&Default::default(), window, cx)
741                        })),
742                        tooltip_message: None,
743                    })
744                } else {
745                    None
746                }
747            })
748    }
749
750    fn version_tooltip_message(version: &VersionCheckType) -> String {
751        format!("Version: {}", {
752            match version {
753                auto_update::VersionCheckType::Sha(sha) => format!("{}", sha.short()),
754                auto_update::VersionCheckType::Semantic(semantic_version) => {
755                    semantic_version.to_string()
756                }
757            }
758        })
759    }
760
761    fn toggle_language_server_work_context_menu(
762        &mut self,
763        window: &mut Window,
764        cx: &mut Context<Self>,
765    ) {
766        self.context_menu_handle.toggle(window, cx);
767    }
768}
769
770impl EventEmitter<Event> for ActivityIndicator {}
771
772const MAX_MESSAGE_LEN: usize = 50;
773
774impl Render for ActivityIndicator {
775    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
776        let result = h_flex()
777            .id("activity-indicator")
778            .on_action(cx.listener(Self::show_error_message))
779            .on_action(cx.listener(Self::dismiss_error_message));
780        let Some(content) = self.content_to_render(cx) else {
781            return result;
782        };
783        let this = cx.entity().downgrade();
784        let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
785        result.gap_2().child(
786            PopoverMenu::new("activity-indicator-popover")
787                .trigger(
788                    ButtonLike::new("activity-indicator-trigger").child(
789                        h_flex()
790                            .id("activity-indicator-status")
791                            .gap_2()
792                            .children(content.icon)
793                            .map(|button| {
794                                if truncate_content {
795                                    button
796                                        .child(
797                                            Label::new(truncate_and_trailoff(
798                                                &content.message,
799                                                MAX_MESSAGE_LEN,
800                                            ))
801                                            .size(LabelSize::Small),
802                                        )
803                                        .tooltip(Tooltip::text(content.message))
804                                } else {
805                                    button
806                                        .child(Label::new(content.message).size(LabelSize::Small))
807                                        .when_some(
808                                            content.tooltip_message,
809                                            |this, tooltip_message| {
810                                                this.tooltip(Tooltip::text(tooltip_message))
811                                            },
812                                        )
813                                }
814                            })
815                            .when_some(content.on_click, |this, handler| {
816                                this.on_click(cx.listener(move |this, _, window, cx| {
817                                    handler(this, window, cx);
818                                }))
819                                .cursor(CursorStyle::PointingHand)
820                            }),
821                    ),
822                )
823                .anchor(gpui::Corner::BottomLeft)
824                .menu(move |window, cx| {
825                    let strong_this = this.upgrade()?;
826                    let mut has_work = false;
827                    let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
828                        for work in strong_this.read(cx).pending_language_server_work(cx) {
829                            has_work = true;
830                            let this = this.clone();
831                            let mut title = work
832                                .progress
833                                .title
834                                .as_deref()
835                                .unwrap_or(work.progress_token)
836                                .to_owned();
837
838                            if work.progress.is_cancellable {
839                                let language_server_id = work.language_server_id;
840                                let token = work.progress_token.to_string();
841                                let title = SharedString::from(title);
842                                menu = menu.custom_entry(
843                                    move |_, _| {
844                                        h_flex()
845                                            .w_full()
846                                            .justify_between()
847                                            .child(Label::new(title.clone()))
848                                            .child(Icon::new(IconName::XCircle))
849                                            .into_any_element()
850                                    },
851                                    move |_, cx| {
852                                        this.update(cx, |this, cx| {
853                                            this.project.update(cx, |project, cx| {
854                                                project.cancel_language_server_work(
855                                                    language_server_id,
856                                                    Some(token.clone()),
857                                                    cx,
858                                                );
859                                            });
860                                            this.context_menu_handle.hide(cx);
861                                            cx.notify();
862                                        })
863                                        .ok();
864                                    },
865                                );
866                            } else {
867                                if let Some(progress_message) = work.progress.message.as_ref() {
868                                    title.push_str(": ");
869                                    title.push_str(progress_message);
870                                }
871
872                                menu = menu.label(title);
873                            }
874                        }
875                        menu
876                    });
877                    has_work.then_some(menu)
878                }),
879        )
880    }
881}
882
883impl StatusItemView for ActivityIndicator {
884    fn set_active_pane_item(
885        &mut self,
886        _: Option<&dyn ItemHandle>,
887        _window: &mut Window,
888        _: &mut Context<Self>,
889    ) {
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use gpui::SemanticVersion;
896    use release_channel::AppCommitSha;
897
898    use super::*;
899
900    #[test]
901    fn test_version_tooltip_message() {
902        let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
903            SemanticVersion::new(1, 0, 0),
904        ));
905
906        assert_eq!(message, "Version: 1.0.0");
907
908        let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha(
909            AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()),
910        ));
911
912        assert_eq!(message, "Version: 14d9a41…");
913    }
914}