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, 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 str,
65 progress: &'a LanguageServerProgress,
66}
67
68struct Content {
69 icon: Option<gpui::AnyElement>,
70 message: String,
71 on_click:
72 Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
73 tooltip_message: Option<String>,
74}
75
76impl ActivityIndicator {
77 pub fn new(
78 workspace: &mut Workspace,
79 languages: Arc<LanguageRegistry>,
80 window: &mut Window,
81 cx: &mut Context<Workspace>,
82 ) -> Entity<ActivityIndicator> {
83 let project = workspace.project().clone();
84 let auto_updater = AutoUpdater::get(cx);
85 let 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(|(token, progress)| PendingWork {
317 language_server_id: server_id,
318 progress_token: token.as_str(),
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
362 .title
363 .as_deref()
364 .unwrap_or(progress_token)
365 .to_string();
366
367 if let Some(percentage) = progress.percentage {
368 write!(&mut message, " ({}%)", percentage).unwrap();
369 }
370
371 if let Some(progress_message) = progress.message.as_ref() {
372 message.push_str(": ");
373 message.push_str(progress_message);
374 }
375
376 let additional_work_count = pending_work.count();
377 if additional_work_count > 0 {
378 write!(&mut message, " + {} more", additional_work_count).unwrap();
379 }
380
381 return Some(Content {
382 icon: Some(
383 Icon::new(IconName::ArrowCircle)
384 .size(IconSize::Small)
385 .with_rotate_animation(2)
386 .into_any_element(),
387 ),
388 message,
389 on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
390 tooltip_message: None,
391 });
392 }
393 }
394
395 if let Some(session) = self
396 .project
397 .read(cx)
398 .dap_store()
399 .read(cx)
400 .sessions()
401 .find(|s| !s.read(cx).is_started())
402 {
403 return Some(Content {
404 icon: Some(
405 Icon::new(IconName::ArrowCircle)
406 .size(IconSize::Small)
407 .with_rotate_animation(2)
408 .into_any_element(),
409 ),
410 message: format!("Debug: {}", session.read(cx).adapter()),
411 tooltip_message: session.read(cx).label().map(|label| label.to_string()),
412 on_click: None,
413 });
414 }
415
416 let current_job = self
417 .project
418 .read(cx)
419 .active_repository(cx)
420 .map(|r| r.read(cx))
421 .and_then(Repository::current_job);
422 // Show any long-running git command
423 if let Some(job_info) = current_job
424 && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
425 {
426 return Some(Content {
427 icon: Some(
428 Icon::new(IconName::ArrowCircle)
429 .size(IconSize::Small)
430 .with_rotate_animation(2)
431 .into_any_element(),
432 ),
433 message: job_info.message.into(),
434 on_click: None,
435 tooltip_message: None,
436 });
437 }
438
439 // Show any language server installation info.
440 let mut downloading = SmallVec::<[_; 3]>::new();
441 let mut checking_for_update = SmallVec::<[_; 3]>::new();
442 let mut failed = SmallVec::<[_; 3]>::new();
443 let mut health_messages = SmallVec::<[_; 3]>::new();
444 let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
445 for status in &self.statuses {
446 match &status.status {
447 LanguageServerStatusUpdate::Binary(
448 BinaryStatus::Starting | BinaryStatus::Stopping,
449 ) => {}
450 LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
451 servers_to_clear_statuses.insert(status.name.clone());
452 }
453 LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
454 checking_for_update.push(status.name.clone());
455 }
456 LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
457 downloading.push(status.name.clone());
458 }
459 LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
460 failed.push(status.name.clone());
461 }
462 LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
463 LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
464 Some(server_status) => {
465 health_messages.push((status.name.clone(), *health, server_status.clone()));
466 }
467 None => {
468 servers_to_clear_statuses.insert(status.name.clone());
469 }
470 },
471 }
472 }
473 self.statuses
474 .retain(|status| !servers_to_clear_statuses.contains(&status.name));
475
476 health_messages.sort_by_key(|(_, health, _)| match health {
477 ServerHealth::Error => 2,
478 ServerHealth::Warning => 1,
479 ServerHealth::Ok => 0,
480 });
481
482 if !downloading.is_empty() {
483 return Some(Content {
484 icon: Some(
485 Icon::new(IconName::Download)
486 .size(IconSize::Small)
487 .into_any_element(),
488 ),
489 message: format!(
490 "Downloading {}...",
491 downloading.iter().map(|name| name.as_ref()).fold(
492 String::new(),
493 |mut acc, s| {
494 if !acc.is_empty() {
495 acc.push_str(", ");
496 }
497 acc.push_str(s);
498 acc
499 }
500 )
501 ),
502 on_click: Some(Arc::new(move |this, window, cx| {
503 this.statuses
504 .retain(|status| !downloading.contains(&status.name));
505 this.dismiss_message(&DismissMessage, window, cx)
506 })),
507 tooltip_message: None,
508 });
509 }
510
511 if !checking_for_update.is_empty() {
512 return Some(Content {
513 icon: Some(
514 Icon::new(IconName::Download)
515 .size(IconSize::Small)
516 .into_any_element(),
517 ),
518 message: format!(
519 "Checking for updates to {}...",
520 checking_for_update.iter().map(|name| name.as_ref()).fold(
521 String::new(),
522 |mut acc, s| {
523 if !acc.is_empty() {
524 acc.push_str(", ");
525 }
526 acc.push_str(s);
527 acc
528 }
529 ),
530 ),
531 on_click: Some(Arc::new(move |this, window, cx| {
532 this.statuses
533 .retain(|status| !checking_for_update.contains(&status.name));
534 this.dismiss_message(&DismissMessage, window, cx)
535 })),
536 tooltip_message: None,
537 });
538 }
539
540 if !failed.is_empty() {
541 return Some(Content {
542 icon: Some(
543 Icon::new(IconName::Warning)
544 .size(IconSize::Small)
545 .into_any_element(),
546 ),
547 message: format!(
548 "Failed to run {}. Click to show error.",
549 failed
550 .iter()
551 .map(|name| name.as_ref())
552 .fold(String::new(), |mut acc, s| {
553 if !acc.is_empty() {
554 acc.push_str(", ");
555 }
556 acc.push_str(s);
557 acc
558 }),
559 ),
560 on_click: Some(Arc::new(|this, window, cx| {
561 this.show_error_message(&ShowErrorMessage, window, cx)
562 })),
563 tooltip_message: None,
564 });
565 }
566
567 // Show any formatting failure
568 if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
569 return Some(Content {
570 icon: Some(
571 Icon::new(IconName::Warning)
572 .size(IconSize::Small)
573 .into_any_element(),
574 ),
575 message: format!("Formatting failed: {failure}. Click to see logs."),
576 on_click: Some(Arc::new(|indicator, window, cx| {
577 indicator.project.update(cx, |project, cx| {
578 project.reset_last_formatting_failure(cx);
579 });
580 window.dispatch_action(Box::new(workspace::OpenLog), cx);
581 })),
582 tooltip_message: None,
583 });
584 }
585
586 // Show any health messages for the language servers
587 if let Some((server_name, health, message)) = health_messages.pop() {
588 let health_str = match health {
589 ServerHealth::Ok => format!("({server_name}) "),
590 ServerHealth::Warning => format!("({server_name}) Warning: "),
591 ServerHealth::Error => format!("({server_name}) Error: "),
592 };
593 let single_line_message = message
594 .lines()
595 .filter_map(|line| {
596 let line = line.trim();
597 if line.is_empty() { None } else { Some(line) }
598 })
599 .collect::<Vec<_>>()
600 .join(" ");
601 let mut altered_message = single_line_message != message;
602 let truncated_message = truncate_and_trailoff(
603 &single_line_message,
604 MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
605 );
606 altered_message |= truncated_message != single_line_message;
607 let final_message = format!("{health_str}{truncated_message}");
608
609 let tooltip_message = if altered_message {
610 Some(format!("{health_str}{message}"))
611 } else {
612 None
613 };
614
615 return Some(Content {
616 icon: Some(
617 Icon::new(IconName::Warning)
618 .size(IconSize::Small)
619 .into_any_element(),
620 ),
621 message: final_message,
622 tooltip_message,
623 on_click: Some(Arc::new(move |activity_indicator, window, cx| {
624 if altered_message {
625 activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
626 } else {
627 activity_indicator
628 .statuses
629 .retain(|status| status.name != server_name);
630 cx.notify();
631 }
632 })),
633 });
634 }
635
636 // Show any application auto-update info.
637 self.auto_updater
638 .as_ref()
639 .and_then(|updater| match &updater.read(cx).status() {
640 AutoUpdateStatus::Checking => Some(Content {
641 icon: Some(
642 Icon::new(IconName::LoadCircle)
643 .size(IconSize::Small)
644 .with_rotate_animation(3)
645 .into_any_element(),
646 ),
647 message: "Checking for Zed updates…".to_string(),
648 on_click: Some(Arc::new(|this, window, cx| {
649 this.dismiss_message(&DismissMessage, window, cx)
650 })),
651 tooltip_message: None,
652 }),
653 AutoUpdateStatus::Downloading { version } => Some(Content {
654 icon: Some(
655 Icon::new(IconName::Download)
656 .size(IconSize::Small)
657 .into_any_element(),
658 ),
659 message: "Downloading Zed update…".to_string(),
660 on_click: Some(Arc::new(|this, window, cx| {
661 this.dismiss_message(&DismissMessage, window, cx)
662 })),
663 tooltip_message: Some(Self::version_tooltip_message(version)),
664 }),
665 AutoUpdateStatus::Installing { version } => Some(Content {
666 icon: Some(
667 Icon::new(IconName::LoadCircle)
668 .size(IconSize::Small)
669 .with_rotate_animation(3)
670 .into_any_element(),
671 ),
672 message: "Installing Zed update…".to_string(),
673 on_click: Some(Arc::new(|this, window, cx| {
674 this.dismiss_message(&DismissMessage, window, cx)
675 })),
676 tooltip_message: Some(Self::version_tooltip_message(version)),
677 }),
678 AutoUpdateStatus::Updated { version } => Some(Content {
679 icon: None,
680 message: "Click to restart and update Zed".to_string(),
681 on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
682 tooltip_message: Some(Self::version_tooltip_message(version)),
683 }),
684 AutoUpdateStatus::Errored { error } => Some(Content {
685 icon: Some(
686 Icon::new(IconName::Warning)
687 .size(IconSize::Small)
688 .into_any_element(),
689 ),
690 message: "Failed to update Zed".to_string(),
691 on_click: Some(Arc::new(|this, window, cx| {
692 window.dispatch_action(Box::new(workspace::OpenLog), cx);
693 this.dismiss_message(&DismissMessage, window, cx);
694 })),
695 tooltip_message: Some(format!("{error}")),
696 }),
697 AutoUpdateStatus::Idle => None,
698 })
699 .or_else(|| {
700 if let Some(extension_store) =
701 ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
702 && let Some((extension_id, operation)) =
703 extension_store.outstanding_operations().iter().next()
704 {
705 let (message, icon, rotate) = match operation {
706 ExtensionOperation::Install => (
707 format!("Installing {extension_id} extension…"),
708 IconName::LoadCircle,
709 true,
710 ),
711 ExtensionOperation::Upgrade => (
712 format!("Updating {extension_id} extension…"),
713 IconName::Download,
714 false,
715 ),
716 ExtensionOperation::Remove => (
717 format!("Removing {extension_id} extension…"),
718 IconName::LoadCircle,
719 true,
720 ),
721 };
722
723 Some(Content {
724 icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
725 if rotate {
726 this.with_rotate_animation(3).into_any_element()
727 } else {
728 this.into_any_element()
729 }
730 })),
731 message,
732 on_click: Some(Arc::new(|this, window, cx| {
733 this.dismiss_message(&Default::default(), window, cx)
734 })),
735 tooltip_message: None,
736 })
737 } else {
738 None
739 }
740 })
741 }
742
743 fn version_tooltip_message(version: &VersionCheckType) -> String {
744 format!("Version: {}", {
745 match version {
746 auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()),
747 auto_update::VersionCheckType::Semantic(semantic_version) => {
748 semantic_version.to_string()
749 }
750 }
751 })
752 }
753
754 fn toggle_language_server_work_context_menu(
755 &mut self,
756 window: &mut Window,
757 cx: &mut Context<Self>,
758 ) {
759 self.context_menu_handle.toggle(window, cx);
760 }
761}
762
763impl EventEmitter<Event> for ActivityIndicator {}
764
765const MAX_MESSAGE_LEN: usize = 50;
766
767impl Render for ActivityIndicator {
768 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
769 let result = h_flex()
770 .id("activity-indicator")
771 .on_action(cx.listener(Self::show_error_message))
772 .on_action(cx.listener(Self::dismiss_message));
773 let Some(content) = self.content_to_render(cx) else {
774 return result;
775 };
776 let this = cx.entity().downgrade();
777 let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
778 result.gap_2().child(
779 PopoverMenu::new("activity-indicator-popover")
780 .trigger(
781 ButtonLike::new("activity-indicator-trigger").child(
782 h_flex()
783 .id("activity-indicator-status")
784 .gap_2()
785 .children(content.icon)
786 .map(|button| {
787 if truncate_content {
788 button
789 .child(
790 Label::new(truncate_and_trailoff(
791 &content.message,
792 MAX_MESSAGE_LEN,
793 ))
794 .size(LabelSize::Small),
795 )
796 .tooltip(Tooltip::text(content.message))
797 } else {
798 button
799 .child(Label::new(content.message).size(LabelSize::Small))
800 .when_some(
801 content.tooltip_message,
802 |this, tooltip_message| {
803 this.tooltip(Tooltip::text(tooltip_message))
804 },
805 )
806 }
807 })
808 .when_some(content.on_click, |this, handler| {
809 this.on_click(cx.listener(move |this, _, window, cx| {
810 handler(this, window, cx);
811 }))
812 .cursor(CursorStyle::PointingHand)
813 }),
814 ),
815 )
816 .anchor(gpui::Corner::BottomLeft)
817 .menu(move |window, cx| {
818 let strong_this = this.upgrade()?;
819 let mut has_work = false;
820 let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
821 for work in strong_this.read(cx).pending_language_server_work(cx) {
822 has_work = true;
823 let this = this.clone();
824 let mut title = work
825 .progress
826 .title
827 .as_deref()
828 .unwrap_or(work.progress_token)
829 .to_owned();
830
831 if work.progress.is_cancellable {
832 let language_server_id = work.language_server_id;
833 let token = work.progress_token.to_string();
834 let title = SharedString::from(title);
835 menu = menu.custom_entry(
836 move |_, _| {
837 h_flex()
838 .w_full()
839 .justify_between()
840 .child(Label::new(title.clone()))
841 .child(Icon::new(IconName::XCircle))
842 .into_any_element()
843 },
844 move |_, cx| {
845 this.update(cx, |this, cx| {
846 this.project.update(cx, |project, cx| {
847 project.cancel_language_server_work(
848 language_server_id,
849 Some(token.clone()),
850 cx,
851 );
852 });
853 this.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}