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