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