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 workspace::{item::ItemHandle, StatusItemView, Workspace};
 16
 17actions!(activity_indicator, [ShowErrorMessage]);
 18
 19const DOWNLOAD_ICON: &str = "icons/download.svg";
 20const WARNING_ICON: &str = "icons/warning.svg";
 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}
 31
 32struct LspStatus {
 33    name: LanguageServerName,
 34    status: LanguageServerBinaryStatus,
 35}
 36
 37struct PendingWork<'a> {
 38    language_server_name: &'a str,
 39    progress_token: &'a str,
 40    progress: &'a LanguageServerProgress,
 41}
 42
 43#[derive(Default)]
 44struct Content {
 45    icon: Option<&'static str>,
 46    message: String,
 47    on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
 48}
 49
 50impl ActivityIndicator {
 51    pub fn new(
 52        workspace: &mut Workspace,
 53        languages: Arc<LanguageRegistry>,
 54        cx: &mut ViewContext<Workspace>,
 55    ) -> View<ActivityIndicator> {
 56        let project = workspace.project().clone();
 57        let auto_updater = AutoUpdater::get(cx);
 58        let this = cx.new_view(|cx: &mut ViewContext<Self>| {
 59            let mut status_events = languages.language_server_binary_statuses();
 60            cx.spawn(|this, mut cx| async move {
 61                while let Some((name, status)) = status_events.next().await {
 62                    this.update(&mut cx, |this, cx| {
 63                        this.statuses.retain(|s| s.name != name);
 64                        this.statuses.push(LspStatus { name, status });
 65                        cx.notify();
 66                    })?;
 67                }
 68                anyhow::Ok(())
 69            })
 70            .detach();
 71            cx.observe(&project, |_, _, cx| cx.notify()).detach();
 72
 73            if let Some(auto_updater) = auto_updater.as_ref() {
 74                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
 75            }
 76
 77            Self {
 78                statuses: Default::default(),
 79                project: project.clone(),
 80                auto_updater,
 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                            cx,
109                        );
110                    })?;
111
112                    anyhow::Ok(())
113                })
114                .detach();
115            }
116        })
117        .detach();
118        this
119    }
120
121    fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
122        self.statuses.retain(|status| {
123            if let LanguageServerBinaryStatus::Failed { error } = &status.status {
124                cx.emit(Event::ShowError {
125                    lsp_name: status.name.0.clone(),
126                    error: error.clone(),
127                });
128                false
129            } else {
130                true
131            }
132        });
133
134        cx.notify();
135    }
136
137    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
138        if let Some(updater) = &self.auto_updater {
139            updater.update(cx, |updater, cx| {
140                updater.dismiss_error(cx);
141            });
142        }
143        cx.notify();
144    }
145
146    fn pending_language_server_work<'a>(
147        &self,
148        cx: &'a AppContext,
149    ) -> impl Iterator<Item = PendingWork<'a>> {
150        self.project
151            .read(cx)
152            .language_server_statuses()
153            .rev()
154            .filter_map(|status| {
155                if status.pending_work.is_empty() {
156                    None
157                } else {
158                    let mut pending_work = status
159                        .pending_work
160                        .iter()
161                        .map(|(token, progress)| PendingWork {
162                            language_server_name: status.name.as_str(),
163                            progress_token: token.as_str(),
164                            progress,
165                        })
166                        .collect::<SmallVec<[_; 4]>>();
167                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
168                    Some(pending_work)
169                }
170            })
171            .flatten()
172    }
173
174    fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
175        // Show any language server has pending activity.
176        let mut pending_work = self.pending_language_server_work(cx);
177        if let Some(PendingWork {
178            language_server_name,
179            progress_token,
180            progress,
181        }) = pending_work.next()
182        {
183            let mut message = language_server_name.to_string();
184
185            message.push_str(": ");
186            if let Some(progress_message) = progress.message.as_ref() {
187                message.push_str(progress_message);
188            } else {
189                message.push_str(progress_token);
190            }
191
192            if let Some(percentage) = progress.percentage {
193                write!(&mut message, " ({}%)", percentage).unwrap();
194            }
195
196            let additional_work_count = pending_work.count();
197            if additional_work_count > 0 {
198                write!(&mut message, " + {} more", additional_work_count).unwrap();
199            }
200
201            return Content {
202                icon: None,
203                message,
204                on_click: None,
205            };
206        }
207
208        // Show any language server installation info.
209        let mut downloading = SmallVec::<[_; 3]>::new();
210        let mut checking_for_update = SmallVec::<[_; 3]>::new();
211        let mut failed = SmallVec::<[_; 3]>::new();
212        for status in &self.statuses {
213            match status.status {
214                LanguageServerBinaryStatus::CheckingForUpdate => {
215                    checking_for_update.push(status.name.0.as_ref())
216                }
217                LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
218                LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
219                LanguageServerBinaryStatus::None => {}
220            }
221        }
222
223        if !downloading.is_empty() {
224            return Content {
225                icon: Some(DOWNLOAD_ICON),
226                message: format!("Downloading {}...", downloading.join(", "),),
227                on_click: None,
228            };
229        }
230
231        if !checking_for_update.is_empty() {
232            return Content {
233                icon: Some(DOWNLOAD_ICON),
234                message: format!(
235                    "Checking for updates to {}...",
236                    checking_for_update.join(", "),
237                ),
238                on_click: None,
239            };
240        }
241
242        if !failed.is_empty() {
243            return Content {
244                icon: Some(WARNING_ICON),
245                message: format!(
246                    "Failed to download {}. Click to show error.",
247                    failed.join(", "),
248                ),
249                on_click: Some(Arc::new(|this, cx| {
250                    this.show_error_message(&Default::default(), cx)
251                })),
252            };
253        }
254
255        // Show any formatting failure
256        if let Some(failure) = self.project.read(cx).last_formatting_failure() {
257            return Content {
258                icon: Some(WARNING_ICON),
259                message: format!("Formatting failed: {}. Click to see logs.", failure),
260                on_click: Some(Arc::new(|_, cx| {
261                    cx.dispatch_action(Box::new(workspace::OpenLog));
262                })),
263            };
264        }
265
266        // Show any application auto-update info.
267        if let Some(updater) = &self.auto_updater {
268            return match &updater.read(cx).status() {
269                AutoUpdateStatus::Checking => Content {
270                    icon: Some(DOWNLOAD_ICON),
271                    message: "Checking for Zed updates…".to_string(),
272                    on_click: None,
273                },
274                AutoUpdateStatus::Downloading => Content {
275                    icon: Some(DOWNLOAD_ICON),
276                    message: "Downloading Zed update…".to_string(),
277                    on_click: None,
278                },
279                AutoUpdateStatus::Installing => Content {
280                    icon: Some(DOWNLOAD_ICON),
281                    message: "Installing Zed update…".to_string(),
282                    on_click: None,
283                },
284                AutoUpdateStatus::Updated { binary_path } => Content {
285                    icon: None,
286                    message: "Click to restart and update Zed".to_string(),
287                    on_click: Some(Arc::new({
288                        let reload = workspace::Reload {
289                            binary_path: Some(binary_path.clone()),
290                        };
291                        move |_, cx| workspace::reload(&reload, cx)
292                    })),
293                },
294                AutoUpdateStatus::Errored => Content {
295                    icon: Some(WARNING_ICON),
296                    message: "Auto update failed".to_string(),
297                    on_click: Some(Arc::new(|this, cx| {
298                        this.dismiss_error_message(&Default::default(), cx)
299                    })),
300                },
301                AutoUpdateStatus::Idle => Default::default(),
302            };
303        }
304
305        if let Some(extension_store) =
306            ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
307        {
308            if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
309                return Content {
310                    icon: Some(DOWNLOAD_ICON),
311                    message: format!("Updating {extension_id} extension…"),
312                    on_click: None,
313                };
314            }
315        }
316
317        Default::default()
318    }
319}
320
321impl EventEmitter<Event> for ActivityIndicator {}
322
323impl Render for ActivityIndicator {
324    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
325        let content = self.content_to_render(cx);
326
327        let mut result = h_flex()
328            .id("activity-indicator")
329            .on_action(cx.listener(Self::show_error_message))
330            .on_action(cx.listener(Self::dismiss_error_message));
331
332        if let Some(on_click) = content.on_click {
333            result = result
334                .cursor(CursorStyle::PointingHand)
335                .on_click(cx.listener(move |this, _, cx| {
336                    on_click(this, cx);
337                }))
338        }
339
340        result
341            .children(content.icon.map(|icon| svg().path(icon)))
342            .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
343    }
344}
345
346impl StatusItemView for ActivityIndicator {
347    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
348}