Always let two completions race with each other (#21919)
When a user types, chances are the model will anticipate what they are about to do. Previously, we would continuously cancel the pending completion until the user stopped typing. With this commit, we allow at most two completions to race with each other (the first and the last one): - If the completion that was requested first completes first, we will show it (assuming we can interpolate it) but avoid canceling the last one. - When the completion that was requested last completes, we will cancel the first one if it's pending. In both cases, if a completion is already on-screen we have a special case for when the completions are just insertions and the new completion is a superset of the existing one. In this case, we will replace the existing completion with the new one. Otherwise we will keep showing the old one to avoid thrashing the UI. This should make latency a lot better. Note that I also reduced the debounce timeout to 8ms. Release Notes: - N/A
This commit is contained in:
parent
91b02a6259
commit
ad4c4aff13
4 changed files with 124 additions and 46 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -16434,7 +16434,6 @@ dependencies = [
|
||||||
"tree-sitter-go",
|
"tree-sitter-go",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
"workspace",
|
"workspace",
|
||||||
"worktree",
|
"worktree",
|
||||||
|
|
|
@ -37,7 +37,6 @@ similar.workspace = true
|
||||||
telemetry_events.workspace = true
|
telemetry_events.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
|
@ -58,7 +57,6 @@ settings = { workspace = true, features = ["test-support"] }
|
||||||
theme = { workspace = true, features = ["test-support"] }
|
theme = { workspace = true, features = ["test-support"] }
|
||||||
tree-sitter-go.workspace = true
|
tree-sitter-go.workspace = true
|
||||||
tree-sitter-rust.workspace = true
|
tree-sitter-rust.workspace = true
|
||||||
util = { workspace = true, features = ["test-support"] }
|
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
worktree = { workspace = true, features = ["test-support"] }
|
worktree = { workspace = true, features = ["test-support"] }
|
||||||
call = { workspace = true, features = ["test-support"] }
|
call = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -344,6 +344,7 @@ impl RateCompletionModal {
|
||||||
};
|
};
|
||||||
|
|
||||||
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
|
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
|
||||||
|
let was_shown = self.zeta.read(cx).was_completion_shown(completion_id);
|
||||||
let feedback_empty = active_completion
|
let feedback_empty = active_completion
|
||||||
.feedback_editor
|
.feedback_editor
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -426,6 +427,16 @@ impl RateCompletionModal {
|
||||||
)
|
)
|
||||||
.child(Label::new("No edits produced.").color(Color::Muted)),
|
.child(Label::new("No edits produced.").color(Color::Muted)),
|
||||||
)
|
)
|
||||||
|
} else if !was_shown {
|
||||||
|
Some(
|
||||||
|
label_container()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Warning)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Warning),
|
||||||
|
)
|
||||||
|
.child(Label::new("Completion wasn't shown because another valid completion was already on screen").color(Color::Warning)),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Some(label_container())
|
Some(label_container())
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,6 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use telemetry_events::InlineCompletionRating;
|
use telemetry_events::InlineCompletionRating;
|
||||||
use util::ResultExt;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||||
|
@ -86,7 +85,7 @@ impl InlineCompletion {
|
||||||
.duration_since(self.request_sent_at)
|
.duration_since(self.request_sent_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interpolate(&self, new_snapshot: BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
|
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
|
|
||||||
let mut user_edits = new_snapshot
|
let mut user_edits = new_snapshot
|
||||||
|
@ -131,9 +130,13 @@ impl InlineCompletion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if edits.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
Some(edits)
|
Some(edits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for InlineCompletion {
|
impl std::fmt::Debug for InlineCompletion {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
@ -151,6 +154,7 @@ pub struct Zeta {
|
||||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||||
recent_completions: VecDeque<InlineCompletion>,
|
recent_completions: VecDeque<InlineCompletion>,
|
||||||
rated_completions: HashSet<InlineCompletionId>,
|
rated_completions: HashSet<InlineCompletionId>,
|
||||||
|
shown_completions: HashSet<InlineCompletionId>,
|
||||||
llm_token: LlmApiToken,
|
llm_token: LlmApiToken,
|
||||||
_llm_token_subscription: Subscription,
|
_llm_token_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
@ -180,6 +184,7 @@ impl Zeta {
|
||||||
events: VecDeque::new(),
|
events: VecDeque::new(),
|
||||||
recent_completions: VecDeque::new(),
|
recent_completions: VecDeque::new(),
|
||||||
rated_completions: HashSet::default(),
|
rated_completions: HashSet::default(),
|
||||||
|
shown_completions: HashSet::default(),
|
||||||
registered_buffers: HashMap::default(),
|
registered_buffers: HashMap::default(),
|
||||||
llm_token: LlmApiToken::default(),
|
llm_token: LlmApiToken::default(),
|
||||||
_llm_token_subscription: cx.subscribe(
|
_llm_token_subscription: cx.subscribe(
|
||||||
|
@ -329,7 +334,9 @@ impl Zeta {
|
||||||
this.recent_completions
|
this.recent_completions
|
||||||
.push_front(inline_completion.clone());
|
.push_front(inline_completion.clone());
|
||||||
if this.recent_completions.len() > 50 {
|
if this.recent_completions.len() > 50 {
|
||||||
this.recent_completions.pop_back();
|
let completion = this.recent_completions.pop_back().unwrap();
|
||||||
|
this.shown_completions.remove(&completion.id);
|
||||||
|
this.rated_completions.remove(&completion.id);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
@ -665,6 +672,14 @@ and then another
|
||||||
self.rated_completions.contains(&completion_id)
|
self.rated_completions.contains(&completion_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn was_completion_shown(&self, completion_id: InlineCompletionId) -> bool {
|
||||||
|
self.shown_completions.contains(&completion_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn completion_shown(&mut self, completion_id: InlineCompletionId) {
|
||||||
|
self.shown_completions.insert(completion_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rate_completion(
|
pub fn rate_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
completion: &InlineCompletion,
|
completion: &InlineCompletion,
|
||||||
|
@ -855,25 +870,51 @@ impl Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
struct CurrentInlineCompletion {
|
struct CurrentInlineCompletion {
|
||||||
buffer_id: EntityId,
|
buffer_id: EntityId,
|
||||||
completion: InlineCompletion,
|
completion: InlineCompletion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CurrentInlineCompletion {
|
||||||
|
fn should_replace_completion(&self, old_completion: &Self, snapshot: &BufferSnapshot) -> bool {
|
||||||
|
if self.buffer_id != old_completion.buffer_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(old_edits) = old_completion.completion.interpolate(&snapshot) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let Some(new_edits) = self.completion.interpolate(&snapshot) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if old_edits.len() == 1 && new_edits.len() == 1 {
|
||||||
|
let (old_range, old_text) = &old_edits[0];
|
||||||
|
let (new_range, new_text) = &new_edits[0];
|
||||||
|
new_range == old_range && new_text.starts_with(old_text)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ZetaInlineCompletionProvider {
|
pub struct ZetaInlineCompletionProvider {
|
||||||
zeta: Model<Zeta>,
|
zeta: Model<Zeta>,
|
||||||
|
first_pending_completion: Option<Task<Result<()>>>,
|
||||||
|
last_pending_completion: Option<Task<Result<()>>>,
|
||||||
current_completion: Option<CurrentInlineCompletion>,
|
current_completion: Option<CurrentInlineCompletion>,
|
||||||
pending_refresh: Task<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ZetaInlineCompletionProvider {
|
impl ZetaInlineCompletionProvider {
|
||||||
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8);
|
||||||
|
|
||||||
pub fn new(zeta: Model<Zeta>) -> Self {
|
pub fn new(zeta: Model<Zeta>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
zeta,
|
zeta,
|
||||||
|
first_pending_completion: None,
|
||||||
|
last_pending_completion: None,
|
||||||
current_completion: None,
|
current_completion: None,
|
||||||
pending_refresh: Task::ready(()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -903,8 +944,8 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
debounce: bool,
|
debounce: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.pending_refresh =
|
let is_first = self.first_pending_completion.is_none();
|
||||||
cx.spawn(|this, mut cx| async move {
|
let task = cx.spawn(|this, mut cx| async move {
|
||||||
if debounce {
|
if debounce {
|
||||||
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
|
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
|
||||||
}
|
}
|
||||||
|
@ -917,20 +958,43 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
|
|
||||||
let mut completion = None;
|
let mut completion = None;
|
||||||
if let Ok(completion_request) = completion_request {
|
if let Ok(completion_request) = completion_request {
|
||||||
completion = completion_request.await.log_err().map(|completion| {
|
completion = Some(CurrentInlineCompletion {
|
||||||
CurrentInlineCompletion {
|
|
||||||
buffer_id: buffer.entity_id(),
|
buffer_id: buffer.entity_id(),
|
||||||
completion,
|
completion: completion_request.await?,
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.current_completion = completion;
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
this.first_pending_completion = None;
|
||||||
.ok();
|
if !is_first {
|
||||||
|
this.last_pending_completion = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_completion) = completion {
|
||||||
|
if let Some(old_completion) = this.current_completion.as_ref() {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
if new_completion.should_replace_completion(&old_completion, &snapshot) {
|
||||||
|
this.zeta.update(cx, |zeta, _cx| {
|
||||||
|
zeta.completion_shown(new_completion.completion.id)
|
||||||
});
|
});
|
||||||
|
this.current_completion = Some(new_completion);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.zeta.update(cx, |zeta, _cx| {
|
||||||
|
zeta.completion_shown(new_completion.completion.id)
|
||||||
|
});
|
||||||
|
this.current_completion = Some(new_completion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_first {
|
||||||
|
self.first_pending_completion = Some(task);
|
||||||
|
} else {
|
||||||
|
self.last_pending_completion = Some(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cycle(
|
fn cycle(
|
||||||
|
@ -943,9 +1007,14 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
// Right now we don't support cycling.
|
// Right now we don't support cycling.
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept(&mut self, _cx: &mut ModelContext<Self>) {}
|
fn accept(&mut self, _cx: &mut ModelContext<Self>) {
|
||||||
|
self.first_pending_completion.take();
|
||||||
|
self.last_pending_completion.take();
|
||||||
|
}
|
||||||
|
|
||||||
fn discard(&mut self, _cx: &mut ModelContext<Self>) {
|
fn discard(&mut self, _cx: &mut ModelContext<Self>) {
|
||||||
|
self.first_pending_completion.take();
|
||||||
|
self.last_pending_completion.take();
|
||||||
self.current_completion.take();
|
self.current_completion.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -958,6 +1027,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
let CurrentInlineCompletion {
|
let CurrentInlineCompletion {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
completion,
|
completion,
|
||||||
|
..
|
||||||
} = self.current_completion.as_mut()?;
|
} = self.current_completion.as_mut()?;
|
||||||
|
|
||||||
// Invalidate previous completion if it was generated for a different buffer.
|
// Invalidate previous completion if it was generated for a different buffer.
|
||||||
|
@ -967,7 +1037,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
let Some(edits) = completion.interpolate(buffer.snapshot()) else {
|
let Some(edits) = completion.interpolate(&buffer.snapshot()) else {
|
||||||
self.current_completion.take();
|
self.current_completion.take();
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
@ -1044,7 +1114,7 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1054,7 +1124,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1064,7 +1134,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1074,7 +1144,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1084,7 +1154,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1094,7 +1164,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1104,7 +1174,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1114,7 +1184,7 @@ mod tests {
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
from_completion_edits(
|
from_completion_edits(
|
||||||
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
|
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||||
&buffer,
|
&buffer,
|
||||||
cx
|
cx
|
||||||
),
|
),
|
||||||
|
@ -1122,7 +1192,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
|
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
|
||||||
assert_eq!(completion.interpolate(buffer.read(cx).snapshot()), None);
|
assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue