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