activity_indicator.rs

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