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