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