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