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