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