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.0.as_ref())
231 }
232 LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
233 LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
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!("Downloading {}...", downloading.join(", "),),
246 on_click: None,
247 });
248 }
249
250 if !checking_for_update.is_empty() {
251 return Some(Content {
252 icon: Some(
253 Icon::new(IconName::Download)
254 .size(IconSize::Small)
255 .into_any_element(),
256 ),
257 message: format!(
258 "Checking for updates to {}...",
259 checking_for_update.join(", "),
260 ),
261 on_click: None,
262 });
263 }
264
265 if !failed.is_empty() {
266 return Some(Content {
267 icon: Some(
268 Icon::new(IconName::Warning)
269 .size(IconSize::Small)
270 .into_any_element(),
271 ),
272 message: format!(
273 "Failed to download {}. Click to show error.",
274 failed.join(", "),
275 ),
276 on_click: Some(Arc::new(|this, cx| {
277 this.show_error_message(&Default::default(), cx)
278 })),
279 });
280 }
281
282 // Show any formatting failure
283 if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
284 return Some(Content {
285 icon: Some(
286 Icon::new(IconName::Warning)
287 .size(IconSize::Small)
288 .into_any_element(),
289 ),
290 message: format!("Formatting failed: {}. Click to see logs.", failure),
291 on_click: Some(Arc::new(|_, cx| {
292 cx.dispatch_action(Box::new(workspace::OpenLog));
293 })),
294 });
295 }
296
297 // Show any application auto-update info.
298 if let Some(updater) = &self.auto_updater {
299 return match &updater.read(cx).status() {
300 AutoUpdateStatus::Checking => Some(Content {
301 icon: Some(
302 Icon::new(IconName::Download)
303 .size(IconSize::Small)
304 .into_any_element(),
305 ),
306 message: "Checking for Zed updates…".to_string(),
307 on_click: None,
308 }),
309 AutoUpdateStatus::Downloading => Some(Content {
310 icon: Some(
311 Icon::new(IconName::Download)
312 .size(IconSize::Small)
313 .into_any_element(),
314 ),
315 message: "Downloading Zed update…".to_string(),
316 on_click: None,
317 }),
318 AutoUpdateStatus::Installing => Some(Content {
319 icon: Some(
320 Icon::new(IconName::Download)
321 .size(IconSize::Small)
322 .into_any_element(),
323 ),
324 message: "Installing Zed update…".to_string(),
325 on_click: None,
326 }),
327 AutoUpdateStatus::Updated { binary_path } => Some(Content {
328 icon: None,
329 message: "Click to restart and update Zed".to_string(),
330 on_click: Some(Arc::new({
331 let reload = workspace::Reload {
332 binary_path: Some(binary_path.clone()),
333 };
334 move |_, cx| workspace::reload(&reload, cx)
335 })),
336 }),
337 AutoUpdateStatus::Errored => Some(Content {
338 icon: Some(
339 Icon::new(IconName::Warning)
340 .size(IconSize::Small)
341 .into_any_element(),
342 ),
343 message: "Auto update failed".to_string(),
344 on_click: Some(Arc::new(|this, cx| {
345 this.dismiss_error_message(&Default::default(), cx)
346 })),
347 }),
348 AutoUpdateStatus::Idle => None,
349 };
350 }
351
352 if let Some(extension_store) =
353 ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
354 {
355 if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
356 return Some(Content {
357 icon: Some(
358 Icon::new(IconName::Download)
359 .size(IconSize::Small)
360 .into_any_element(),
361 ),
362 message: format!("Updating {extension_id} extension…"),
363 on_click: None,
364 });
365 }
366 }
367
368 None
369 }
370
371 fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
372 self.context_menu_handle.toggle(cx);
373 }
374}
375
376impl EventEmitter<Event> for ActivityIndicator {}
377
378impl Render for ActivityIndicator {
379 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
380 let result = h_flex()
381 .id("activity-indicator")
382 .on_action(cx.listener(Self::show_error_message))
383 .on_action(cx.listener(Self::dismiss_error_message));
384 let Some(content) = self.content_to_render(cx) else {
385 return result;
386 };
387 let this = cx.view().downgrade();
388 result.gap_2().child(
389 PopoverMenu::new("activity-indicator-popover")
390 .trigger(
391 ButtonLike::new("activity-indicator-trigger").child(
392 h_flex()
393 .id("activity-indicator-status")
394 .gap_2()
395 .children(content.icon)
396 .child(Label::new(content.message).size(LabelSize::Small))
397 .when_some(content.on_click, |this, handler| {
398 this.on_click(cx.listener(move |this, _, cx| {
399 handler(this, cx);
400 }))
401 .cursor(CursorStyle::PointingHand)
402 }),
403 ),
404 )
405 .anchor(gpui::AnchorCorner::BottomLeft)
406 .menu(move |cx| {
407 let strong_this = this.upgrade()?;
408 let mut has_work = false;
409 let menu = ContextMenu::build(cx, |mut menu, cx| {
410 for work in strong_this.read(cx).pending_language_server_work(cx) {
411 has_work = true;
412 let this = this.clone();
413 let mut title = work
414 .progress
415 .title
416 .as_deref()
417 .unwrap_or(work.progress_token)
418 .to_owned();
419
420 if work.progress.is_cancellable {
421 let language_server_id = work.language_server_id;
422 let token = work.progress_token.to_string();
423 let title = SharedString::from(title);
424 menu = menu.custom_entry(
425 move |_| {
426 h_flex()
427 .w_full()
428 .justify_between()
429 .child(Label::new(title.clone()))
430 .child(Icon::new(IconName::XCircle))
431 .into_any_element()
432 },
433 move |cx| {
434 this.update(cx, |this, cx| {
435 this.project.update(cx, |project, cx| {
436 project.cancel_language_server_work(
437 language_server_id,
438 Some(token.clone()),
439 cx,
440 );
441 });
442 this.context_menu_handle.hide(cx);
443 cx.notify();
444 })
445 .ok();
446 },
447 );
448 } else {
449 if let Some(progress_message) = work.progress.message.as_ref() {
450 title.push_str(": ");
451 title.push_str(progress_message);
452 }
453
454 menu = menu.label(title);
455 }
456 }
457 menu
458 });
459 has_work.then_some(menu)
460 }),
461 )
462 }
463}
464
465impl StatusItemView for ActivityIndicator {
466 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
467}