From e49325080c6b05489bcb63c66f8f5087e3f75dd2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Nov 2023 17:18:21 -0800 Subject: [PATCH] Implement activity indicator in zed2 --- Cargo.lock | 20 ++ Cargo.toml | 1 + crates/activity_indicator2/Cargo.toml | 28 ++ .../src/activity_indicator.rs | 333 ++++++++++++++++++ crates/gpui2/src/app.rs | 4 + crates/gpui2/src/window.rs | 2 +- crates/workspace2/src/status_bar.rs | 14 +- crates/workspace2/src/workspace2.rs | 84 ++--- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/zed2.rs | 9 +- 10 files changed, 436 insertions(+), 61 deletions(-) create mode 100644 crates/activity_indicator2/Cargo.toml create mode 100644 crates/activity_indicator2/src/activity_indicator.rs diff --git a/Cargo.lock b/Cargo.lock index f02c748fbd..5ec386837b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,25 @@ dependencies = [ "workspace", ] +[[package]] +name = "activity_indicator2" +version = "0.1.0" +dependencies = [ + "anyhow", + "auto_update2", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "project2", + "settings2", + "smallvec", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -11697,6 +11716,7 @@ dependencies = [ name = "zed2" version = "0.109.0" dependencies = [ + "activity_indicator2", "ai2", "anyhow", "async-compression", diff --git a/Cargo.toml b/Cargo.toml index 03a854b77f..674bfbd606 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..400869d2fd --- /dev/null +++ b/crates/activity_indicator2/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "activity_indicator2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/activity_indicator.rs" +doctest = false + +[dependencies] +auto_update = { path = "../auto_update2", package = "auto_update2" } +editor = { path = "../editor2", package = "editor2" } +language = { path = "../language2", package = "language2" } +gpui = { path = "../gpui2", package = "gpui2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +ui = { path = "../ui2", package = "ui2" } +util = { path = "../util" } +theme = { path = "../theme2", package = "theme2" } +workspace = { path = "../workspace2", package = "workspace2" } + +anyhow.workspace = true +futures.workspace = true +smallvec.workspace = true + +[dev-dependencies] +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } diff --git a/crates/activity_indicator2/src/activity_indicator.rs b/crates/activity_indicator2/src/activity_indicator.rs new file mode 100644 index 0000000000..1ee5a6689a --- /dev/null +++ b/crates/activity_indicator2/src/activity_indicator.rs @@ -0,0 +1,333 @@ +use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; +use editor::Editor; +use futures::StreamExt; +use gpui::{ + actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model, + ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext as _, +}; +use language::{LanguageRegistry, LanguageServerBinaryStatus}; +use project::{LanguageServerProgress, Project}; +use smallvec::SmallVec; +use std::{cmp::Reverse, fmt::Write, sync::Arc}; +use ui::h_stack; +use util::ResultExt; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +actions!(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: Model, + 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)>>, +} + +impl ActivityIndicator { + pub fn new( + workspace: &mut Workspace, + languages: Arc, + cx: &mut ViewContext, + ) -> View { + let project = workspace.project().clone(); + let auto_updater = AutoUpdater::get(cx); + let this = cx.build_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.build_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(), + }; + } + + // todo!(show active tasks) + // 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 EventEmitter for ActivityIndicator {} + +impl Render for ActivityIndicator { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let content = self.content_to_render(cx); + + let mut result = h_stack() + .id("activity-indicator") + .on_action(cx.listener(Self::show_error_message)) + .on_action(cx.listener(Self::dismiss_error_message)); + + if let Some(on_click) = content.on_click { + result = result + .cursor(CursorStyle::PointingHand) + .on_click(cx.listener(move |this, _, cx| { + on_click(this, cx); + })) + } + + result + .children(content.icon.map(|icon| svg().path(icon))) + .child(SharedString::from(content.message)) + } +} + +impl StatusItemView for ActivityIndicator { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} +} diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index e8f2a60a6a..94a7d3be0b 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -520,6 +520,10 @@ impl AppContext { self.platform.should_auto_hide_scrollbars() } + pub fn restart(&self) { + self.platform.restart() + } + pub(crate) fn push_effect(&mut self, effect: Effect) { match &effect { Effect::Notify { emitter } => { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 507c46d067..1f2df0788d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2582,7 +2582,7 @@ impl WindowHandle { cx.read_window(self, |root_view, _cx| root_view.clone()) } - pub fn is_active(&self, cx: &WindowContext) -> Option { + pub fn is_active(&self, cx: &AppContext) -> Option { cx.windows .get(self.id) .and_then(|window| window.as_ref().map(|window| window.active)) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index ad2997e421..397859648b 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -47,19 +47,7 @@ impl Render for StatusBar { .w_full() .h_8() .bg(cx.theme().colors().status_bar_background) - // Nate: I know this isn't how we render status bar tools - // We can move these to the correct place once we port their tools - .child( - h_stack().gap_1().child(self.render_left_tools(cx)).child( - h_stack().gap_4().child( - // TODO: Language Server status - div() - .border() - .border_color(gpui::red()) - .child("Checking..."), - ), - ), - ) + .child(h_stack().gap_1().child(self.render_left_tools(cx))) .child( h_stack() .gap_4() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 0252d868b8..23907f8dae 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -4481,50 +4481,54 @@ pub fn create_and_open_local_file( // }) // } -// pub fn restart(_: &Restart, cx: &mut AppContext) { -// let should_confirm = settings::get::(cx).confirm_quit; -// cx.spawn(|mut cx| async move { -// let mut workspace_windows = cx -// .windows() -// .into_iter() -// .filter_map(|window| window.downcast::()) -// .collect::>(); +pub fn restart(_: &Restart, cx: &mut AppContext) { + let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; + let mut workspace_windows = cx + .windows() + .into_iter() + .filter_map(|window| window.downcast::()) + .collect::>(); -// // If multiple windows have unsaved changes, and need a save prompt, -// // prompt in the active window before switching to a different window. -// workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + // If multiple windows have unsaved changes, and need a save prompt, + // prompt in the active window before switching to a different window. + workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); -// if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { -// let answer = window.prompt( -// PromptLevel::Info, -// "Are you sure you want to restart?", -// &["Restart", "Cancel"], -// &mut cx, -// ); + let mut prompt = None; + if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { + prompt = window + .update(cx, |_, cx| { + cx.prompt( + PromptLevel::Info, + "Are you sure you want to restart?", + &["Restart", "Cancel"], + ) + }) + .ok(); + } -// if let Some(mut answer) = answer { -// let answer = answer.next().await; -// if answer != Some(0) { -// return Ok(()); -// } -// } -// } + cx.spawn(|mut cx| async move { + if let Some(mut prompt) = prompt { + let answer = prompt.await?; + if answer != 0 { + return Ok(()); + } + } -// // If the user cancels any save prompt, then keep the app open. -// for window in workspace_windows { -// if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { -// workspace.prepare_to_close(true, cx) -// }) { -// if !should_close.await? { -// return Ok(()); -// } -// } -// } -// cx.platform().restart(); -// anyhow::Ok(()) -// }) -// .detach_and_log_err(cx); -// } + // If the user cancels any save prompt, then keep the app open. + for window in workspace_windows { + if let Ok(should_close) = window.update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) { + if !should_close.await? { + return Ok(()); + } + } + } + + cx.update(|cx| cx.restart()) + }) + .detach_and_log_err(cx); +} fn parse_pixel_position_env_var(value: &str) -> Option> { let mut parts = value.split(','); diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 3f4fb5c7d8..bd2a8e5a2f 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] ai = { package = "ai2", path = "../ai2"} audio = { package = "audio2", path = "../audio2" } -# activity_indicator = { path = "../activity_indicator" } +activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"} auto_update = { package = "auto_update2", path = "../auto_update2" } # breadcrumbs = { path = "../breadcrumbs" } call = { package = "call2", path = "../call2" } diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 50998a7fb8..07b75bf8d4 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -139,11 +139,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); let diagnostic_summary = cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); - // let activity_indicator = activity_indicator::ActivityIndicator::new( - // workspace, - // app_state.languages.clone(), - // cx, - // ); + let activity_indicator = + activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); // let active_buffer_language = // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); // let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx)); @@ -153,7 +150,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); - // status_bar.add_left_item(activity_indicator, cx); + status_bar.add_left_item(activity_indicator, cx); // status_bar.add_right_item(feedback_button, cx); // status_bar.add_right_item(copilot, cx);