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