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