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