activity_indicator.rs

  1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
  2use editor::Editor;
  3use extension::ExtensionStore;
  4use futures::StreamExt;
  5use gpui::{
  6    actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
  7    ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
  8    ViewContext, VisualContext as _,
  9};
 10use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 11use project::{LanguageServerProgress, Project};
 12use smallvec::SmallVec;
 13use std::{cmp::Reverse, fmt::Write, sync::Arc};
 14use ui::prelude::*;
 15use util::ResultExt;
 16use workspace::{item::ItemHandle, StatusItemView, Workspace};
 17
 18actions!(activity_indicator, [ShowErrorMessage]);
 19
 20const DOWNLOAD_ICON: &str = "icons/download.svg";
 21const WARNING_ICON: &str = "icons/warning.svg";
 22
 23pub enum Event {
 24    ShowError { lsp_name: Arc<str>, error: String },
 25}
 26
 27pub struct ActivityIndicator {
 28    statuses: Vec<LspStatus>,
 29    project: Model<Project>,
 30    auto_updater: Option<Model<AutoUpdater>>,
 31}
 32
 33struct LspStatus {
 34    name: LanguageServerName,
 35    status: LanguageServerBinaryStatus,
 36}
 37
 38struct PendingWork<'a> {
 39    language_server_name: &'a str,
 40    progress_token: &'a str,
 41    progress: &'a LanguageServerProgress,
 42}
 43
 44#[derive(Default)]
 45struct Content {
 46    icon: Option<&'static str>,
 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            }
 83        });
 84
 85        cx.subscribe(&this, move |workspace, _, event, cx| match event {
 86            Event::ShowError { lsp_name, error } => {
 87                if let Some(buffer) = project
 88                    .update(cx, |project, cx| project.create_buffer(error, None, cx))
 89                    .log_err()
 90                {
 91                    buffer.update(cx, |buffer, cx| {
 92                        buffer.edit(
 93                            [(0..0, format!("Language server error: {}\n\n", lsp_name))],
 94                            None,
 95                            cx,
 96                        );
 97                    });
 98                    workspace.add_item_to_active_pane(
 99                        Box::new(
100                            cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
101                        ),
102                        cx,
103                    );
104                }
105            }
106        })
107        .detach();
108        this
109    }
110
111    fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
112        self.statuses.retain(|status| {
113            if let LanguageServerBinaryStatus::Failed { error } = &status.status {
114                cx.emit(Event::ShowError {
115                    lsp_name: status.name.0.clone(),
116                    error: error.clone(),
117                });
118                false
119            } else {
120                true
121            }
122        });
123
124        cx.notify();
125    }
126
127    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
128        if let Some(updater) = &self.auto_updater {
129            updater.update(cx, |updater, cx| {
130                updater.dismiss_error(cx);
131            });
132        }
133        cx.notify();
134    }
135
136    fn pending_language_server_work<'a>(
137        &self,
138        cx: &'a AppContext,
139    ) -> impl Iterator<Item = PendingWork<'a>> {
140        self.project
141            .read(cx)
142            .language_server_statuses()
143            .rev()
144            .filter_map(|status| {
145                if status.pending_work.is_empty() {
146                    None
147                } else {
148                    let mut pending_work = status
149                        .pending_work
150                        .iter()
151                        .map(|(token, progress)| PendingWork {
152                            language_server_name: status.name.as_str(),
153                            progress_token: token.as_str(),
154                            progress,
155                        })
156                        .collect::<SmallVec<[_; 4]>>();
157                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
158                    Some(pending_work)
159                }
160            })
161            .flatten()
162    }
163
164    fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
165        // Show any language server has pending activity.
166        let mut pending_work = self.pending_language_server_work(cx);
167        if let Some(PendingWork {
168            language_server_name,
169            progress_token,
170            progress,
171        }) = pending_work.next()
172        {
173            let mut message = language_server_name.to_string();
174
175            message.push_str(": ");
176            if let Some(progress_message) = progress.message.as_ref() {
177                message.push_str(progress_message);
178            } else {
179                message.push_str(progress_token);
180            }
181
182            if let Some(percentage) = progress.percentage {
183                write!(&mut message, " ({}%)", percentage).unwrap();
184            }
185
186            let additional_work_count = pending_work.count();
187            if additional_work_count > 0 {
188                write!(&mut message, " + {} more", additional_work_count).unwrap();
189            }
190
191            return Content {
192                icon: None,
193                message,
194                on_click: None,
195            };
196        }
197
198        // Show any language server installation info.
199        let mut downloading = SmallVec::<[_; 3]>::new();
200        let mut checking_for_update = SmallVec::<[_; 3]>::new();
201        let mut failed = SmallVec::<[_; 3]>::new();
202        for status in &self.statuses {
203            match status.status {
204                LanguageServerBinaryStatus::CheckingForUpdate => {
205                    checking_for_update.push(status.name.0.as_ref())
206                }
207                LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
208                LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
209                LanguageServerBinaryStatus::None => {}
210            }
211        }
212
213        if !downloading.is_empty() {
214            return Content {
215                icon: Some(DOWNLOAD_ICON),
216                message: format!("Downloading {}...", downloading.join(", "),),
217                on_click: None,
218            };
219        }
220
221        if !checking_for_update.is_empty() {
222            return Content {
223                icon: Some(DOWNLOAD_ICON),
224                message: format!(
225                    "Checking for updates to {}...",
226                    checking_for_update.join(", "),
227                ),
228                on_click: None,
229            };
230        }
231
232        if !failed.is_empty() {
233            return Content {
234                icon: Some(WARNING_ICON),
235                message: format!(
236                    "Failed to download {}. Click to show error.",
237                    failed.join(", "),
238                ),
239                on_click: Some(Arc::new(|this, cx| {
240                    this.show_error_message(&Default::default(), cx)
241                })),
242            };
243        }
244
245        // Show any formatting failure
246        if let Some(failure) = self.project.read(cx).last_formatting_failure() {
247            return Content {
248                icon: Some(WARNING_ICON),
249                message: format!("Formatting failed: {}. Click to see logs.", failure),
250                on_click: Some(Arc::new(|_, cx| {
251                    cx.dispatch_action(Box::new(workspace::OpenLog));
252                })),
253            };
254        }
255
256        // Show any application auto-update info.
257        if let Some(updater) = &self.auto_updater {
258            return match &updater.read(cx).status() {
259                AutoUpdateStatus::Checking => Content {
260                    icon: Some(DOWNLOAD_ICON),
261                    message: "Checking for Zed updates…".to_string(),
262                    on_click: None,
263                },
264                AutoUpdateStatus::Downloading => Content {
265                    icon: Some(DOWNLOAD_ICON),
266                    message: "Downloading Zed update…".to_string(),
267                    on_click: None,
268                },
269                AutoUpdateStatus::Installing => Content {
270                    icon: Some(DOWNLOAD_ICON),
271                    message: "Installing Zed update…".to_string(),
272                    on_click: None,
273                },
274                AutoUpdateStatus::Updated => Content {
275                    icon: None,
276                    message: "Click to restart and update Zed".to_string(),
277                    on_click: Some(Arc::new(|_, cx| {
278                        workspace::restart(&Default::default(), cx)
279                    })),
280                },
281                AutoUpdateStatus::Errored => Content {
282                    icon: Some(WARNING_ICON),
283                    message: "Auto update failed".to_string(),
284                    on_click: Some(Arc::new(|this, cx| {
285                        this.dismiss_error_message(&Default::default(), cx)
286                    })),
287                },
288                AutoUpdateStatus::Idle => Default::default(),
289            };
290        }
291
292        if let Some(extension_store) =
293            ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
294        {
295            if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
296                return Content {
297                    icon: Some(DOWNLOAD_ICON),
298                    message: format!("Updating {extension_id} extension…"),
299                    on_click: None,
300                };
301            }
302        }
303
304        Default::default()
305    }
306}
307
308impl EventEmitter<Event> for ActivityIndicator {}
309
310impl Render for ActivityIndicator {
311    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
312        let content = self.content_to_render(cx);
313
314        let mut result = h_flex()
315            .id("activity-indicator")
316            .on_action(cx.listener(Self::show_error_message))
317            .on_action(cx.listener(Self::dismiss_error_message));
318
319        if let Some(on_click) = content.on_click {
320            result = result
321                .cursor(CursorStyle::PointingHand)
322                .on_click(cx.listener(move |this, _, cx| {
323                    on_click(this, cx);
324                }))
325        }
326
327        result
328            .children(content.icon.map(|icon| svg().path(icon)))
329            .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
330    }
331}
332
333impl StatusItemView for ActivityIndicator {
334    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
335}