1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
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::{BinaryStatus, LanguageRegistry, LanguageServerId};
11use project::{
12 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
13 ProjectEnvironmentEvent,
14};
15use smallvec::SmallVec;
16use std::{cmp::Reverse, fmt::Write, path::Path, sync::Arc, time::Duration};
17use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
18use util::truncate_and_trailoff;
19use workspace::{StatusItemView, Workspace, item::ItemHandle};
20
21actions!(activity_indicator, [ShowErrorMessage]);
22
23pub enum Event {
24 ShowError {
25 server_name: SharedString,
26 error: String,
27 },
28}
29
30pub struct ActivityIndicator {
31 statuses: Vec<ServerStatus>,
32 project: Entity<Project>,
33 auto_updater: Option<Entity<AutoUpdater>>,
34 context_menu_handle: PopoverMenuHandle<ContextMenu>,
35}
36
37struct ServerStatus {
38 name: SharedString,
39 status: BinaryStatus,
40}
41
42struct PendingWork<'a> {
43 language_server_id: LanguageServerId,
44 progress_token: &'a str,
45 progress: &'a LanguageServerProgress,
46}
47
48struct Content {
49 icon: Option<gpui::AnyElement>,
50 message: String,
51 on_click:
52 Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
53}
54
55impl ActivityIndicator {
56 pub fn new(
57 workspace: &mut Workspace,
58 languages: Arc<LanguageRegistry>,
59 window: &mut Window,
60 cx: &mut Context<Workspace>,
61 ) -> Entity<ActivityIndicator> {
62 let project = workspace.project().clone();
63 let auto_updater = AutoUpdater::get(cx);
64 let this = cx.new(|cx| {
65 let mut status_events = languages.language_server_binary_statuses();
66 cx.spawn(async move |this, cx| {
67 while let Some((name, status)) = status_events.next().await {
68 this.update(cx, |this: &mut ActivityIndicator, cx| {
69 this.statuses.retain(|s| s.name != name);
70 this.statuses.push(ServerStatus { name, status });
71 cx.notify();
72 })?;
73 }
74 anyhow::Ok(())
75 })
76 .detach();
77
78 let mut status_events = languages.dap_server_binary_statuses();
79 cx.spawn(async move |this, cx| {
80 while let Some((name, status)) = status_events.next().await {
81 this.update(cx, |this, cx| {
82 this.statuses.retain(|s| s.name != name);
83 this.statuses.push(ServerStatus { name, status });
84 cx.notify();
85 })?;
86 }
87 anyhow::Ok(())
88 })
89 .detach();
90
91 cx.subscribe(
92 &project.read(cx).lsp_store(),
93 |_, _, event, cx| match event {
94 LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
95 _ => {}
96 },
97 )
98 .detach();
99
100 cx.subscribe(
101 &project.read(cx).environment().clone(),
102 |_, _, event, cx| match event {
103 ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
104 },
105 )
106 .detach();
107
108 if let Some(auto_updater) = auto_updater.as_ref() {
109 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
110 }
111
112 Self {
113 statuses: Default::default(),
114 project: project.clone(),
115 auto_updater,
116 context_menu_handle: Default::default(),
117 }
118 });
119
120 cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
121 Event::ShowError { server_name, error } => {
122 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
123 let project = project.clone();
124 let error = error.clone();
125 let server_name = server_name.clone();
126 cx.spawn_in(window, async move |workspace, cx| {
127 let buffer = create_buffer.await?;
128 buffer.update(cx, |buffer, cx| {
129 buffer.edit(
130 [(
131 0..0,
132 format!("Language server error: {}\n\n{}", server_name, error),
133 )],
134 None,
135 cx,
136 );
137 buffer.set_capability(language::Capability::ReadOnly, cx);
138 })?;
139 workspace.update_in(cx, |workspace, window, cx| {
140 workspace.add_item_to_active_pane(
141 Box::new(cx.new(|cx| {
142 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
143 })),
144 None,
145 true,
146 window,
147 cx,
148 );
149 })?;
150
151 anyhow::Ok(())
152 })
153 .detach();
154 }
155 })
156 .detach();
157 this
158 }
159
160 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
161 self.statuses.retain(|status| {
162 if let BinaryStatus::Failed { error } = &status.status {
163 cx.emit(Event::ShowError {
164 server_name: status.name.clone(),
165 error: error.clone(),
166 });
167 false
168 } else {
169 true
170 }
171 });
172
173 cx.notify();
174 }
175
176 fn dismiss_error_message(
177 &mut self,
178 _: &DismissErrorMessage,
179 _: &mut Window,
180 cx: &mut Context<Self>,
181 ) {
182 if let Some(updater) = &self.auto_updater {
183 updater.update(cx, |updater, cx| {
184 updater.dismiss_error(cx);
185 });
186 }
187 cx.notify();
188 }
189
190 fn pending_language_server_work<'a>(
191 &self,
192 cx: &'a App,
193 ) -> impl Iterator<Item = PendingWork<'a>> {
194 self.project
195 .read(cx)
196 .language_server_statuses(cx)
197 .rev()
198 .filter_map(|(server_id, status)| {
199 if status.pending_work.is_empty() {
200 None
201 } else {
202 let mut pending_work = status
203 .pending_work
204 .iter()
205 .map(|(token, progress)| PendingWork {
206 language_server_id: server_id,
207 progress_token: token.as_str(),
208 progress,
209 })
210 .collect::<SmallVec<[_; 4]>>();
211 pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
212 Some(pending_work)
213 }
214 })
215 .flatten()
216 }
217
218 fn pending_environment_errors<'a>(
219 &'a self,
220 cx: &'a App,
221 ) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
222 self.project.read(cx).shell_environment_errors(cx)
223 }
224
225 fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
226 // Show if any direnv calls failed
227 if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
228 let abs_path = abs_path.clone();
229 return Some(Content {
230 icon: Some(
231 Icon::new(IconName::Warning)
232 .size(IconSize::Small)
233 .into_any_element(),
234 ),
235 message: error.0.clone(),
236 on_click: Some(Arc::new(move |this, window, cx| {
237 this.project.update(cx, |project, cx| {
238 project.remove_environment_error(&abs_path, cx);
239 });
240 window.dispatch_action(Box::new(workspace::OpenLog), cx);
241 })),
242 });
243 }
244 // Show any language server has pending activity.
245 let mut pending_work = self.pending_language_server_work(cx);
246 if let Some(PendingWork {
247 progress_token,
248 progress,
249 ..
250 }) = pending_work.next()
251 {
252 let mut message = progress
253 .title
254 .as_deref()
255 .unwrap_or(progress_token)
256 .to_string();
257
258 if let Some(percentage) = progress.percentage {
259 write!(&mut message, " ({}%)", percentage).unwrap();
260 }
261
262 if let Some(progress_message) = progress.message.as_ref() {
263 message.push_str(": ");
264 message.push_str(progress_message);
265 }
266
267 let additional_work_count = pending_work.count();
268 if additional_work_count > 0 {
269 write!(&mut message, " + {} more", additional_work_count).unwrap();
270 }
271
272 return Some(Content {
273 icon: Some(
274 Icon::new(IconName::ArrowCircle)
275 .size(IconSize::Small)
276 .with_animation(
277 "arrow-circle",
278 Animation::new(Duration::from_secs(2)).repeat(),
279 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
280 )
281 .into_any_element(),
282 ),
283 message,
284 on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
285 });
286 }
287
288 // Show any language server installation info.
289 let mut downloading = SmallVec::<[_; 3]>::new();
290 let mut checking_for_update = SmallVec::<[_; 3]>::new();
291 let mut failed = SmallVec::<[_; 3]>::new();
292 for status in &self.statuses {
293 match status.status {
294 BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
295 BinaryStatus::Downloading => downloading.push(status.name.clone()),
296 BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
297 BinaryStatus::None => {}
298 }
299 }
300
301 if !downloading.is_empty() {
302 return Some(Content {
303 icon: Some(
304 Icon::new(IconName::Download)
305 .size(IconSize::Small)
306 .into_any_element(),
307 ),
308 message: format!(
309 "Downloading {}...",
310 downloading.iter().map(|name| name.as_ref()).fold(
311 String::new(),
312 |mut acc, s| {
313 if !acc.is_empty() {
314 acc.push_str(", ");
315 }
316 acc.push_str(s);
317 acc
318 }
319 )
320 ),
321 on_click: Some(Arc::new(move |this, window, cx| {
322 this.statuses
323 .retain(|status| !downloading.contains(&status.name));
324 this.dismiss_error_message(&DismissErrorMessage, window, cx)
325 })),
326 });
327 }
328
329 if !checking_for_update.is_empty() {
330 return Some(Content {
331 icon: Some(
332 Icon::new(IconName::Download)
333 .size(IconSize::Small)
334 .into_any_element(),
335 ),
336 message: format!(
337 "Checking for updates to {}...",
338 checking_for_update.iter().map(|name| name.as_ref()).fold(
339 String::new(),
340 |mut acc, s| {
341 if !acc.is_empty() {
342 acc.push_str(", ");
343 }
344 acc.push_str(s);
345 acc
346 }
347 ),
348 ),
349 on_click: Some(Arc::new(move |this, window, cx| {
350 this.statuses
351 .retain(|status| !checking_for_update.contains(&status.name));
352 this.dismiss_error_message(&DismissErrorMessage, window, cx)
353 })),
354 });
355 }
356
357 if !failed.is_empty() {
358 return Some(Content {
359 icon: Some(
360 Icon::new(IconName::Warning)
361 .size(IconSize::Small)
362 .into_any_element(),
363 ),
364 message: format!(
365 "Failed to run {}. Click to show error.",
366 failed
367 .iter()
368 .map(|name| name.as_ref())
369 .fold(String::new(), |mut acc, s| {
370 if !acc.is_empty() {
371 acc.push_str(", ");
372 }
373 acc.push_str(s);
374 acc
375 }),
376 ),
377 on_click: Some(Arc::new(|this, window, cx| {
378 this.show_error_message(&Default::default(), window, cx)
379 })),
380 });
381 }
382
383 // Show any formatting failure
384 if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
385 return Some(Content {
386 icon: Some(
387 Icon::new(IconName::Warning)
388 .size(IconSize::Small)
389 .into_any_element(),
390 ),
391 message: format!("Formatting failed: {}. Click to see logs.", failure),
392 on_click: Some(Arc::new(|indicator, window, cx| {
393 indicator.project.update(cx, |project, cx| {
394 project.reset_last_formatting_failure(cx);
395 });
396 window.dispatch_action(Box::new(workspace::OpenLog), cx);
397 })),
398 });
399 }
400
401 // Show any application auto-update info.
402 if let Some(updater) = &self.auto_updater {
403 return match &updater.read(cx).status() {
404 AutoUpdateStatus::Checking => Some(Content {
405 icon: Some(
406 Icon::new(IconName::Download)
407 .size(IconSize::Small)
408 .into_any_element(),
409 ),
410 message: "Checking for Zed updates…".to_string(),
411 on_click: Some(Arc::new(|this, window, cx| {
412 this.dismiss_error_message(&DismissErrorMessage, window, cx)
413 })),
414 }),
415 AutoUpdateStatus::Downloading => Some(Content {
416 icon: Some(
417 Icon::new(IconName::Download)
418 .size(IconSize::Small)
419 .into_any_element(),
420 ),
421 message: "Downloading Zed update…".to_string(),
422 on_click: Some(Arc::new(|this, window, cx| {
423 this.dismiss_error_message(&DismissErrorMessage, window, cx)
424 })),
425 }),
426 AutoUpdateStatus::Installing => Some(Content {
427 icon: Some(
428 Icon::new(IconName::Download)
429 .size(IconSize::Small)
430 .into_any_element(),
431 ),
432 message: "Installing Zed update…".to_string(),
433 on_click: Some(Arc::new(|this, window, cx| {
434 this.dismiss_error_message(&DismissErrorMessage, window, cx)
435 })),
436 }),
437 AutoUpdateStatus::Updated { binary_path } => Some(Content {
438 icon: None,
439 message: "Click to restart and update Zed".to_string(),
440 on_click: Some(Arc::new({
441 let reload = workspace::Reload {
442 binary_path: Some(binary_path.clone()),
443 };
444 move |_, _, cx| workspace::reload(&reload, cx)
445 })),
446 }),
447 AutoUpdateStatus::Errored => Some(Content {
448 icon: Some(
449 Icon::new(IconName::Warning)
450 .size(IconSize::Small)
451 .into_any_element(),
452 ),
453 message: "Auto update failed".to_string(),
454 on_click: Some(Arc::new(|this, window, cx| {
455 this.dismiss_error_message(&DismissErrorMessage, window, cx)
456 })),
457 }),
458 AutoUpdateStatus::Idle => None,
459 };
460 }
461
462 if let Some(extension_store) =
463 ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
464 {
465 if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
466 return Some(Content {
467 icon: Some(
468 Icon::new(IconName::Download)
469 .size(IconSize::Small)
470 .into_any_element(),
471 ),
472 message: format!("Updating {extension_id} extension…"),
473 on_click: Some(Arc::new(|this, window, cx| {
474 this.dismiss_error_message(&DismissErrorMessage, window, cx)
475 })),
476 });
477 }
478 }
479
480 None
481 }
482
483 fn toggle_language_server_work_context_menu(
484 &mut self,
485 window: &mut Window,
486 cx: &mut Context<Self>,
487 ) {
488 self.context_menu_handle.toggle(window, cx);
489 }
490}
491
492impl EventEmitter<Event> for ActivityIndicator {}
493
494const MAX_MESSAGE_LEN: usize = 50;
495
496impl Render for ActivityIndicator {
497 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
498 let result = h_flex()
499 .id("activity-indicator")
500 .on_action(cx.listener(Self::show_error_message))
501 .on_action(cx.listener(Self::dismiss_error_message));
502 let Some(content) = self.content_to_render(cx) else {
503 return result;
504 };
505 let this = cx.entity().downgrade();
506 let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
507 result.gap_2().child(
508 PopoverMenu::new("activity-indicator-popover")
509 .trigger(
510 ButtonLike::new("activity-indicator-trigger").child(
511 h_flex()
512 .id("activity-indicator-status")
513 .gap_2()
514 .children(content.icon)
515 .map(|button| {
516 if truncate_content {
517 button
518 .child(
519 Label::new(truncate_and_trailoff(
520 &content.message,
521 MAX_MESSAGE_LEN,
522 ))
523 .size(LabelSize::Small),
524 )
525 .tooltip(Tooltip::text(content.message))
526 } else {
527 button.child(Label::new(content.message).size(LabelSize::Small))
528 }
529 })
530 .when_some(content.on_click, |this, handler| {
531 this.on_click(cx.listener(move |this, _, window, cx| {
532 handler(this, window, cx);
533 }))
534 .cursor(CursorStyle::PointingHand)
535 }),
536 ),
537 )
538 .anchor(gpui::Corner::BottomLeft)
539 .menu(move |window, cx| {
540 let strong_this = this.upgrade()?;
541 let mut has_work = false;
542 let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
543 for work in strong_this.read(cx).pending_language_server_work(cx) {
544 has_work = true;
545 let this = this.clone();
546 let mut title = work
547 .progress
548 .title
549 .as_deref()
550 .unwrap_or(work.progress_token)
551 .to_owned();
552
553 if work.progress.is_cancellable {
554 let language_server_id = work.language_server_id;
555 let token = work.progress_token.to_string();
556 let title = SharedString::from(title);
557 menu = menu.custom_entry(
558 move |_, _| {
559 h_flex()
560 .w_full()
561 .justify_between()
562 .child(Label::new(title.clone()))
563 .child(Icon::new(IconName::XCircle))
564 .into_any_element()
565 },
566 move |_, cx| {
567 this.update(cx, |this, cx| {
568 this.project.update(cx, |project, cx| {
569 project.cancel_language_server_work(
570 language_server_id,
571 Some(token.clone()),
572 cx,
573 );
574 });
575 this.context_menu_handle.hide(cx);
576 cx.notify();
577 })
578 .ok();
579 },
580 );
581 } else {
582 if let Some(progress_message) = work.progress.message.as_ref() {
583 title.push_str(": ");
584 title.push_str(progress_message);
585 }
586
587 menu = menu.label(title);
588 }
589 }
590 menu
591 });
592 has_work.then_some(menu)
593 }),
594 )
595 }
596}
597
598impl StatusItemView for ActivityIndicator {
599 fn set_active_pane_item(
600 &mut self,
601 _: Option<&dyn ItemHandle>,
602 _window: &mut Window,
603 _: &mut Context<Self>,
604 ) {
605 }
606}