1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
2use editor::Editor;
3use extension_host::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::{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: 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(|indicator, cx| {
355 indicator.project.update(cx, |project, cx| {
356 project.reset_last_formatting_failure(cx);
357 });
358 cx.dispatch_action(Box::new(workspace::OpenLog));
359 })),
360 });
361 }
362
363 // Show any application auto-update info.
364 if let Some(updater) = &self.auto_updater {
365 return match &updater.read(cx).status() {
366 AutoUpdateStatus::Checking => Some(Content {
367 icon: Some(
368 Icon::new(IconName::Download)
369 .size(IconSize::Small)
370 .into_any_element(),
371 ),
372 message: "Checking for Zed updates…".to_string(),
373 on_click: Some(Arc::new(|this, cx| {
374 this.dismiss_error_message(&DismissErrorMessage, cx)
375 })),
376 }),
377 AutoUpdateStatus::Downloading => Some(Content {
378 icon: Some(
379 Icon::new(IconName::Download)
380 .size(IconSize::Small)
381 .into_any_element(),
382 ),
383 message: "Downloading Zed update…".to_string(),
384 on_click: Some(Arc::new(|this, cx| {
385 this.dismiss_error_message(&DismissErrorMessage, cx)
386 })),
387 }),
388 AutoUpdateStatus::Installing => Some(Content {
389 icon: Some(
390 Icon::new(IconName::Download)
391 .size(IconSize::Small)
392 .into_any_element(),
393 ),
394 message: "Installing Zed update…".to_string(),
395 on_click: Some(Arc::new(|this, cx| {
396 this.dismiss_error_message(&DismissErrorMessage, cx)
397 })),
398 }),
399 AutoUpdateStatus::Updated { binary_path } => Some(Content {
400 icon: None,
401 message: "Click to restart and update Zed".to_string(),
402 on_click: Some(Arc::new({
403 let reload = workspace::Reload {
404 binary_path: Some(binary_path.clone()),
405 };
406 move |_, cx| workspace::reload(&reload, cx)
407 })),
408 }),
409 AutoUpdateStatus::Errored => Some(Content {
410 icon: Some(
411 Icon::new(IconName::Warning)
412 .size(IconSize::Small)
413 .into_any_element(),
414 ),
415 message: "Auto update failed".to_string(),
416 on_click: Some(Arc::new(|this, cx| {
417 this.dismiss_error_message(&DismissErrorMessage, cx)
418 })),
419 }),
420 AutoUpdateStatus::Idle => None,
421 };
422 }
423
424 if let Some(extension_store) =
425 ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
426 {
427 if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
428 return Some(Content {
429 icon: Some(
430 Icon::new(IconName::Download)
431 .size(IconSize::Small)
432 .into_any_element(),
433 ),
434 message: format!("Updating {extension_id} extension…"),
435 on_click: Some(Arc::new(|this, cx| {
436 this.dismiss_error_message(&DismissErrorMessage, cx)
437 })),
438 });
439 }
440 }
441
442 None
443 }
444
445 fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
446 self.context_menu_handle.toggle(cx);
447 }
448}
449
450impl EventEmitter<Event> for ActivityIndicator {}
451
452const MAX_MESSAGE_LEN: usize = 50;
453
454impl Render for ActivityIndicator {
455 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
456 let result = h_flex()
457 .id("activity-indicator")
458 .on_action(cx.listener(Self::show_error_message))
459 .on_action(cx.listener(Self::dismiss_error_message));
460 let Some(content) = self.content_to_render(cx) else {
461 return result;
462 };
463 let this = cx.view().downgrade();
464 let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
465 result.gap_2().child(
466 PopoverMenu::new("activity-indicator-popover")
467 .trigger(
468 ButtonLike::new("activity-indicator-trigger").child(
469 h_flex()
470 .id("activity-indicator-status")
471 .gap_2()
472 .children(content.icon)
473 .map(|button| {
474 if truncate_content {
475 button
476 .child(
477 Label::new(truncate_and_trailoff(
478 &content.message,
479 MAX_MESSAGE_LEN,
480 ))
481 .size(LabelSize::Small),
482 )
483 .tooltip(move |cx| Tooltip::text(&content.message, cx))
484 } else {
485 button.child(Label::new(content.message).size(LabelSize::Small))
486 }
487 })
488 .when_some(content.on_click, |this, handler| {
489 this.on_click(cx.listener(move |this, _, cx| {
490 handler(this, cx);
491 }))
492 .cursor(CursorStyle::PointingHand)
493 }),
494 ),
495 )
496 .anchor(gpui::Corner::BottomLeft)
497 .menu(move |cx| {
498 let strong_this = this.upgrade()?;
499 let mut has_work = false;
500 let menu = ContextMenu::build(cx, |mut menu, cx| {
501 for work in strong_this.read(cx).pending_language_server_work(cx) {
502 has_work = true;
503 let this = this.clone();
504 let mut title = work
505 .progress
506 .title
507 .as_deref()
508 .unwrap_or(work.progress_token)
509 .to_owned();
510
511 if work.progress.is_cancellable {
512 let language_server_id = work.language_server_id;
513 let token = work.progress_token.to_string();
514 let title = SharedString::from(title);
515 menu = menu.custom_entry(
516 move |_| {
517 h_flex()
518 .w_full()
519 .justify_between()
520 .child(Label::new(title.clone()))
521 .child(Icon::new(IconName::XCircle))
522 .into_any_element()
523 },
524 move |cx| {
525 this.update(cx, |this, cx| {
526 this.project.update(cx, |project, cx| {
527 project.cancel_language_server_work(
528 language_server_id,
529 Some(token.clone()),
530 cx,
531 );
532 });
533 this.context_menu_handle.hide(cx);
534 cx.notify();
535 })
536 .ok();
537 },
538 );
539 } else {
540 if let Some(progress_message) = work.progress.message.as_ref() {
541 title.push_str(": ");
542 title.push_str(progress_message);
543 }
544
545 menu = menu.label(title);
546 }
547 }
548 menu
549 });
550 has_work.then_some(menu)
551 }),
552 )
553 }
554}
555
556impl StatusItemView for ActivityIndicator {
557 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
558}