activity_indicator.rs

  1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
  2use editor::Editor;
  3use futures::StreamExt;
  4use gpui::{
  5    actions, anyhow,
  6    elements::*,
  7    platform::{CursorStyle, MouseButton},
  8    Action, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
  9};
 10use language::{LanguageRegistry, LanguageServerBinaryStatus};
 11use project::{LanguageServerProgress, Project};
 12use settings::Settings;
 13use smallvec::SmallVec;
 14use std::{cmp::Reverse, fmt::Write, sync::Arc};
 15use util::ResultExt;
 16use workspace::{item::ItemHandle, StatusItemView, Workspace};
 17
 18actions!(lsp_status, [ShowErrorMessage]);
 19
 20const DOWNLOAD_ICON: &str = "icons/download_12.svg";
 21const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
 22
 23pub enum Event {
 24    ShowError { lsp_name: Arc<str>, error: String },
 25}
 26
 27pub struct ActivityIndicator {
 28    statuses: Vec<LspStatus>,
 29    project: ModelHandle<Project>,
 30    auto_updater: Option<ModelHandle<AutoUpdater>>,
 31}
 32
 33struct LspStatus {
 34    name: Arc<str>,
 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    action: Option<Box<dyn Action>>,
 49}
 50
 51pub fn init(cx: &mut AppContext) {
 52    cx.add_action(ActivityIndicator::show_error_message);
 53    cx.add_action(ActivityIndicator::dismiss_error_message);
 54}
 55
 56impl ActivityIndicator {
 57    pub fn new(
 58        workspace: &mut Workspace,
 59        languages: Arc<LanguageRegistry>,
 60        cx: &mut ViewContext<Workspace>,
 61    ) -> ViewHandle<ActivityIndicator> {
 62        let project = workspace.project().clone();
 63        let auto_updater = AutoUpdater::get(cx);
 64        let this = cx.add_view(|cx: &mut ViewContext<Self>| {
 65            let mut status_events = languages.language_server_binary_statuses();
 66            cx.spawn_weak(|this, mut cx| async move {
 67                while let Some((language, event)) = status_events.next().await {
 68                    if let Some(this) = this.upgrade(&cx) {
 69                        this.update(&mut cx, |this, cx| {
 70                            this.statuses.retain(|s| s.name != language.name());
 71                            this.statuses.push(LspStatus {
 72                                name: language.name(),
 73                                status: event,
 74                            });
 75                            cx.notify();
 76                        })?;
 77                    } else {
 78                        break;
 79                    }
 80                }
 81                anyhow::Ok(())
 82            })
 83            .detach();
 84            cx.observe(&project, |_, _, cx| cx.notify()).detach();
 85            if let Some(auto_updater) = auto_updater.as_ref() {
 86                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
 87            }
 88            cx.observe_active_labeled_tasks(|_, cx| cx.notify())
 89                .detach();
 90
 91            Self {
 92                statuses: Default::default(),
 93                project: project.clone(),
 94                auto_updater,
 95            }
 96        });
 97        cx.subscribe(&this, move |workspace, _, event, cx| match event {
 98            Event::ShowError { lsp_name, error } => {
 99                if let Some(buffer) = project
100                    .update(cx, |project, cx| project.create_buffer(error, None, cx))
101                    .log_err()
102                {
103                    buffer.update(cx, |buffer, cx| {
104                        buffer.edit(
105                            [(0..0, format!("Language server error: {}\n\n", lsp_name))],
106                            None,
107                            cx,
108                        );
109                    });
110                    workspace.add_item(
111                        Box::new(
112                            cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
113                        ),
114                        cx,
115                    );
116                }
117            }
118        })
119        .detach();
120        this
121    }
122
123    fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
124        self.statuses.retain(|status| {
125            if let LanguageServerBinaryStatus::Failed { error } = &status.status {
126                cx.emit(Event::ShowError {
127                    lsp_name: status.name.clone(),
128                    error: error.clone(),
129                });
130                false
131            } else {
132                true
133            }
134        });
135
136        cx.notify();
137    }
138
139    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
140        if let Some(updater) = &self.auto_updater {
141            updater.update(cx, |updater, cx| {
142                updater.dismiss_error(cx);
143            });
144        }
145        cx.notify();
146    }
147
148    fn pending_language_server_work<'a>(
149        &self,
150        cx: &'a AppContext,
151    ) -> impl Iterator<Item = PendingWork<'a>> {
152        self.project
153            .read(cx)
154            .language_server_statuses()
155            .rev()
156            .filter_map(|status| {
157                if status.pending_work.is_empty() {
158                    None
159                } else {
160                    let mut pending_work = status
161                        .pending_work
162                        .iter()
163                        .map(|(token, progress)| PendingWork {
164                            language_server_name: status.name.as_str(),
165                            progress_token: token.as_str(),
166                            progress,
167                        })
168                        .collect::<SmallVec<[_; 4]>>();
169                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
170                    Some(pending_work)
171                }
172            })
173            .flatten()
174    }
175
176    fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
177        // Show any language server has pending activity.
178        let mut pending_work = self.pending_language_server_work(cx);
179        if let Some(PendingWork {
180            language_server_name,
181            progress_token,
182            progress,
183        }) = pending_work.next()
184        {
185            let mut message = language_server_name.to_string();
186
187            message.push_str(": ");
188            if let Some(progress_message) = progress.message.as_ref() {
189                message.push_str(progress_message);
190            } else {
191                message.push_str(progress_token);
192            }
193
194            if let Some(percentage) = progress.percentage {
195                write!(&mut message, " ({}%)", percentage).unwrap();
196            }
197
198            let additional_work_count = pending_work.count();
199            if additional_work_count > 0 {
200                write!(&mut message, " + {} more", additional_work_count).unwrap();
201            }
202
203            return Content {
204                icon: None,
205                message,
206                action: None,
207            };
208        }
209
210        // Show any language server installation info.
211        let mut downloading = SmallVec::<[_; 3]>::new();
212        let mut checking_for_update = SmallVec::<[_; 3]>::new();
213        let mut failed = SmallVec::<[_; 3]>::new();
214        for status in &self.statuses {
215            match status.status {
216                LanguageServerBinaryStatus::CheckingForUpdate => {
217                    checking_for_update.push(status.name.clone());
218                }
219                LanguageServerBinaryStatus::Downloading => {
220                    downloading.push(status.name.clone());
221                }
222                LanguageServerBinaryStatus::Failed { .. } => {
223                    failed.push(status.name.clone());
224                }
225                LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
226            }
227        }
228
229        if !downloading.is_empty() {
230            return Content {
231                icon: Some(DOWNLOAD_ICON),
232                message: format!(
233                    "Downloading {} language server{}...",
234                    downloading.join(", "),
235                    if downloading.len() > 1 { "s" } else { "" }
236                ),
237                action: None,
238            };
239        } else if !checking_for_update.is_empty() {
240            return Content {
241                icon: Some(DOWNLOAD_ICON),
242                message: format!(
243                    "Checking for updates to {} language server{}...",
244                    checking_for_update.join(", "),
245                    if checking_for_update.len() > 1 {
246                        "s"
247                    } else {
248                        ""
249                    }
250                ),
251                action: None,
252            };
253        } else if !failed.is_empty() {
254            return Content {
255                icon: Some(WARNING_ICON),
256                message: format!(
257                    "Failed to download {} language server{}. Click to show error.",
258                    failed.join(", "),
259                    if failed.len() > 1 { "s" } else { "" }
260                ),
261                action: Some(Box::new(ShowErrorMessage)),
262            };
263        }
264
265        // Show any application auto-update info.
266        if let Some(updater) = &self.auto_updater {
267            return match &updater.read(cx).status() {
268                AutoUpdateStatus::Checking => Content {
269                    icon: Some(DOWNLOAD_ICON),
270                    message: "Checking for Zed updates…".to_string(),
271                    action: None,
272                },
273                AutoUpdateStatus::Downloading => Content {
274                    icon: Some(DOWNLOAD_ICON),
275                    message: "Downloading Zed update…".to_string(),
276                    action: None,
277                },
278                AutoUpdateStatus::Installing => Content {
279                    icon: Some(DOWNLOAD_ICON),
280                    message: "Installing Zed update…".to_string(),
281                    action: None,
282                },
283                AutoUpdateStatus::Updated => Content {
284                    icon: None,
285                    message: "Click to restart and update Zed".to_string(),
286                    action: Some(Box::new(workspace::Restart)),
287                },
288                AutoUpdateStatus::Errored => Content {
289                    icon: Some(WARNING_ICON),
290                    message: "Auto update failed".to_string(),
291                    action: Some(Box::new(DismissErrorMessage)),
292                },
293                AutoUpdateStatus::Idle => Default::default(),
294            };
295        }
296
297        if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
298            return Content {
299                icon: None,
300                message: most_recent_active_task.to_string(),
301                action: None,
302            };
303        }
304
305        Default::default()
306    }
307}
308
309impl Entity for ActivityIndicator {
310    type Event = Event;
311}
312
313impl View for ActivityIndicator {
314    fn ui_name() -> &'static str {
315        "ActivityIndicator"
316    }
317
318    fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
319        let Content {
320            icon,
321            message,
322            action,
323        } = self.content_to_render(cx);
324
325        let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
326            let theme = &cx
327                .global::<Settings>()
328                .theme
329                .workspace
330                .status_bar
331                .lsp_status;
332            let style = if state.hovered() && action.is_some() {
333                theme.hover.as_ref().unwrap_or(&theme.default)
334            } else {
335                &theme.default
336            };
337            Flex::row()
338                .with_children(icon.map(|path| {
339                    Svg::new(path)
340                        .with_color(style.icon_color)
341                        .constrained()
342                        .with_width(style.icon_width)
343                        .contained()
344                        .with_margin_right(style.icon_spacing)
345                        .aligned()
346                        .named("activity-icon")
347                }))
348                .with_child(
349                    Text::new(message, style.message.clone())
350                        .with_soft_wrap(false)
351                        .aligned()
352                        .boxed(),
353                )
354                .constrained()
355                .with_height(style.height)
356                .contained()
357                .with_style(style.container)
358                .aligned()
359                .boxed()
360        });
361
362        if let Some(action) = action {
363            element = element
364                .with_cursor_style(CursorStyle::PointingHand)
365                .on_click(MouseButton::Left, move |_, _, cx| {
366                    cx.dispatch_any_action(action.boxed_clone())
367                });
368        }
369
370        element.boxed()
371    }
372}
373
374impl StatusItemView for ActivityIndicator {
375    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
376}