Animate Zeta button while generating completions (#22899)
Release Notes: - N/A Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
parent
7d905d0791
commit
e64a56ffad
10 changed files with 106 additions and 29 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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(())
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue