1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
2use editor::Editor;
3use extension_host::{ExtensionOperation, ExtensionStore};
4use futures::StreamExt;
5use gpui::{
6 App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _,
7 Render, SharedString, StatefulInteractiveElement, Styled, Window, actions,
8};
9use language::{
10 BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
11 LanguageServerStatusUpdate, ServerHealth,
12};
13use project::{
14 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
15 ProjectEnvironmentEvent,
16 git_store::{GitStoreEvent, Repository},
17};
18use smallvec::SmallVec;
19use std::{
20 cmp::Reverse,
21 collections::HashSet,
22 fmt::Write,
23 path::Path,
24 sync::Arc,
25 time::{Duration, Instant},
26};
27use ui::{
28 ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip,
29 prelude::*,
30};
31use util::truncate_and_trailoff;
32use workspace::{StatusItemView, Workspace, item::ItemHandle};
33
34const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
35
36actions!(
37 activity_indicator,
38 [
39 /// Displays error messages from language servers in the status bar.
40 ShowErrorMessage
41 ]
42);
43
44pub enum Event {
45 ShowStatus {
46 server_name: LanguageServerName,
47 status: SharedString,
48 },
49}
50
51pub struct ActivityIndicator {
52 statuses: Vec<ServerStatus>,
53 project: Entity<Project>,
54 auto_updater: Option<Entity<AutoUpdater>>,
55 context_menu_handle: PopoverMenuHandle<ContextMenu>,
56}
57
58#[derive(Debug)]
59struct ServerStatus {
60 name: LanguageServerName,
61 status: LanguageServerStatusUpdate,
62}
63
64struct PendingWork<'a> {
65 language_server_id: LanguageServerId,
66 progress_token: &'a str,
67 progress: &'a LanguageServerProgress,
68}
69
70struct Content {
71 icon: Option<gpui::AnyElement>,
72 message: String,
73 on_click:
74 Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
75 tooltip_message: Option<String>,
76}
77
78impl ActivityIndicator {
79 pub fn new(
80 workspace: &mut Workspace,
81 languages: Arc<LanguageRegistry>,
82 window: &mut Window,
83 cx: &mut Context<Workspace>,
84 ) -> Entity<ActivityIndicator> {
85 let project = workspace.project().clone();
86 let auto_updater = AutoUpdater::get(cx);
87 let 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_error_message(
284 &mut self,
285 _: &DismissErrorMessage,
286 _: &mut Window,
287 cx: &mut Context<Self>,
288 ) {
289 let error_dismissed = if let Some(updater) = &self.auto_updater {
290 updater.update(cx, |updater, cx| updater.dismiss_error(cx))
291 } else {
292 false
293 };
294 if error_dismissed {
295 return;
296 }
297
298 self.project.update(cx, |project, cx| {
299 if project.last_formatting_failure(cx).is_some() {
300 project.reset_last_formatting_failure(cx);
301 true
302 } else {
303 false
304 }
305 });
306 }
307
308 fn pending_language_server_work<'a>(
309 &self,
310 cx: &'a App,
311 ) -> impl Iterator<Item = PendingWork<'a>> {
312 self.project
313 .read(cx)
314 .language_server_statuses(cx)
315 .rev()
316 .filter_map(|(server_id, status)| {
317 if status.pending_work.is_empty() {
318 None
319 } else {
320 let mut pending_work = status
321 .pending_work
322 .iter()
323 .map(|(token, progress)| PendingWork {
324 language_server_id: server_id,
325 progress_token: token.as_str(),
326 progress,
327 })
328 .collect::<SmallVec<[_; 4]>>();
329 pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
330 Some(pending_work)
331 }
332 })
333 .flatten()
334 }
335
336 fn pending_environment_errors<'a>(
337 &'a self,
338 cx: &'a App,
339 ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
340 self.project.read(cx).shell_environment_errors(cx)
341 }
342
343 fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
344 // Show if any direnv calls failed
345 if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
346 let abs_path = abs_path.clone();
347 return Some(Content {
348 icon: Some(
349 Icon::new(IconName::Warning)
350 .size(IconSize::Small)
351 .into_any_element(),
352 ),
353 message: error.0.clone(),
354 on_click: Some(Arc::new(move |this, window, cx| {
355 this.project.update(cx, |project, cx| {
356 project.remove_environment_error(&abs_path, cx);
357 });
358 window.dispatch_action(Box::new(workspace::OpenLog), cx);
359 })),
360 tooltip_message: None,
361 });
362 }
363 // Show any language server has pending activity.
364 {
365 let mut pending_work = self.pending_language_server_work(cx);
366 if let Some(PendingWork {
367 progress_token,
368 progress,
369 ..
370 }) = pending_work.next()
371 {
372 let mut message = progress
373 .title
374 .as_deref()
375 .unwrap_or(progress_token)
376 .to_string();
377
378 if let Some(percentage) = progress.percentage {
379 write!(&mut message, " ({}%)", percentage).unwrap();
380 }
381
382 if let Some(progress_message) = progress.message.as_ref() {
383 message.push_str(": ");
384 message.push_str(progress_message);
385 }
386
387 let additional_work_count = pending_work.count();
388 if additional_work_count > 0 {
389 write!(&mut message, " + {} more", additional_work_count).unwrap();
390 }
391
392 return Some(Content {
393 icon: Some(
394 Icon::new(IconName::ArrowCircle)
395 .size(IconSize::Small)
396 .with_rotate_animation(2)
397 .into_any_element(),
398 ),
399 message,
400 on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
401 tooltip_message: None,
402 });
403 }
404 }
405
406 if let Some(session) = self
407 .project
408 .read(cx)
409 .dap_store()
410 .read(cx)
411 .sessions()
412 .find(|s| !s.read(cx).is_started())
413 {
414 return Some(Content {
415 icon: Some(
416 Icon::new(IconName::ArrowCircle)
417 .size(IconSize::Small)
418 .with_rotate_animation(2)
419 .into_any_element(),
420 ),
421 message: format!("Debug: {}", session.read(cx).adapter()),
422 tooltip_message: session.read(cx).label().map(|label| label.to_string()),
423 on_click: None,
424 });
425 }
426
427 let current_job = self
428 .project
429 .read(cx)
430 .active_repository(cx)
431 .map(|r| r.read(cx))
432 .and_then(Repository::current_job);
433 // Show any long-running git command
434 if let Some(job_info) = current_job
435 && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
436 {
437 return Some(Content {
438 icon: Some(
439 Icon::new(IconName::ArrowCircle)
440 .size(IconSize::Small)
441 .with_rotate_animation(2)
442 .into_any_element(),
443 ),
444 message: job_info.message.into(),
445 on_click: None,
446 tooltip_message: None,
447 });
448 }
449
450 // Show any language server installation info.
451 let mut downloading = SmallVec::<[_; 3]>::new();
452 let mut checking_for_update = SmallVec::<[_; 3]>::new();
453 let mut failed = SmallVec::<[_; 3]>::new();
454 let mut health_messages = SmallVec::<[_; 3]>::new();
455 let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
456 for status in &self.statuses {
457 match &status.status {
458 LanguageServerStatusUpdate::Binary(
459 BinaryStatus::Starting | BinaryStatus::Stopping,
460 ) => {}
461 LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
462 servers_to_clear_statuses.insert(status.name.clone());
463 }
464 LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
465 checking_for_update.push(status.name.clone());
466 }
467 LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
468 downloading.push(status.name.clone());
469 }
470 LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
471 failed.push(status.name.clone());
472 }
473 LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
474 LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
475 Some(server_status) => {
476 health_messages.push((status.name.clone(), *health, server_status.clone()));
477 }
478 None => {
479 servers_to_clear_statuses.insert(status.name.clone());
480 }
481 },
482 }
483 }
484 self.statuses
485 .retain(|status| !servers_to_clear_statuses.contains(&status.name));
486
487 health_messages.sort_by_key(|(_, health, _)| match health {
488 ServerHealth::Error => 2,
489 ServerHealth::Warning => 1,
490 ServerHealth::Ok => 0,
491 });
492
493 if !downloading.is_empty() {
494 return Some(Content {
495 icon: Some(
496 Icon::new(IconName::Download)
497 .size(IconSize::Small)
498 .into_any_element(),
499 ),
500 message: format!(
501 "Downloading {}...",
502 downloading.iter().map(|name| name.as_ref()).fold(
503 String::new(),
504 |mut acc, s| {
505 if !acc.is_empty() {
506 acc.push_str(", ");
507 }
508 acc.push_str(s);
509 acc
510 }
511 )
512 ),
513 on_click: Some(Arc::new(move |this, window, cx| {
514 this.statuses
515 .retain(|status| !downloading.contains(&status.name));
516 this.dismiss_error_message(&DismissErrorMessage, window, cx)
517 })),
518 tooltip_message: None,
519 });
520 }
521
522 if !checking_for_update.is_empty() {
523 return Some(Content {
524 icon: Some(
525 Icon::new(IconName::Download)
526 .size(IconSize::Small)
527 .into_any_element(),
528 ),
529 message: format!(
530 "Checking for updates to {}...",
531 checking_for_update.iter().map(|name| name.as_ref()).fold(
532 String::new(),
533 |mut acc, s| {
534 if !acc.is_empty() {
535 acc.push_str(", ");
536 }
537 acc.push_str(s);
538 acc
539 }
540 ),
541 ),
542 on_click: Some(Arc::new(move |this, window, cx| {
543 this.statuses
544 .retain(|status| !checking_for_update.contains(&status.name));
545 this.dismiss_error_message(&DismissErrorMessage, window, cx)
546 })),
547 tooltip_message: None,
548 });
549 }
550
551 if !failed.is_empty() {
552 return Some(Content {
553 icon: Some(
554 Icon::new(IconName::Warning)
555 .size(IconSize::Small)
556 .into_any_element(),
557 ),
558 message: format!(
559 "Failed to run {}. Click to show error.",
560 failed
561 .iter()
562 .map(|name| name.as_ref())
563 .fold(String::new(), |mut acc, s| {
564 if !acc.is_empty() {
565 acc.push_str(", ");
566 }
567 acc.push_str(s);
568 acc
569 }),
570 ),
571 on_click: Some(Arc::new(|this, window, cx| {
572 this.show_error_message(&ShowErrorMessage, window, cx)
573 })),
574 tooltip_message: None,
575 });
576 }
577
578 // Show any formatting failure
579 if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
580 return Some(Content {
581 icon: Some(
582 Icon::new(IconName::Warning)
583 .size(IconSize::Small)
584 .into_any_element(),
585 ),
586 message: format!("Formatting failed: {failure}. Click to see logs."),
587 on_click: Some(Arc::new(|indicator, window, cx| {
588 indicator.project.update(cx, |project, cx| {
589 project.reset_last_formatting_failure(cx);
590 });
591 window.dispatch_action(Box::new(workspace::OpenLog), cx);
592 })),
593 tooltip_message: None,
594 });
595 }
596
597 // Show any health messages for the language servers
598 if let Some((server_name, health, message)) = health_messages.pop() {
599 let health_str = match health {
600 ServerHealth::Ok => format!("({server_name}) "),
601 ServerHealth::Warning => format!("({server_name}) Warning: "),
602 ServerHealth::Error => format!("({server_name}) Error: "),
603 };
604 let single_line_message = message
605 .lines()
606 .filter_map(|line| {
607 let line = line.trim();
608 if line.is_empty() { None } else { Some(line) }
609 })
610 .collect::<Vec<_>>()
611 .join(" ");
612 let mut altered_message = single_line_message != message;
613 let truncated_message = truncate_and_trailoff(
614 &single_line_message,
615 MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
616 );
617 altered_message |= truncated_message != single_line_message;
618 let final_message = format!("{health_str}{truncated_message}");
619
620 let tooltip_message = if altered_message {
621 Some(format!("{health_str}{message}"))
622 } else {
623 None
624 };
625
626 return Some(Content {
627 icon: Some(
628 Icon::new(IconName::Warning)
629 .size(IconSize::Small)
630 .into_any_element(),
631 ),
632 message: final_message,
633 tooltip_message,
634 on_click: Some(Arc::new(move |activity_indicator, window, cx| {
635 if altered_message {
636 activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
637 } else {
638 activity_indicator
639 .statuses
640 .retain(|status| status.name != server_name);
641 cx.notify();
642 }
643 })),
644 });
645 }
646
647 // Show any application auto-update info.
648 self.auto_updater
649 .as_ref()
650 .and_then(|updater| match &updater.read(cx).status() {
651 AutoUpdateStatus::Checking => Some(Content {
652 icon: Some(
653 Icon::new(IconName::Download)
654 .size(IconSize::Small)
655 .into_any_element(),
656 ),
657 message: "Checking for Zed updates…".to_string(),
658 on_click: Some(Arc::new(|this, window, cx| {
659 this.dismiss_error_message(&DismissErrorMessage, window, cx)
660 })),
661 tooltip_message: None,
662 }),
663 AutoUpdateStatus::Downloading { version } => Some(Content {
664 icon: Some(
665 Icon::new(IconName::Download)
666 .size(IconSize::Small)
667 .into_any_element(),
668 ),
669 message: "Downloading Zed update…".to_string(),
670 on_click: Some(Arc::new(|this, window, cx| {
671 this.dismiss_error_message(&DismissErrorMessage, window, cx)
672 })),
673 tooltip_message: Some(Self::version_tooltip_message(version)),
674 }),
675 AutoUpdateStatus::Installing { version } => Some(Content {
676 icon: Some(
677 Icon::new(IconName::Download)
678 .size(IconSize::Small)
679 .into_any_element(),
680 ),
681 message: "Installing Zed update…".to_string(),
682 on_click: Some(Arc::new(|this, window, cx| {
683 this.dismiss_error_message(&DismissErrorMessage, window, cx)
684 })),
685 tooltip_message: Some(Self::version_tooltip_message(version)),
686 }),
687 AutoUpdateStatus::Updated { version } => Some(Content {
688 icon: None,
689 message: "Click to restart and update Zed".to_string(),
690 on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
691 tooltip_message: Some(Self::version_tooltip_message(version)),
692 }),
693 AutoUpdateStatus::Errored => Some(Content {
694 icon: Some(
695 Icon::new(IconName::Warning)
696 .size(IconSize::Small)
697 .into_any_element(),
698 ),
699 message: "Auto update failed".to_string(),
700 on_click: Some(Arc::new(|this, window, cx| {
701 this.dismiss_error_message(&DismissErrorMessage, window, cx)
702 })),
703 tooltip_message: None,
704 }),
705 AutoUpdateStatus::Idle => None,
706 })
707 .or_else(|| {
708 if let Some(extension_store) =
709 ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
710 && let Some((extension_id, operation)) =
711 extension_store.outstanding_operations().iter().next()
712 {
713 let (message, icon, rotate) = match operation {
714 ExtensionOperation::Install => (
715 format!("Installing {extension_id} extension…"),
716 IconName::LoadCircle,
717 true,
718 ),
719 ExtensionOperation::Upgrade => (
720 format!("Updating {extension_id} extension…"),
721 IconName::Download,
722 false,
723 ),
724 ExtensionOperation::Remove => (
725 format!("Removing {extension_id} extension…"),
726 IconName::LoadCircle,
727 true,
728 ),
729 };
730
731 Some(Content {
732 icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
733 if rotate {
734 this.with_rotate_animation(3).into_any_element()
735 } else {
736 this.into_any_element()
737 }
738 })),
739 message,
740 on_click: Some(Arc::new(|this, window, cx| {
741 this.dismiss_error_message(&Default::default(), window, cx)
742 })),
743 tooltip_message: None,
744 })
745 } else {
746 None
747 }
748 })
749 }
750
751 fn version_tooltip_message(version: &VersionCheckType) -> String {
752 format!("Version: {}", {
753 match version {
754 auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()),
755 auto_update::VersionCheckType::Semantic(semantic_version) => {
756 semantic_version.to_string()
757 }
758 }
759 })
760 }
761
762 fn toggle_language_server_work_context_menu(
763 &mut self,
764 window: &mut Window,
765 cx: &mut Context<Self>,
766 ) {
767 self.context_menu_handle.toggle(window, cx);
768 }
769}
770
771impl EventEmitter<Event> for ActivityIndicator {}
772
773const MAX_MESSAGE_LEN: usize = 50;
774
775impl Render for ActivityIndicator {
776 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
777 let result = h_flex()
778 .id("activity-indicator")
779 .on_action(cx.listener(Self::show_error_message))
780 .on_action(cx.listener(Self::dismiss_error_message));
781 let Some(content) = self.content_to_render(cx) else {
782 return result;
783 };
784 let this = cx.entity().downgrade();
785 let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
786 result.gap_2().child(
787 PopoverMenu::new("activity-indicator-popover")
788 .trigger(
789 ButtonLike::new("activity-indicator-trigger").child(
790 h_flex()
791 .id("activity-indicator-status")
792 .gap_2()
793 .children(content.icon)
794 .map(|button| {
795 if truncate_content {
796 button
797 .child(
798 Label::new(truncate_and_trailoff(
799 &content.message,
800 MAX_MESSAGE_LEN,
801 ))
802 .size(LabelSize::Small),
803 )
804 .tooltip(Tooltip::text(content.message))
805 } else {
806 button
807 .child(Label::new(content.message).size(LabelSize::Small))
808 .when_some(
809 content.tooltip_message,
810 |this, tooltip_message| {
811 this.tooltip(Tooltip::text(tooltip_message))
812 },
813 )
814 }
815 })
816 .when_some(content.on_click, |this, handler| {
817 this.on_click(cx.listener(move |this, _, window, cx| {
818 handler(this, window, cx);
819 }))
820 .cursor(CursorStyle::PointingHand)
821 }),
822 ),
823 )
824 .anchor(gpui::Corner::BottomLeft)
825 .menu(move |window, cx| {
826 let strong_this = this.upgrade()?;
827 let mut has_work = false;
828 let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
829 for work in strong_this.read(cx).pending_language_server_work(cx) {
830 has_work = true;
831 let this = this.clone();
832 let mut title = work
833 .progress
834 .title
835 .as_deref()
836 .unwrap_or(work.progress_token)
837 .to_owned();
838
839 if work.progress.is_cancellable {
840 let language_server_id = work.language_server_id;
841 let token = work.progress_token.to_string();
842 let title = SharedString::from(title);
843 menu = menu.custom_entry(
844 move |_, _| {
845 h_flex()
846 .w_full()
847 .justify_between()
848 .child(Label::new(title.clone()))
849 .child(Icon::new(IconName::XCircle))
850 .into_any_element()
851 },
852 move |_, cx| {
853 this.update(cx, |this, cx| {
854 this.project.update(cx, |project, cx| {
855 project.cancel_language_server_work(
856 language_server_id,
857 Some(token.clone()),
858 cx,
859 );
860 });
861 this.context_menu_handle.hide(cx);
862 cx.notify();
863 })
864 .ok();
865 },
866 );
867 } else {
868 if let Some(progress_message) = work.progress.message.as_ref() {
869 title.push_str(": ");
870 title.push_str(progress_message);
871 }
872
873 menu = menu.label(title);
874 }
875 }
876 menu
877 });
878 has_work.then_some(menu)
879 }),
880 )
881 }
882}
883
884impl StatusItemView for ActivityIndicator {
885 fn set_active_pane_item(
886 &mut self,
887 _: Option<&dyn ItemHandle>,
888 _window: &mut Window,
889 _: &mut Context<Self>,
890 ) {
891 }
892}
893
894#[cfg(test)]
895mod tests {
896 use gpui::SemanticVersion;
897 use release_channel::AppCommitSha;
898
899 use super::*;
900
901 #[test]
902 fn test_version_tooltip_message() {
903 let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
904 SemanticVersion::new(1, 0, 0),
905 ));
906
907 assert_eq!(message, "Version: 1.0.0");
908
909 let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha(
910 AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()),
911 ));
912
913 assert_eq!(message, "Version: 14d9a41…");
914 }
915}