Improve Zeta rating ergonomics (#21845)

This PR adds keyboard shortcuts to common interactions you might want to
do in the Zeta rating panel.

This PR also adds a way to fake inline completion requests, as well as
the test data used to create this PR, to make it easier to adjust the UI
in the future.

It also changes the status bar from the text "Zeta" to "ζ", because I
thought it looked cool.

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki 2024-12-11 01:57:46 -08:00 committed by GitHub
parent 63e1bf01a4
commit b36dcf3b92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 525 additions and 89 deletions

View file

@ -66,6 +66,7 @@
"cmd-v": "editor::Paste", "cmd-v": "editor::Paste",
"cmd-z": "editor::Undo", "cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo", "cmd-shift-z": "editor::Redo",
"ctrl-shift-z": "zeta::RateCompletions",
"up": "editor::MoveUp", "up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph", "ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::MovePageUp", "pageup": "editor::MovePageUp",
@ -788,5 +789,25 @@
"ctrl-k left": "pane::SplitLeft", "ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight" "ctrl-k right": "pane::SplitRight"
} }
},
{
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"cmd-delete": "zeta::ThumbsDown",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"space": "zeta::PreviewCompletion"
}
},
{
"context": "RateCompletionModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
} }
] ]

View file

@ -204,7 +204,7 @@ impl Render for InlineCompletionButton {
} }
div().child( div().child(
Button::new("zeta", "Zeta") Button::new("zeta", "ζ")
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() { if let Some(workspace) = this.workspace.upgrade() {

View file

@ -39,6 +39,7 @@ pub struct ListItem {
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
selectable: bool, selectable: bool,
overflow_x: bool, overflow_x: bool,
focused: Option<bool>,
} }
impl ListItem { impl ListItem {
@ -62,6 +63,7 @@ impl ListItem {
children: SmallVec::new(), children: SmallVec::new(),
selectable: true, selectable: true,
overflow_x: false, overflow_x: false,
focused: None,
} }
} }
@ -140,6 +142,11 @@ impl ListItem {
self.overflow_x = true; self.overflow_x = true;
self self
} }
pub fn focused(mut self, focused: bool) -> Self {
self.focused = Some(focused);
self
}
} }
impl Disableable for ListItem { impl Disableable for ListItem {
@ -177,9 +184,14 @@ impl RenderOnce for ListItem {
this this
// TODO: Add focus state // TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| { // .when(self.state == InteractionState::Focused, |this| {
// this.border_1() .when_some(self.focused, |this, focused| {
// .border_color(cx.theme().colors().border_focused) if focused {
// }) this.border_1()
.border_color(cx.theme().colors().border_focused)
} else {
this.border_1()
}
})
.when(self.selectable, |this| { .when(self.selectable, |this| {
this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active)) .active(|style| style.bg(cx.theme().colors().ghost_element_active))
@ -204,10 +216,15 @@ impl RenderOnce for ListItem {
.when(self.inset && !self.disabled, |this| { .when(self.inset && !self.disabled, |this| {
this this
// TODO: Add focus state // TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| { //.when(self.state == InteractionState::Focused, |this| {
// this.border_1() .when_some(self.focused, |this, focused| {
// .border_color(cx.theme().colors().border_focused) if focused {
// }) this.border_1()
.border_color(cx.theme().colors().border_focused)
} else {
this.border_1()
}
})
.when(self.selectable, |this| { .when(self.selectable, |this| {
this.hover(|style| { this.hover(|style| {
style.bg(cx.theme().colors().ghost_element_hover) style.bg(cx.theme().colors().ghost_element_hover)

View file

@ -463,6 +463,7 @@ fn main() {
welcome::init(cx); welcome::init(cx);
settings_ui::init(cx); settings_ui::init(cx);
extensions_ui::init(cx); extensions_ui::init(cx);
zeta::init(cx);
cx.observe_global::<SettingsStore>({ cx.observe_global::<SettingsStore>({
let languages = app_state.languages.clone(); let languages = app_state.languages.clone();

View file

@ -165,7 +165,7 @@ fn assign_inline_completion_provider(
} }
} }
language::language_settings::InlineCompletionProvider::Zeta => { language::language_settings::InlineCompletionProvider::Zeta => {
if cx.has_flag::<ZetaFeatureFlag>() { if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
let zeta = zeta::Zeta::register(client.clone(), cx); let zeta = zeta::Zeta::register(client.clone(), cx);
if let Some(buffer) = editor.buffer().read(cx).as_singleton() { if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if buffer.read(cx).file().is_some() { if buffer.read(cx).file().is_some() {

View file

@ -13,6 +13,9 @@ workspace = true
path = "src/zeta.rs" path = "src/zeta.rs"
doctest = false doctest = false
[features]
test-support = []
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
client.workspace = true client.workspace = true
@ -21,6 +24,7 @@ editor.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true http_client.workspace = true
indoc.workspace = true
inline_completion.workspace = true inline_completion.workspace = true
language.workspace = true language.workspace = true
language_models.workspace = true language_models.workspace = true
@ -32,8 +36,8 @@ settings.workspace = true
similar.workspace = true similar.workspace = true
telemetry_events.workspace = true telemetry_events.workspace = true
theme.workspace = true theme.workspace = true
util.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true
uuid.workspace = true uuid.workspace = true
workspace.workspace = true workspace.workspace = true

View file

@ -1,18 +1,44 @@
use crate::{InlineCompletion, InlineCompletionRating, Zeta}; use crate::{InlineCompletion, InlineCompletionRating, Zeta};
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle, actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
Model, StyledText, TextStyle, View, ViewContext, HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
}; };
use language::{language_settings, OffsetRangeExt}; use language::{language_settings, OffsetRangeExt};
use settings::Settings; use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, List, ListItem, ListItemSpacing, TintColor}; use ui::{prelude::*, List, ListItem, ListItemSpacing, TintColor};
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
actions!(
zeta,
[
RateCompletions,
ThumbsUp,
ThumbsDown,
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
PreviousEdit,
FocusCompletions,
PreviewCompletion,
]
);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &RateCompletions, cx| {
RateCompletionModal::toggle(workspace, cx);
});
})
.detach();
}
pub struct RateCompletionModal { pub struct RateCompletionModal {
zeta: Model<Zeta>, zeta: Model<Zeta>,
active_completion: Option<ActiveCompletion>, active_completion: Option<ActiveCompletion>,
selected_index: usize,
focus_handle: FocusHandle, focus_handle: FocusHandle,
_subscription: gpui::Subscription, _subscription: gpui::Subscription,
} }
@ -33,6 +59,7 @@ impl RateCompletionModal {
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify()); let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
Self { Self {
zeta, zeta,
selected_index: 0,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
active_completion: None, active_completion: None,
_subscription: subscription, _subscription: subscription,
@ -43,15 +70,211 @@ impl RateCompletionModal {
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
self.selected_index += 1;
self.selected_index = usize::min(
self.selected_index,
self.zeta.read(cx).recent_completions().count(),
);
cx.notify();
}
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
self.selected_index = self.selected_index.saturating_sub(1);
cx.notify();
}
fn select_next_edit(&mut self, _: &NextEdit, cx: &mut ViewContext<Self>) {
let next_index = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.enumerate()
.skip(1) // Skip straight to the next item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| ix + self.selected_index);
if let Some(next_index) = next_index {
self.selected_index = next_index;
cx.notify();
}
}
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
let zeta = self.zeta.read(cx);
let completions_len = zeta.recent_completions_len();
let prev_index = self
.zeta
.read(cx)
.recent_completions()
.rev()
.skip((completions_len - 1) - self.selected_index)
.enumerate()
.skip(1) // Skip straight to the previous item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| self.selected_index - ix);
if let Some(prev_index) = prev_index {
self.selected_index = prev_index;
cx.notify();
}
cx.notify();
}
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = 0;
cx.notify();
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
cx.notify();
}
fn thumbs_up(&mut self, _: &ThumbsUp, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
let completion = zeta
.recent_completions()
.skip(self.selected_index)
.next()
.cloned();
if let Some(completion) = completion {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
"".to_string(),
cx,
);
}
});
self.select_next_edit(&Default::default(), cx);
cx.notify();
}
fn thumbs_down(&mut self, _: &ThumbsDown, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
let completion = zeta
.recent_completions()
.skip(self.selected_index)
.next()
.cloned();
if let Some(completion) = completion {
zeta.rate_completion(
&completion,
InlineCompletionRating::Negative,
"".to_string(),
cx,
);
}
});
self.select_next_edit(&Default::default(), cx);
cx.notify();
}
fn thumbs_up_active(&mut self, _: &ThumbsUpActiveCompletion, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
if let Some(active) = &self.active_completion {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Positive,
active.feedback_editor.read(cx).text(cx),
cx,
);
}
});
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, cx);
self.select_next_edit(&Default::default(), cx);
self.confirm(&Default::default(), cx);
cx.notify();
}
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
self.zeta.update(cx, |zeta, cx| {
if let Some(active) = &self.active_completion {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Negative,
active.feedback_editor.read(cx).text(cx),
cx,
);
}
});
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, cx);
self.select_next_edit(&Default::default(), cx);
self.confirm(&Default::default(), cx);
cx.notify();
}
fn focus_completions(&mut self, _: &FocusCompletions, cx: &mut ViewContext<Self>) {
cx.focus_self();
cx.notify();
}
fn preview_completion(&mut self, _: &PreviewCompletion, cx: &mut ViewContext<Self>) {
let completion = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, false, cx);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let completion = self
.zeta
.read(cx)
.recent_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, true, cx);
}
pub fn select_completion( pub fn select_completion(
&mut self, &mut self,
completion: Option<InlineCompletion>, completion: Option<InlineCompletion>,
focus: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
// Avoid resetting completion rating if it's already selected. // Avoid resetting completion rating if it's already selected.
if let Some(completion) = completion.as_ref() { if let Some(completion) = completion.as_ref() {
self.selected_index = self
.zeta
.read(cx)
.recent_completions()
.enumerate()
.find(|(_, completion_b)| completion.id == completion_b.id)
.map(|(ix, _)| ix)
.unwrap_or(self.selected_index);
cx.notify();
if let Some(prev_completion) = self.active_completion.as_ref() { if let Some(prev_completion) = self.active_completion.as_ref() {
if completion.id == prev_completion.completion.id { if completion.id == prev_completion.completion.id {
if focus {
cx.focus_view(&prev_completion.feedback_editor);
}
return; return;
} }
} }
@ -70,9 +293,13 @@ impl RateCompletionModal {
editor.set_show_indent_guides(false, cx); editor.set_show_indent_guides(false, cx);
editor.set_show_inline_completions(Some(false), cx); editor.set_show_inline_completions(Some(false), cx);
editor.set_placeholder_text("Add your feedback about this completion…", cx); editor.set_placeholder_text("Add your feedback about this completion…", cx);
if focus {
cx.focus_self();
}
editor editor
}), }),
}); });
cx.notify();
} }
fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> { fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
@ -204,21 +431,12 @@ impl RateCompletionModal {
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.icon_color(Color::Error) .icon_color(Color::Error)
.disabled(rated) .disabled(rated)
.on_click({ .on_click(cx.listener(move |this, _, cx| {
let completion = active_completion.completion.clone(); this.thumbs_down_active(
let feedback_editor = &ThumbsDownActiveCompletion,
active_completion.feedback_editor.clone(); cx,
cx.listener(move |this, _, cx| { );
this.zeta.update(cx, |zeta, cx| { })),
zeta.rate_completion(
&completion,
InlineCompletionRating::Negative,
feedback_editor.read(cx).text(cx),
cx,
)
})
})
}),
) )
.child( .child(
Button::new("good", "Good Completion") Button::new("good", "Good Completion")
@ -228,21 +446,9 @@ impl RateCompletionModal {
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.icon_color(Color::Success) .icon_color(Color::Success)
.disabled(rated) .disabled(rated)
.on_click({ .on_click(cx.listener(move |this, _, cx| {
let completion = active_completion.completion.clone(); this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
let feedback_editor = })),
active_completion.feedback_editor.clone();
cx.listener(move |this, _, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&completion,
InlineCompletionRating::Positive,
feedback_editor.read(cx).text(cx),
cx,
)
})
})
}),
), ),
), ),
), ),
@ -257,7 +463,23 @@ impl Render for RateCompletionModal {
h_flex() h_flex()
.key_context("RateCompletionModal") .key_context("RateCompletionModal")
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.focus(|this| {
this.border_1().border_color(cx.theme().colors().border_focused)
})
.on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_prev_edit))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_next_edit))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::thumbs_up))
.on_action(cx.listener(Self::thumbs_down))
.on_action(cx.listener(Self::thumbs_up_active))
.on_action(cx.listener(Self::thumbs_down_active))
.on_action(cx.listener(Self::focus_completions))
.on_action(cx.listener(Self::preview_completion))
.bg(cx.theme().colors().elevated_surface_background) .bg(cx.theme().colors().elevated_surface_background)
.border_1() .border_1()
.border_color(border_color) .border_color(border_color)
@ -285,8 +507,8 @@ impl Render for RateCompletionModal {
) )
.into_any_element(), .into_any_element(),
) )
.children(self.zeta.read(cx).recent_completions().cloned().map( .children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|completion| { |(index, completion)| {
let selected = let selected =
self.active_completion.as_ref().map_or(false, |selected| { self.active_completion.as_ref().map_or(false, |selected| {
selected.completion.id == completion.id selected.completion.id == completion.id
@ -296,6 +518,7 @@ impl Render for RateCompletionModal {
ListItem::new(completion.id) ListItem::new(completion.id)
.inset(true) .inset(true)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.selected(selected) .selected(selected)
.start_slot(if rated { .start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success) Icon::new(IconName::Check).color(Color::Success)
@ -316,7 +539,7 @@ impl Render for RateCompletionModal {
.size(LabelSize::XSmall)), .size(LabelSize::XSmall)),
) )
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.select_completion(Some(completion.clone()), cx); this.select_completion(Some(completion.clone()), true, cx);
})) }))
}, },
)), )),

View file

@ -18,6 +18,7 @@ use std::{
borrow::Cow, borrow::Cow,
cmp, cmp,
fmt::Write, fmt::Write,
future::Future,
mem, mem,
ops::Range, ops::Range,
path::Path, path::Path,
@ -253,12 +254,17 @@ impl Zeta {
} }
} }
pub fn request_completion( pub fn request_completion_impl<F, R>(
&mut self, &mut self,
buffer: &Model<Buffer>, buffer: &Model<Buffer>,
position: language::Anchor, position: language::Anchor,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> { perform_predict_edits: F,
) -> Task<Result<InlineCompletion>>
where
F: FnOnce(Arc<Client>, LlmApiToken, PredictEditsParams) -> R + 'static,
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
{
let snapshot = self.report_changes_for_buffer(buffer, cx); let snapshot = self.report_changes_for_buffer(buffer, cx);
let point = position.to_point(&snapshot); let point = position.to_point(&snapshot);
let offset = point.to_offset(&snapshot); let offset = point.to_offset(&snapshot);
@ -292,7 +298,7 @@ impl Zeta {
input_excerpt: input_excerpt.clone(), input_excerpt: input_excerpt.clone(),
}; };
let response = Self::perform_predict_edits(&client, llm_token, body).await?; let response = perform_predict_edits(client, llm_token, body).await?;
let output_excerpt = response.output_excerpt; let output_excerpt = response.output_excerpt;
log::debug!("prediction took: {:?}", start.elapsed()); log::debug!("prediction took: {:?}", start.elapsed());
@ -320,50 +326,210 @@ impl Zeta {
}) })
} }
async fn perform_predict_edits( // Generates several example completions of various states to fill the Zeta completion modal
client: &Arc<Client>, #[cfg(any(test, feature = "test-support"))]
pub fn fill_with_fake_completions(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
"#};
let buffer = cx.new_model(|cx| Buffer::local(test_buffer_text, cx));
let position = buffer.read(cx).anchor_before(Point::new(1, 0));
let completion_tasks = vec![
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!("{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
[here's an edit]
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
", ),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
[and another edit]
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
[a third completion]
and then another
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
and then another
[fourth completion example]
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
self.fake_completion(
&buffer,
position,
PredictEditsResponse {
output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
And maybe a short line
Then a few lines
and then another
[fifth and final completion]
{EDITABLE_REGION_END_MARKER}
"#),
},
cx,
),
];
cx.spawn(|zeta, mut cx| async move {
for task in completion_tasks {
task.await.unwrap();
}
zeta.update(&mut cx, |zeta, _cx| {
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
})
.ok();
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake_completion(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
response: PredictEditsResponse,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
use std::future::ready;
self.request_completion_impl(buffer, position, cx, |_, _, _| ready(Ok(response)))
}
pub fn request_completion(
&mut self,
buffer: &Model<Buffer>,
position: language::Anchor,
cx: &mut ModelContext<Self>,
) -> Task<Result<InlineCompletion>> {
self.request_completion_impl(buffer, position, cx, Self::perform_predict_edits)
}
fn perform_predict_edits(
client: Arc<Client>,
llm_token: LlmApiToken, llm_token: LlmApiToken,
body: PredictEditsParams, body: PredictEditsParams,
) -> Result<PredictEditsResponse> { ) -> impl Future<Output = Result<PredictEditsResponse>> {
let http_client = client.http_client(); async move {
let mut token = llm_token.acquire(client).await?; let http_client = client.http_client();
let mut did_retry = false; let mut token = llm_token.acquire(&client).await?;
let mut did_retry = false;
loop { loop {
let request_builder = http_client::Request::builder(); let request_builder = http_client::Request::builder();
let request = request_builder let request = request_builder
.method(Method::POST) .method(Method::POST)
.uri( .uri(
http_client http_client
.build_zed_llm_url("/predict_edits", &[])? .build_zed_llm_url("/predict_edits", &[])?
.as_ref(), .as_ref(),
) )
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token)) .header("Authorization", format!("Bearer {}", token))
.body(serde_json::to_string(&body)?.into())?; .body(serde_json::to_string(&body)?.into())?;
let mut response = http_client.send(request).await?; let mut response = http_client.send(request).await?;
if response.status().is_success() { if response.status().is_success() {
let mut body = String::new(); let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?; response.body_mut().read_to_string(&mut body).await?;
return Ok(serde_json::from_str(&body)?); return Ok(serde_json::from_str(&body)?);
} else if !did_retry } else if !did_retry
&& response && response
.headers() .headers()
.get(EXPIRED_LLM_TOKEN_HEADER_NAME) .get(EXPIRED_LLM_TOKEN_HEADER_NAME)
.is_some() .is_some()
{ {
did_retry = true; did_retry = true;
token = llm_token.refresh(client).await?; token = llm_token.refresh(&client).await?;
} else { } else {
let mut body = String::new(); let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?; response.body_mut().read_to_string(&mut body).await?;
return Err(anyhow!( return Err(anyhow!(
"error predicting edits.\nStatus: {:?}\nBody: {}", "error predicting edits.\nStatus: {:?}\nBody: {}",
response.status(), response.status(),
body body
)); ));
}
} }
} }
} }
@ -409,7 +575,7 @@ impl Zeta {
}) })
} }
fn compute_edits( pub fn compute_edits(
old_text: String, old_text: String,
new_text: &str, new_text: &str,
offset: usize, offset: usize,
@ -500,10 +666,14 @@ impl Zeta {
cx.notify(); cx.notify();
} }
pub fn recent_completions(&self) -> impl Iterator<Item = &InlineCompletion> { pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
self.recent_completions.iter() self.recent_completions.iter()
} }
pub fn recent_completions_len(&self) -> usize {
self.recent_completions.len()
}
fn report_changes_for_buffer( fn report_changes_for_buffer(
&mut self, &mut self,
buffer: &Model<Buffer>, buffer: &Model<Buffer>,