activity_indicator.rs

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