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