@@ -0,0 +1,363 @@
+use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
+use editor::Editor;
+use futures::StreamExt;
+use gpui2::{
+ actions, anyhow,
+ elements::*,
+ platform::{CursorStyle, MouseButton},
+ AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
+};
+use language::{LanguageRegistry, LanguageServerBinaryStatus};
+use project::{LanguageServerProgress, Project};
+use smallvec::SmallVec;
+use std::{cmp::Reverse, fmt::Write, sync::Arc};
+use util::ResultExt;
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+actions!(lsp_status, [ShowErrorMessage]);
+
+const DOWNLOAD_ICON: &str = "icons/download.svg";
+const WARNING_ICON: &str = "icons/warning.svg";
+
+pub enum Event {
+ ShowError { lsp_name: Arc<str>, error: String },
+}
+
+pub struct ActivityIndicator {
+ statuses: Vec<LspStatus>,
+ project: ModelHandle<Project>,
+ auto_updater: Option<ModelHandle<AutoUpdater>>,
+}
+
+struct LspStatus {
+ name: Arc<str>,
+ status: LanguageServerBinaryStatus,
+}
+
+struct PendingWork<'a> {
+ language_server_name: &'a str,
+ progress_token: &'a str,
+ progress: &'a LanguageServerProgress,
+}
+
+#[derive(Default)]
+struct Content {
+ icon: Option<&'static str>,
+ message: String,
+ on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
+}
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(ActivityIndicator::show_error_message);
+ cx.add_action(ActivityIndicator::dismiss_error_message);
+}
+
+impl ActivityIndicator {
+ pub fn new(
+ workspace: &mut Workspace,
+ languages: Arc<LanguageRegistry>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> ViewHandle<ActivityIndicator> {
+ let project = workspace.project().clone();
+ let auto_updater = AutoUpdater::get(cx);
+ let this = cx.add_view(|cx: &mut ViewContext<Self>| {
+ let mut status_events = languages.language_server_binary_statuses();
+ cx.spawn(|this, mut cx| async move {
+ while let Some((language, event)) = status_events.next().await {
+ this.update(&mut cx, |this, cx| {
+ this.statuses.retain(|s| s.name != language.name());
+ this.statuses.push(LspStatus {
+ name: language.name(),
+ status: event,
+ });
+ cx.notify();
+ })?;
+ }
+ anyhow::Ok(())
+ })
+ .detach();
+ cx.observe(&project, |_, _, cx| cx.notify()).detach();
+ if let Some(auto_updater) = auto_updater.as_ref() {
+ cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
+ }
+ cx.observe_active_labeled_tasks(|_, cx| cx.notify())
+ .detach();
+
+ Self {
+ statuses: Default::default(),
+ project: project.clone(),
+ auto_updater,
+ }
+ });
+ cx.subscribe(&this, move |workspace, _, event, cx| match event {
+ Event::ShowError { lsp_name, error } => {
+ if let Some(buffer) = project
+ .update(cx, |project, cx| project.create_buffer(error, None, cx))
+ .log_err()
+ {
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ [(0..0, format!("Language server error: {}\n\n", lsp_name))],
+ None,
+ cx,
+ );
+ });
+ workspace.add_item(
+ Box::new(
+ cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
+ ),
+ cx,
+ );
+ }
+ }
+ })
+ .detach();
+ this
+ }
+
+ fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
+ self.statuses.retain(|status| {
+ if let LanguageServerBinaryStatus::Failed { error } = &status.status {
+ cx.emit(Event::ShowError {
+ lsp_name: status.name.clone(),
+ error: error.clone(),
+ });
+ false
+ } else {
+ true
+ }
+ });
+
+ cx.notify();
+ }
+
+ fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
+ if let Some(updater) = &self.auto_updater {
+ updater.update(cx, |updater, cx| {
+ updater.dismiss_error(cx);
+ });
+ }
+ cx.notify();
+ }
+
+ fn pending_language_server_work<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> impl Iterator<Item = PendingWork<'a>> {
+ self.project
+ .read(cx)
+ .language_server_statuses()
+ .rev()
+ .filter_map(|status| {
+ if status.pending_work.is_empty() {
+ None
+ } else {
+ let mut pending_work = status
+ .pending_work
+ .iter()
+ .map(|(token, progress)| PendingWork {
+ language_server_name: status.name.as_str(),
+ progress_token: token.as_str(),
+ progress,
+ })
+ .collect::<SmallVec<[_; 4]>>();
+ pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
+ Some(pending_work)
+ }
+ })
+ .flatten()
+ }
+
+ fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
+ // Show any language server has pending activity.
+ let mut pending_work = self.pending_language_server_work(cx);
+ if let Some(PendingWork {
+ language_server_name,
+ progress_token,
+ progress,
+ }) = pending_work.next()
+ {
+ let mut message = language_server_name.to_string();
+
+ message.push_str(": ");
+ if let Some(progress_message) = progress.message.as_ref() {
+ message.push_str(progress_message);
+ } else {
+ message.push_str(progress_token);
+ }
+
+ if let Some(percentage) = progress.percentage {
+ write!(&mut message, " ({}%)", percentage).unwrap();
+ }
+
+ let additional_work_count = pending_work.count();
+ if additional_work_count > 0 {
+ write!(&mut message, " + {} more", additional_work_count).unwrap();
+ }
+
+ return Content {
+ icon: None,
+ message,
+ on_click: None,
+ };
+ }
+
+ // Show any language server installation info.
+ let mut downloading = SmallVec::<[_; 3]>::new();
+ let mut checking_for_update = SmallVec::<[_; 3]>::new();
+ let mut failed = SmallVec::<[_; 3]>::new();
+ for status in &self.statuses {
+ let name = status.name.clone();
+ match status.status {
+ LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
+ LanguageServerBinaryStatus::Downloading => downloading.push(name),
+ LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
+ LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
+ }
+ }
+
+ if !downloading.is_empty() {
+ return Content {
+ icon: Some(DOWNLOAD_ICON),
+ message: format!(
+ "Downloading {} language server{}...",
+ downloading.join(", "),
+ if downloading.len() > 1 { "s" } else { "" }
+ ),
+ on_click: None,
+ };
+ } else if !checking_for_update.is_empty() {
+ return Content {
+ icon: Some(DOWNLOAD_ICON),
+ message: format!(
+ "Checking for updates to {} language server{}...",
+ checking_for_update.join(", "),
+ if checking_for_update.len() > 1 {
+ "s"
+ } else {
+ ""
+ }
+ ),
+ on_click: None,
+ };
+ } else if !failed.is_empty() {
+ return Content {
+ icon: Some(WARNING_ICON),
+ message: format!(
+ "Failed to download {} language server{}. Click to show error.",
+ failed.join(", "),
+ if failed.len() > 1 { "s" } else { "" }
+ ),
+ on_click: Some(Arc::new(|this, cx| {
+ this.show_error_message(&Default::default(), cx)
+ })),
+ };
+ }
+
+ // Show any application auto-update info.
+ if let Some(updater) = &self.auto_updater {
+ return match &updater.read(cx).status() {
+ AutoUpdateStatus::Checking => Content {
+ icon: Some(DOWNLOAD_ICON),
+ message: "Checking for Zed updatesโฆ".to_string(),
+ on_click: None,
+ },
+ AutoUpdateStatus::Downloading => Content {
+ icon: Some(DOWNLOAD_ICON),
+ message: "Downloading Zed updateโฆ".to_string(),
+ on_click: None,
+ },
+ AutoUpdateStatus::Installing => Content {
+ icon: Some(DOWNLOAD_ICON),
+ message: "Installing Zed updateโฆ".to_string(),
+ on_click: None,
+ },
+ AutoUpdateStatus::Updated => Content {
+ icon: None,
+ message: "Click to restart and update Zed".to_string(),
+ on_click: Some(Arc::new(|_, cx| {
+ workspace::restart(&Default::default(), cx)
+ })),
+ },
+ AutoUpdateStatus::Errored => Content {
+ icon: Some(WARNING_ICON),
+ message: "Auto update failed".to_string(),
+ on_click: Some(Arc::new(|this, cx| {
+ this.dismiss_error_message(&Default::default(), cx)
+ })),
+ },
+ AutoUpdateStatus::Idle => Default::default(),
+ };
+ }
+
+ if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
+ return Content {
+ icon: None,
+ message: most_recent_active_task.to_string(),
+ on_click: None,
+ };
+ }
+
+ Default::default()
+ }
+}
+
+impl Entity for ActivityIndicator {
+ type Event = Event;
+}
+
+impl View for ActivityIndicator {
+ fn ui_name() -> &'static str {
+ "ActivityIndicator"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ let Content {
+ icon,
+ message,
+ on_click,
+ } = self.content_to_render(cx);
+
+ let mut element = MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
+ let theme = &theme::current(cx).workspace.status_bar.lsp_status;
+ let style = if state.hovered() && on_click.is_some() {
+ theme.hovered.as_ref().unwrap_or(&theme.default)
+ } else {
+ &theme.default
+ };
+ Flex::row()
+ .with_children(icon.map(|path| {
+ Svg::new(path)
+ .with_color(style.icon_color)
+ .constrained()
+ .with_width(style.icon_width)
+ .contained()
+ .with_margin_right(style.icon_spacing)
+ .aligned()
+ .into_any_named("activity-icon")
+ }))
+ .with_child(
+ Text::new(message, style.message.clone())
+ .with_soft_wrap(false)
+ .aligned(),
+ )
+ .constrained()
+ .with_height(style.height)
+ .contained()
+ .with_style(style.container)
+ .aligned()
+ });
+
+ if let Some(on_click) = on_click.clone() {
+ element = element
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
+ }
+
+ element.into_any()
+ }
+}
+
+impl StatusItemView for ActivityIndicator {
+ fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
+}