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