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