Fix the completions being too slow (#19013)

Closes https://github.com/zed-industries/zed/issues/19005

Release Notes:

- Fixed completion items inserted with a delay
([#19005](https://github.com/zed-industries/zed/issues/19005))

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
This commit is contained in:
Kirill Bulatov 2024-10-10 12:53:02 +03:00 committed by GitHub
parent f6f5ad138d
commit 5841ac406d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 65 additions and 145 deletions

View file

@ -379,75 +379,51 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.next() .next()
.await .await
.unwrap(); .unwrap();
cx_a.executor().finish_waiting();
// Open the buffer on the host. // Open the buffer on the host.
let buffer_a = project_a let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
cx_a.executor().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| { buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a. }") assert_eq!(buffer.text(), "fn main() { a. }")
}); });
// Confirm a completion on the guest.
editor_b.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
});
// Return a resolved completion from the host's language server. // Return a resolved completion from the host's language server.
// The resolved completion has an additional text edit. // The resolved completion has an additional text edit.
fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>( fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
|params, _| async move { |params, _| async move {
Ok(match params.label.as_str() { assert_eq!(params.label, "first_method(…)");
"first_method(…)" => lsp::CompletionItem { Ok(lsp::CompletionItem {
label: "first_method(…)".into(), label: "first_method(…)".into(),
detail: Some("fn(&mut self, B) -> C".into()), detail: Some("fn(&mut self, B) -> C".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
new_text: "first_method($1)".to_string(), new_text: "first_method($1)".to_string(),
range: lsp::Range::new( range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
lsp::Position::new(0, 14), })),
lsp::Position::new(0, 14), additional_text_edits: Some(vec![lsp::TextEdit {
), new_text: "use d::SomeTrait;\n".to_string(),
})), range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
additional_text_edits: Some(vec![lsp::TextEdit { }]),
new_text: "use d::SomeTrait;\n".to_string(), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), ..Default::default()
}]),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
..Default::default()
},
"second_method(…)" => lsp::CompletionItem {
label: "second_method(…)".into(),
detail: Some("fn(&mut self, C) -> D<E>".into()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
new_text: "second_method()".to_string(),
range: lsp::Range::new(
lsp::Position::new(0, 14),
lsp::Position::new(0, 14),
),
})),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
additional_text_edits: Some(vec![lsp::TextEdit {
new_text: "use d::SomeTrait;\n".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
}]),
..Default::default()
},
_ => panic!("unexpected completion label: {:?}", params.label),
}) })
}, },
); );
cx_a.executor().finish_waiting();
cx_a.executor().run_until_parked();
// Confirm a completion on the guest.
editor_b
.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
})
.unwrap()
.await
.unwrap();
cx_a.executor().run_until_parked();
cx_b.executor().run_until_parked();
// The additional edit is applied. // The additional edit is applied.
cx_a.executor().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| { buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!( assert_eq!(
buffer.text(), buffer.text(),
@ -540,15 +516,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
cx_b.executor().run_until_parked(); cx_b.executor().run_until_parked();
// When accepting the completion, the snippet is insert. // When accepting the completion, the snippet is insert.
editor_b
.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
})
.unwrap()
.await
.unwrap();
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!( assert_eq!(
editor.text(cx), editor.text(cx),
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }" "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"

View file

@ -363,12 +363,10 @@ mod tests {
// Confirming a completion inserts it and hides the context menu, without showing // Confirming a completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards. // the copilot suggestion afterwards.
editor.confirm_completion(&Default::default(), cx).unwrap() editor
}) .confirm_completion(&Default::default(), cx)
.await .unwrap()
.unwrap(); .detach();
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible()); assert!(!editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx)); assert!(!editor.has_active_inline_completion(cx));
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");

View file

@ -1,4 +1,4 @@
use std::{ops::ControlFlow, time::Duration}; use std::time::Duration;
use futures::{channel::oneshot, FutureExt}; use futures::{channel::oneshot, FutureExt};
use gpui::{Task, ViewContext}; use gpui::{Task, ViewContext};
@ -7,7 +7,7 @@ use crate::Editor;
pub struct DebouncedDelay { pub struct DebouncedDelay {
task: Option<Task<()>>, task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>, cancel_channel: Option<oneshot::Sender<()>>,
} }
impl DebouncedDelay { impl DebouncedDelay {
@ -23,22 +23,17 @@ impl DebouncedDelay {
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>, F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
{ {
if let Some(channel) = self.cancel_channel.take() { if let Some(channel) = self.cancel_channel.take() {
channel.send(ControlFlow::Break(())).ok(); _ = channel.send(());
} }
let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>(); let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender); self.cancel_channel = Some(sender);
drop(self.task.take()); drop(self.task.take());
self.task = Some(cx.spawn(move |model, mut cx| async move { self.task = Some(cx.spawn(move |model, mut cx| async move {
let mut timer = cx.background_executor().timer(delay).fuse(); let mut timer = cx.background_executor().timer(delay).fuse();
futures::select_biased! { futures::select_biased! {
interrupt = receiver => { _ = receiver => return,
match interrupt {
Ok(ControlFlow::Break(())) | Err(_) => return,
Ok(ControlFlow::Continue(())) => {},
}
}
_ = timer => {} _ = timer => {}
} }
@ -47,11 +42,4 @@ impl DebouncedDelay {
} }
})); }));
} }
pub fn start_now(&mut self) -> Option<Task<()>> {
if let Some(channel) = self.cancel_channel.take() {
channel.send(ControlFlow::Continue(())).ok();
}
self.task.take()
}
} }

View file

@ -4459,49 +4459,16 @@ impl Editor {
&mut self, &mut self,
item_ix: Option<usize>, item_ix: Option<usize>,
intent: CompletionIntent, intent: CompletionIntent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Editor>,
) -> Option<Task<anyhow::Result<()>>> { ) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
menu menu
} else { } else {
return None; return None;
}; };
let mut resolve_task_store = completions_menu
.selected_completion_documentation_resolve_debounce
.lock();
let selected_completion_resolve = resolve_task_store.start_now();
let menu_pre_resolve = self
.completion_documentation_pre_resolve_debounce
.start_now();
drop(resolve_task_store);
Some(cx.spawn(|editor, mut cx| async move {
match (selected_completion_resolve, menu_pre_resolve) {
(None, None) => {}
(Some(resolve), None) | (None, Some(resolve)) => resolve.await,
(Some(resolve_1), Some(resolve_2)) => {
futures::join!(resolve_1, resolve_2);
}
}
if let Some(apply_edits_task) = editor.update(&mut cx, |editor, cx| {
editor.apply_resolved_completion(completions_menu, item_ix, intent, cx)
})? {
apply_edits_task.await?;
}
Ok(())
}))
}
fn apply_resolved_completion(
&mut self,
completions_menu: CompletionsMenu,
item_ix: Option<usize>,
intent: CompletionIntent,
cx: &mut ViewContext<'_, Editor>,
) -> Option<Task<anyhow::Result<Option<language::Transaction>>>> {
use language::ToOffset as _;
let mat = completions_menu let mat = completions_menu
.matches .matches
.get(item_ix.unwrap_or(completions_menu.selected_item))?; .get(item_ix.unwrap_or(completions_menu.selected_item))?;
@ -4660,7 +4627,11 @@ impl Editor {
// so we should automatically call signature_help // so we should automatically call signature_help
self.show_signature_help(&ShowSignatureHelp, cx); self.show_signature_help(&ShowSignatureHelp, cx);
} }
Some(apply_edits)
Some(cx.foreground_executor().spawn(async move {
apply_edits.await?;
Ok(())
}))
} }
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) { pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {

View file

@ -7996,7 +7996,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
.unwrap() .unwrap()
}); });
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
one.ˇ one.second_completionˇ
two two
three three
"}); "});
@ -8029,9 +8029,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
one.second_completionˇ one.second_completionˇ
two two
thoverlapping additional editree three
additional edit
additional edit"}); "});
cx.set_state(indoc! {" cx.set_state(indoc! {"
one.second_completion one.second_completion
@ -8091,8 +8091,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
}); });
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
one.second_completion one.second_completion
two siˇ two sixth_completionˇ
three siˇ three sixth_completionˇ
additional edit additional edit
"}); "});
@ -8133,11 +8133,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
.confirm_completion(&ConfirmCompletion::default(), cx) .confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap() .unwrap()
}); });
cx.assert_editor_state("editor.cloˇ"); cx.assert_editor_state("editor.closeˇ");
handle_resolve_completion_request(&mut cx, None).await; handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap(); apply_additional_edits.await.unwrap();
cx.assert_editor_state(indoc! {"
editor.closeˇ"});
} }
#[gpui::test] #[gpui::test]
@ -10142,7 +10140,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
.confirm_completion(&ConfirmCompletion::default(), cx) .confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap() .unwrap()
}); });
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"}); cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| { cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let task_completion_item = completion_item.clone(); let task_completion_item = completion_item.clone();

View file

@ -810,7 +810,7 @@ mod tests {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
completion_provider: Some(lsp::CompletionOptions { completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]), trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
resolve_provider: Some(false), resolve_provider: Some(true),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -902,15 +902,12 @@ mod tests {
assert_eq!(counter.load(atomic::Ordering::Acquire), 1); assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
//apply a completion and check it was successfully applied //apply a completion and check it was successfully applied
let () = cx let _apply_additional_edits = cx.update_editor(|editor, cx| {
.update_editor(|editor, cx| { editor.context_menu_next(&Default::default(), cx);
editor.context_menu_next(&Default::default(), cx); editor
editor .confirm_completion(&ConfirmCompletion::default(), cx)
.confirm_completion(&ConfirmCompletion::default(), cx) .unwrap()
.unwrap() });
})
.await
.unwrap();
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
one.second_completionˇ one.second_completionˇ
two two

View file

@ -13,7 +13,6 @@ use itertools::Itertools;
use language::{Buffer, BufferSnapshot, LanguageRegistry}; use language::{Buffer, BufferSnapshot, LanguageRegistry};
use multi_buffer::{ExcerptRange, ToPoint}; use multi_buffer::{ExcerptRange, ToPoint};
use parking_lot::RwLock; use parking_lot::RwLock;
use pretty_assertions::assert_eq;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use std::{ use std::{
any::TypeId, any::TypeId,

View file

@ -631,7 +631,8 @@ impl LanguageServer {
"filterText".to_string(), "filterText".to_string(),
"labelDetails".to_string(), "labelDetails".to_string(),
"tags".to_string(), "tags".to_string(),
"textEdit".to_string(), // NB: Do not have this resolved, otherwise Zed becomes slow to complete things
// "textEdit".to_string(),
], ],
}), }),
insert_replace_support: Some(true), insert_replace_support: Some(true),

View file

@ -387,7 +387,7 @@ mod test {
lsp::ServerCapabilities { lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions { completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]), trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
resolve_provider: Some(false), resolve_provider: Some(true),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -432,9 +432,7 @@ mod test {
request.next().await; request.next().await;
cx.condition(|editor, _| editor.context_menu_visible()) cx.condition(|editor, _| editor.context_menu_visible())
.await; .await;
cx.simulate_keystrokes("down enter"); cx.simulate_keystrokes("down enter ! escape");
cx.run_until_parked();
cx.simulate_keystrokes("! escape");
cx.assert_state( cx.assert_state(
indoc! {" indoc! {"