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