activity_indicator.rs

  1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
  2use editor::Editor;
  3use futures::StreamExt;
  4use gpui::{
  5    actions,
  6    elements::*,
  7    platform::{CursorStyle, MouseButton},
  8    Action, AppContext, Entity, ModelHandle, RenderContext, 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            })
 82            .detach();
 83            cx.observe(&project, |_, _, cx| cx.notify()).detach();
 84            if let Some(auto_updater) = auto_updater.as_ref() {
 85                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
 86            }
 87            cx.observe_active_labeled_tasks(|_, cx| cx.notify())
 88                .detach();
 89
 90            Self {
 91                statuses: Default::default(),
 92                project: project.clone(),
 93                auto_updater,
 94            }
 95        });
 96        cx.subscribe(&this, move |workspace, _, event, cx| match event {
 97            Event::ShowError { lsp_name, error } => {
 98                if let Some(buffer) = project
 99                    .update(cx, |project, cx| project.create_buffer(error, None, cx))
100                    .log_err()
101                {
102                    buffer.update(cx, |buffer, cx| {
103                        buffer.edit(
104                            [(0..0, format!("Language server error: {}\n\n", lsp_name))],
105                            None,
106                            cx,
107                        );
108                    });
109                    workspace.add_item(
110                        Box::new(
111                            cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
112                        ),
113                        cx,
114                    );
115                }
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.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()
154            .rev()
155            .filter_map(|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_name: status.name.as_str(),
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 RenderContext<Self>) -> 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            language_server_name,
180            progress_token,
181            progress,
182        }) = pending_work.next()
183        {
184            let mut message = language_server_name.to_string();
185
186            message.push_str(": ");
187            if let Some(progress_message) = progress.message.as_ref() {
188                message.push_str(progress_message);
189            } else {
190                message.push_str(progress_token);
191            }
192
193            if let Some(percentage) = progress.percentage {
194                write!(&mut message, " ({}%)", percentage).unwrap();
195            }
196
197            let additional_work_count = pending_work.count();
198            if additional_work_count > 0 {
199                write!(&mut message, " + {} more", additional_work_count).unwrap();
200            }
201
202            return Content {
203                icon: None,
204                message,
205                action: None,
206            };
207        }
208
209        // Show any language server installation info.
210        let mut downloading = SmallVec::<[_; 3]>::new();
211        let mut checking_for_update = SmallVec::<[_; 3]>::new();
212        let mut failed = SmallVec::<[_; 3]>::new();
213        for status in &self.statuses {
214            match status.status {
215                LanguageServerBinaryStatus::CheckingForUpdate => {
216                    checking_for_update.push(status.name.clone());
217                }
218                LanguageServerBinaryStatus::Downloading => {
219                    downloading.push(status.name.clone());
220                }
221                LanguageServerBinaryStatus::Failed { .. } => {
222                    failed.push(status.name.clone());
223                }
224                LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
225            }
226        }
227
228        if !downloading.is_empty() {
229            return Content {
230                icon: Some(DOWNLOAD_ICON),
231                message: format!(
232                    "Downloading {} language server{}...",
233                    downloading.join(", "),
234                    if downloading.len() > 1 { "s" } else { "" }
235                ),
236                action: None,
237            };
238        } else if !checking_for_update.is_empty() {
239            return Content {
240                icon: Some(DOWNLOAD_ICON),
241                message: format!(
242                    "Checking for updates to {} language server{}...",
243                    checking_for_update.join(", "),
244                    if checking_for_update.len() > 1 {
245                        "s"
246                    } else {
247                        ""
248                    }
249                ),
250                action: None,
251            };
252        } else if !failed.is_empty() {
253            return Content {
254                icon: Some(WARNING_ICON),
255                message: format!(
256                    "Failed to download {} language server{}. Click to show error.",
257                    failed.join(", "),
258                    if failed.len() > 1 { "s" } else { "" }
259                ),
260                action: Some(Box::new(ShowErrorMessage)),
261            };
262        }
263
264        // Show any application auto-update info.
265        if let Some(updater) = &self.auto_updater {
266            return match &updater.read(cx).status() {
267                AutoUpdateStatus::Checking => Content {
268                    icon: Some(DOWNLOAD_ICON),
269                    message: "Checking for Zed updates…".to_string(),
270                    action: None,
271                },
272                AutoUpdateStatus::Downloading => Content {
273                    icon: Some(DOWNLOAD_ICON),
274                    message: "Downloading Zed update…".to_string(),
275                    action: None,
276                },
277                AutoUpdateStatus::Installing => Content {
278                    icon: Some(DOWNLOAD_ICON),
279                    message: "Installing Zed update…".to_string(),
280                    action: None,
281                },
282                AutoUpdateStatus::Updated => Content {
283                    icon: None,
284                    message: "Click to restart and update Zed".to_string(),
285                    action: Some(Box::new(workspace::Restart)),
286                },
287                AutoUpdateStatus::Errored => Content {
288                    icon: Some(WARNING_ICON),
289                    message: "Auto update failed".to_string(),
290                    action: Some(Box::new(DismissErrorMessage)),
291                },
292                AutoUpdateStatus::Idle => Default::default(),
293            };
294        }
295
296        if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
297            return Content {
298                icon: None,
299                message: most_recent_active_task.to_string(),
300                action: None,
301            };
302        }
303
304        Default::default()
305    }
306}
307
308impl Entity for ActivityIndicator {
309    type Event = Event;
310}
311
312impl View for ActivityIndicator {
313    fn ui_name() -> &'static str {
314        "ActivityIndicator"
315    }
316
317    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
318        let Content {
319            icon,
320            message,
321            action,
322        } = self.content_to_render(cx);
323
324        let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
325            let theme = &cx
326                .global::<Settings>()
327                .theme
328                .workspace
329                .status_bar
330                .lsp_status;
331            let style = if state.hovered() && action.is_some() {
332                theme.hover.as_ref().unwrap_or(&theme.default)
333            } else {
334                &theme.default
335            };
336            Flex::row()
337                .with_children(icon.map(|path| {
338                    Svg::new(path)
339                        .with_color(style.icon_color)
340                        .constrained()
341                        .with_width(style.icon_width)
342                        .contained()
343                        .with_margin_right(style.icon_spacing)
344                        .aligned()
345                        .named("activity-icon")
346                }))
347                .with_child(
348                    Text::new(message, style.message.clone())
349                        .with_soft_wrap(false)
350                        .aligned()
351                        .boxed(),
352                )
353                .constrained()
354                .with_height(style.height)
355                .contained()
356                .with_style(style.container)
357                .aligned()
358                .boxed()
359        });
360
361        if let Some(action) = action {
362            element = element
363                .with_cursor_style(CursorStyle::PointingHand)
364                .on_click(MouseButton::Left, move |_, cx| {
365                    cx.dispatch_any_action(action.boxed_clone())
366                });
367        }
368
369        element.boxed()
370    }
371}
372
373impl StatusItemView for ActivityIndicator {
374    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
375}