activity_indicator.rs

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