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