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