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