Animate Zeta button while generating completions (#22899)

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
Antonio Scandurra 2025-01-09 16:24:35 +01:00 committed by GitHub
parent 7d905d0791
commit e64a56ffad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 106 additions and 29 deletions

1
Cargo.lock generated
View file

@ -6298,6 +6298,7 @@ dependencies = [
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"indoc", "indoc",
"inline_completion",
"language", "language",
"lsp", "lsp",
"paths", "paths",

View file

@ -17,8 +17,8 @@ pub struct CopilotCompletionProvider {
completions: Vec<Completion>, completions: Vec<Completion>,
active_completion_index: usize, active_completion_index: usize,
file_extension: Option<String>, file_extension: Option<String>,
pending_refresh: Task<Result<()>>, pending_refresh: Option<Task<Result<()>>>,
pending_cycling_refresh: Task<Result<()>>, pending_cycling_refresh: Option<Task<Result<()>>>,
copilot: Model<Copilot>, copilot: Model<Copilot>,
} }
@ -30,8 +30,8 @@ impl CopilotCompletionProvider {
completions: Vec::new(), completions: Vec::new(),
active_completion_index: 0, active_completion_index: 0,
file_extension: None, file_extension: None,
pending_refresh: Task::ready(Ok(())), pending_refresh: None,
pending_cycling_refresh: Task::ready(Ok(())), pending_cycling_refresh: None,
copilot, copilot,
} }
} }
@ -67,6 +67,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
false false
} }
fn is_refreshing(&self) -> bool {
self.pending_refresh.is_some()
}
fn is_enabled( fn is_enabled(
&self, &self,
buffer: &Model<Buffer>, buffer: &Model<Buffer>,
@ -92,7 +96,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let copilot = self.copilot.clone(); 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 { if debounce {
cx.background_executor() cx.background_executor()
.timer(COPILOT_DEBOUNCE_TIMEOUT) .timer(COPILOT_DEBOUNCE_TIMEOUT)
@ -108,7 +112,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if !completions.is_empty() { if !completions.is_empty() {
this.cycled = false; this.cycled = false;
this.pending_cycling_refresh = Task::ready(Ok(())); this.pending_refresh = None;
this.pending_cycling_refresh = None;
this.completions.clear(); this.completions.clear();
this.active_completion_index = 0; this.active_completion_index = 0;
this.buffer_id = Some(buffer.entity_id()); this.buffer_id = Some(buffer.entity_id());
@ -129,7 +134,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?; })?;
Ok(()) Ok(())
}); }));
} }
fn cycle( fn cycle(
@ -161,7 +166,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
cx.notify(); cx.notify();
} else { } else {
let copilot = self.copilot.clone(); 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 let completions = copilot
.update(&mut cx, |copilot, cx| { .update(&mut cx, |copilot, cx| {
copilot.completions_cycling(&buffer, cursor_position, cx) copilot.completions_cycling(&buffer, cursor_position, cx)
@ -185,7 +190,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?; })?;
Ok(()) Ok(())
}); }));
} }
} }

View file

@ -387,6 +387,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
true true
} }
fn is_refreshing(&self) -> bool {
false
}
fn refresh( fn refresh(
&mut self, &mut self,
_buffer: gpui::Model<language::Buffer>, _buffer: gpui::Model<language::Buffer>,

View file

@ -28,6 +28,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
cursor_position: language::Anchor, cursor_position: language::Anchor,
cx: &AppContext, cx: &AppContext,
) -> bool; ) -> bool;
fn is_refreshing(&self) -> bool;
fn refresh( fn refresh(
&mut self, &mut self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
@ -63,6 +64,7 @@ pub trait InlineCompletionProviderHandle {
) -> bool; ) -> bool;
fn show_completions_in_menu(&self) -> bool; fn show_completions_in_menu(&self) -> bool;
fn show_completions_in_normal_mode(&self) -> bool; fn show_completions_in_normal_mode(&self) -> bool;
fn is_refreshing(&self, cx: &AppContext) -> bool;
fn refresh( fn refresh(
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
@ -116,6 +118,10 @@ where
self.read(cx).is_enabled(buffer, cursor_position, cx) self.read(cx).is_enabled(buffer, cursor_position, cx)
} }
fn is_refreshing(&self, cx: &AppContext) -> bool {
self.read(cx).is_refreshing()
}
fn refresh( fn refresh(
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,

View file

@ -19,6 +19,7 @@ editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
fs.workspace = true fs.workspace = true
gpui.workspace = true gpui.workspace = true
inline_completion.workspace = true
language.workspace = true language.workspace = true
paths.workspace = true paths.workspace = true
settings.workspace = true settings.workspace = true

View file

@ -4,8 +4,9 @@ use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag}; use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
actions, div, Action, AppContext, AsyncWindowContext, Corner, Entity, IntoElement, actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext, AsyncWindowContext, Corner, Entity, IntoElement, ParentElement, Render, Subscription, View,
ViewContext, WeakView, WindowContext,
}; };
use language::{ use language::{
language_settings::{ language_settings::{
@ -14,7 +15,7 @@ use language::{
File, Language, File, Language,
}; };
use settings::{update_settings_file, Settings, SettingsStore}; 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 supermaven::{AccountStatus, Supermaven};
use workspace::{ use workspace::{
create_and_open_local_file, create_and_open_local_file,
@ -39,6 +40,7 @@ pub struct InlineCompletionButton {
editor_enabled: Option<bool>, editor_enabled: Option<bool>,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
} }
@ -205,17 +207,34 @@ impl Render for InlineCompletionButton {
} }
let this = cx.view().clone(); let this = cx.view().clone();
div().child( let button = IconButton::new("zeta", IconName::ZedPredict)
PopoverMenu::new("zeta") .tooltip(|cx| Tooltip::text("Zed Predict", cx));
.menu(move |cx| {
Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx))) let is_refreshing = self
}) .inline_completion_provider
.anchor(Corner::BottomRight) .as_ref()
.trigger( .map_or(false, |provider| provider.is_refreshing(cx));
IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| Tooltip::text("Zed Predict", 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, editor_enabled: None,
language: None, language: None,
file: None, file: None,
inline_completion_provider: None,
workspace, workspace,
fs, fs,
} }
@ -390,6 +410,7 @@ impl InlineCompletionButton {
), ),
) )
}; };
self.inline_completion_provider = editor.inline_completion_provider();
self.language = language.cloned(); self.language = language.cloned();
self.file = file; self.file = file;

View file

@ -19,7 +19,7 @@ pub struct SupermavenCompletionProvider {
buffer_id: Option<EntityId>, buffer_id: Option<EntityId>,
completion_id: Option<SupermavenCompletionStateId>, completion_id: Option<SupermavenCompletionStateId>,
file_extension: Option<String>, file_extension: Option<String>,
pending_refresh: Task<Result<()>>, pending_refresh: Option<Task<Result<()>>>,
} }
impl SupermavenCompletionProvider { impl SupermavenCompletionProvider {
@ -29,7 +29,7 @@ impl SupermavenCompletionProvider {
buffer_id: None, buffer_id: None,
completion_id: None, completion_id: None,
file_extension: 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) 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( fn refresh(
&mut self, &mut self,
buffer_handle: Model<Buffer>, buffer_handle: Model<Buffer>,
@ -135,7 +139,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
return; return;
}; };
self.pending_refresh = cx.spawn(|this, mut cx| async move { self.pending_refresh = Some(cx.spawn(|this, mut cx| async move {
if debounce { if debounce {
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
} }
@ -152,11 +156,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
.to_string(), .to_string(),
) )
}); });
this.pending_refresh = None;
cx.notify(); cx.notify();
})?; })?;
} }
Ok(()) Ok(())
}); }));
} }
fn cycle( fn cycle(
@ -169,12 +174,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
} }
fn accept(&mut self, _cx: &mut ModelContext<Self>) { fn accept(&mut self, _cx: &mut ModelContext<Self>) {
self.pending_refresh = Task::ready(Ok(())); self.pending_refresh = None;
self.completion_id = None; self.completion_id = None;
} }
fn discard(&mut self, _cx: &mut ModelContext<Self>) { fn discard(&mut self, _cx: &mut ModelContext<Self>) {
self.pending_refresh = Task::ready(Ok(())); self.pending_refresh = None;
self.completion_id = None; self.completion_id = None;
} }

View file

@ -22,6 +22,7 @@ pub struct IconButton {
icon_size: IconSize, icon_size: IconSize,
icon_color: Color, icon_color: Color,
selected_icon: Option<IconName>, selected_icon: Option<IconName>,
alpha: Option<f32>,
} }
impl IconButton { impl IconButton {
@ -33,6 +34,7 @@ impl IconButton {
icon_size: IconSize::default(), icon_size: IconSize::default(),
icon_color: Color::Default, icon_color: Color::Default,
selected_icon: None, selected_icon: None,
alpha: None,
}; };
this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon)); this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon));
this this
@ -53,6 +55,11 @@ impl IconButton {
self self
} }
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = Some(alpha);
self
}
pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self { pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into(); self.selected_icon = icon.into();
self self
@ -146,6 +153,7 @@ impl RenderOnce for IconButton {
let is_selected = self.base.selected; let is_selected = self.base.selected;
let selected_style = self.base.selected_style; let selected_style = self.base.selected_style;
let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0));
self.base self.base
.map(|this| match self.shape { .map(|this| match self.shape {
IconButtonShape::Square => { IconButtonShape::Square => {
@ -161,7 +169,7 @@ impl RenderOnce for IconButton {
.selected_icon(self.selected_icon) .selected_icon(self.selected_icon)
.when_some(selected_style, |this, style| this.selected_style(style)) .when_some(selected_style, |this, style| this.selected_style(style))
.size(self.icon_size) .size(self.icon_size)
.color(self.icon_color), .color(Color::Custom(color)),
) )
} }
} }

View file

@ -15,6 +15,28 @@ pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {} impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
impl<T: Clickable> Clickable for gpui::AnimationElement<T>
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<T: Toggleable> Toggleable for gpui::AnimationElement<T>
where
T: Toggleable + 'static,
{
fn toggle_state(self, selected: bool) -> Self {
self.map_element(|e| e.toggle_state(selected))
}
}
pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>); pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
impl<M> Clone for PopoverMenuHandle<M> { impl<M> Clone for PopoverMenuHandle<M> {

View file

@ -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) 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( fn refresh(
&mut self, &mut self,
buffer: Model<Buffer>, buffer: Model<Buffer>,