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