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