From e64a56ffad0d543f53111d867263439e26cb3509 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 9 Jan 2025 16:24:35 +0100 Subject: [PATCH] Animate Zeta button while generating completions (#22899) Release Notes: - N/A Co-authored-by: Thorsten --- Cargo.lock | 1 + .../src/copilot_completion_provider.rs | 23 +++++---- crates/editor/src/inline_completion_tests.rs | 4 ++ .../src/inline_completion.rs | 6 +++ crates/inline_completion_button/Cargo.toml | 1 + .../src/inline_completion_button.rs | 47 ++++++++++++++----- .../src/supermaven_completion_provider.rs | 17 ++++--- .../ui/src/components/button/icon_button.rs | 10 +++- crates/ui/src/components/popover_menu.rs | 22 +++++++++ crates/zeta/src/zeta.rs | 4 ++ 10 files changed, 106 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f90c71a672..02d9036071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6298,6 +6298,7 @@ dependencies = [ "futures 0.3.31", "gpui", "indoc", + "inline_completion", "language", "lsp", "paths", diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 70e18c8edd..79443f8d3a 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -17,8 +17,8 @@ pub struct CopilotCompletionProvider { completions: Vec, active_completion_index: usize, file_extension: Option, - pending_refresh: Task>, - pending_cycling_refresh: Task>, + pending_refresh: Option>>, + pending_cycling_refresh: Option>>, copilot: Model, } @@ -30,8 +30,8 @@ impl CopilotCompletionProvider { completions: Vec::new(), active_completion_index: 0, file_extension: None, - pending_refresh: Task::ready(Ok(())), - pending_cycling_refresh: Task::ready(Ok(())), + pending_refresh: None, + pending_cycling_refresh: None, copilot, } } @@ -67,6 +67,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider { false } + fn is_refreshing(&self) -> bool { + self.pending_refresh.is_some() + } + fn is_enabled( &self, buffer: &Model, @@ -92,7 +96,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { cx: &mut ModelContext, ) { let copilot = self.copilot.clone(); - self.pending_refresh = cx.spawn(|this, mut cx| async move { + self.pending_refresh = Some(cx.spawn(|this, mut cx| async move { if debounce { cx.background_executor() .timer(COPILOT_DEBOUNCE_TIMEOUT) @@ -108,7 +112,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider { this.update(&mut cx, |this, cx| { if !completions.is_empty() { this.cycled = false; - this.pending_cycling_refresh = Task::ready(Ok(())); + this.pending_refresh = None; + this.pending_cycling_refresh = None; this.completions.clear(); this.active_completion_index = 0; this.buffer_id = Some(buffer.entity_id()); @@ -129,7 +134,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { })?; Ok(()) - }); + })); } fn cycle( @@ -161,7 +166,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { cx.notify(); } else { let copilot = self.copilot.clone(); - self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move { + self.pending_cycling_refresh = Some(cx.spawn(|this, mut cx| async move { let completions = copilot .update(&mut cx, |copilot, cx| { copilot.completions_cycling(&buffer, cursor_position, cx) @@ -185,7 +190,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { })?; Ok(()) - }); + })); } } diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/inline_completion_tests.rs index a80f8dc62a..d7c44dc95e 100644 --- a/crates/editor/src/inline_completion_tests.rs +++ b/crates/editor/src/inline_completion_tests.rs @@ -387,6 +387,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider { true } + fn is_refreshing(&self) -> bool { + false + } + fn refresh( &mut self, _buffer: gpui::Model, diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index 5f87bd576e..17b77ca4bf 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -28,6 +28,7 @@ pub trait InlineCompletionProvider: 'static + Sized { cursor_position: language::Anchor, cx: &AppContext, ) -> bool; + fn is_refreshing(&self) -> bool; fn refresh( &mut self, buffer: Model, @@ -63,6 +64,7 @@ pub trait InlineCompletionProviderHandle { ) -> bool; fn show_completions_in_menu(&self) -> bool; fn show_completions_in_normal_mode(&self) -> bool; + fn is_refreshing(&self, cx: &AppContext) -> bool; fn refresh( &self, buffer: Model, @@ -116,6 +118,10 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } + fn is_refreshing(&self, cx: &AppContext) -> bool { + self.read(cx).is_refreshing() + } + fn refresh( &self, buffer: Model, diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index 2029ab4da2..2416e42a9c 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -19,6 +19,7 @@ editor.workspace = true feature_flags.workspace = true fs.workspace = true gpui.workspace = true +inline_completion.workspace = true language.workspace = true paths.workspace = true settings.workspace = true diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 06a9885d2a..dc616833c4 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -4,8 +4,9 @@ use editor::{scroll::Autoscroll, Editor}; use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag}; use fs::Fs; use gpui::{ - actions, div, Action, AppContext, AsyncWindowContext, Corner, Entity, IntoElement, - ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext, + actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext, + AsyncWindowContext, Corner, Entity, IntoElement, ParentElement, Render, Subscription, View, + ViewContext, WeakView, WindowContext, }; use language::{ language_settings::{ @@ -14,7 +15,7 @@ use language::{ File, Language, }; use settings::{update_settings_file, Settings, SettingsStore}; -use std::{path::Path, sync::Arc}; +use std::{path::Path, sync::Arc, time::Duration}; use supermaven::{AccountStatus, Supermaven}; use workspace::{ create_and_open_local_file, @@ -39,6 +40,7 @@ pub struct InlineCompletionButton { editor_enabled: Option, language: Option>, file: Option>, + inline_completion_provider: Option>, fs: Arc, workspace: WeakView, } @@ -205,17 +207,34 @@ impl Render for InlineCompletionButton { } let this = cx.view().clone(); - div().child( - PopoverMenu::new("zeta") - .menu(move |cx| { - Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx))) - }) - .anchor(Corner::BottomRight) - .trigger( - IconButton::new("zeta", IconName::ZedPredict) - .tooltip(|cx| Tooltip::text("Zed Predict", cx)), + let button = IconButton::new("zeta", IconName::ZedPredict) + .tooltip(|cx| Tooltip::text("Zed Predict", cx)); + + let is_refreshing = self + .inline_completion_provider + .as_ref() + .map_or(false, |provider| provider.is_refreshing(cx)); + + let mut popover_menu = PopoverMenu::new("zeta") + .menu(move |cx| { + Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx))) + }) + .anchor(Corner::BottomRight); + if is_refreshing { + popover_menu = popover_menu.trigger( + button.with_animation( + "pulsating-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.2, 1.0)), + |icon_button, delta| icon_button.alpha(delta), ), - ) + ); + } else { + popover_menu = popover_menu.trigger(button); + } + + div().child(popover_menu.into_any_element()) } } } @@ -239,6 +258,7 @@ impl InlineCompletionButton { editor_enabled: None, language: None, file: None, + inline_completion_provider: None, workspace, fs, } @@ -390,6 +410,7 @@ impl InlineCompletionButton { ), ) }; + self.inline_completion_provider = editor.inline_completion_provider(); self.language = language.cloned(); self.file = file; diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index eea12959c3..e9c2bfb37b 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -19,7 +19,7 @@ pub struct SupermavenCompletionProvider { buffer_id: Option, completion_id: Option, file_extension: Option, - pending_refresh: Task>, + pending_refresh: Option>>, } impl SupermavenCompletionProvider { @@ -29,7 +29,7 @@ impl SupermavenCompletionProvider { buffer_id: None, completion_id: None, file_extension: None, - pending_refresh: Task::ready(Ok(())), + pending_refresh: None, } } } @@ -122,6 +122,10 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } + fn is_refreshing(&self) -> bool { + self.pending_refresh.is_some() + } + fn refresh( &mut self, buffer_handle: Model, @@ -135,7 +139,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { return; }; - self.pending_refresh = cx.spawn(|this, mut cx| async move { + self.pending_refresh = Some(cx.spawn(|this, mut cx| async move { if debounce { cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; } @@ -152,11 +156,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { .to_string(), ) }); + this.pending_refresh = None; cx.notify(); })?; } Ok(()) - }); + })); } fn cycle( @@ -169,12 +174,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { } fn accept(&mut self, _cx: &mut ModelContext) { - self.pending_refresh = Task::ready(Ok(())); + self.pending_refresh = None; self.completion_id = None; } fn discard(&mut self, _cx: &mut ModelContext) { - self.pending_refresh = Task::ready(Ok(())); + self.pending_refresh = None; self.completion_id = None; } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 3abe4bb309..1c37140e29 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -22,6 +22,7 @@ pub struct IconButton { icon_size: IconSize, icon_color: Color, selected_icon: Option, + alpha: Option, } impl IconButton { @@ -33,6 +34,7 @@ impl IconButton { icon_size: IconSize::default(), icon_color: Color::Default, selected_icon: None, + alpha: None, }; this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon)); this @@ -53,6 +55,11 @@ impl IconButton { self } + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = Some(alpha); + self + } + pub fn selected_icon(mut self, icon: impl Into>) -> Self { self.selected_icon = icon.into(); self @@ -146,6 +153,7 @@ impl RenderOnce for IconButton { let is_selected = self.base.selected; let selected_style = self.base.selected_style; + let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0)); self.base .map(|this| match self.shape { IconButtonShape::Square => { @@ -161,7 +169,7 @@ impl RenderOnce for IconButton { .selected_icon(self.selected_icon) .when_some(selected_style, |this, style| this.selected_style(style)) .size(self.icon_size) - .color(self.icon_color), + .color(Color::Custom(color)), ) } } diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 28fc881baf..9594960d9f 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -15,6 +15,28 @@ pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {} impl PopoverTrigger for T {} +impl Clickable for gpui::AnimationElement +where + T: Clickable + 'static, +{ + fn on_click(self, handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static) -> Self { + self.map_element(|e| e.on_click(handler)) + } + + fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self { + self.map_element(|e| e.cursor_style(cursor_style)) + } +} + +impl Toggleable for gpui::AnimationElement +where + T: Toggleable + 'static, +{ + fn toggle_state(self, selected: bool) -> Self { + self.map_element(|e| e.toggle_state(selected)) + } +} + pub struct PopoverMenuHandle(Rc>>>); impl Clone for PopoverMenuHandle { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 80c8dda6b5..a3c6b2928b 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1027,6 +1027,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } + fn is_refreshing(&self) -> bool { + !self.pending_completions.is_empty() + } + fn refresh( &mut self, buffer: Model,