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