diff --git a/Cargo.lock b/Cargo.lock index 021f3a1a72..d451f4ed14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,23 @@ dependencies = [ "workspace", ] +[[package]] +name = "activity_indicator2" +version = "0.1.0" +dependencies = [ + "auto_update", + "editor", + "futures 0.3.28", + "gpui2", + "language2", + "project", + "settings2", + "smallvec", + "theme2", + "util", + "workspace", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -10796,6 +10813,7 @@ dependencies = [ name = "zed2" version = "0.109.0" dependencies = [ + "activity_indicator2", "anyhow", "async-compression", "async-recursion 0.3.2", diff --git a/Cargo.toml b/Cargo.toml index 82af9265dd..41a7651c9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/activity_indicator", + "crates/activity_indicator2", "crates/ai", "crates/assistant", "crates/audio", diff --git a/crates/activity_indicator2/Cargo.toml b/crates/activity_indicator2/Cargo.toml new file mode 100644 index 0000000000..5d9979c310 --- /dev/null +++ b/crates/activity_indicator2/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "activity_indicator2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/activity_indicator2.rs" +doctest = false + +[dependencies] +auto_update = { path = "../auto_update" } +editor = { path = "../editor" } +language2 = { path = "../language2" } +gpui2= { path = "../gpui2" } +project = { path = "../project" } +settings2 = { path = "../settings2" } +util = { path = "../util" } +theme2 = { path = "../theme2" } +workspace = { path = "../workspace" } + +futures.workspace = true +smallvec.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/activity_indicator2/src/activity_indicator2.rs b/crates/activity_indicator2/src/activity_indicator2.rs new file mode 100644 index 0000000000..a9b358300b --- /dev/null +++ b/crates/activity_indicator2/src/activity_indicator2.rs @@ -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, error: String }, +} + +pub struct ActivityIndicator { + statuses: Vec, + project: ModelHandle, + auto_updater: Option>, +} + +struct LspStatus { + name: Arc, + 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)>>, +} + +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, + cx: &mut ViewContext, + ) -> ViewHandle { + let project = workspace.project().clone(); + let auto_updater = AutoUpdater::get(cx); + let this = cx.add_view(|cx: &mut ViewContext| { + 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.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) { + 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> { + 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::>(); + 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) -> 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) -> AnyElement { + let Content { + icon, + message, + on_click, + } = self.content_to_render(cx); + + let mut element = MouseEventHandler::new::(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) {} +} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 670a994d5f..685d8c2b85 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -82,7 +82,7 @@ const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4; #[ctor] unsafe fn build_classes() { - ::util::gpui1_loaded(); + ::util::gpui2_loaded(); WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow)); PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel)); VIEW_CLASS = { diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 1d90536a33..cb6cfab8ce 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] # audio = { path = "../audio" } -# activity_indicator = { path = "../activity_indicator" } +activity_indicator2 = { path = "../activity_indicator2" } # auto_update = { path = "../auto_update" } # breadcrumbs = { path = "../breadcrumbs" } call2 = { path = "../call2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 44a441db0b..9122293c2a 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -191,7 +191,7 @@ fn main() { // journal::init(app_state.clone(), cx); // language_selector::init(cx); // theme_selector::init(cx); - // activity_indicator::init(cx); + activity_indicator2::init(cx); // language_tools::init(cx); call2::init(app_state.client.clone(), app_state.user_store.clone(), cx); // collab_ui::init(&app_state, cx);