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