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