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