activity_indicator.rs

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