activity_indicator.rs

  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}