Always wait for completion resolve before applying the completion edits (#18907)

After https://github.com/rust-lang/rust-analyzer/pull/18167 and certain
people who type and complete rapidly, it turned out that we have not
waited for `completionItem/resolve` to finish before applying the
completion results.

Release Notes:

- Fixed completion items applied improperly on fast typing
This commit is contained in:
Kirill Bulatov 2024-10-09 17:18:20 +03:00 committed by GitHub
parent f50bca7630
commit a62a2fa8f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 144 additions and 63 deletions

View file

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

@ -4427,16 +4427,49 @@ impl Editor {
&mut self,
item_ix: Option<usize>,
intent: CompletionIntent,
cx: &mut ViewContext<Editor>,
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
cx: &mut ViewContext<Self>,
) -> Option<Task<anyhow::Result<()>>> {
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
menu
} else {
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
.matches
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
@ -4595,11 +4628,7 @@ impl Editor {
// so we should automatically call signature_help
self.show_signature_help(&ShowSignatureHelp, cx);
}
Some(cx.foreground_executor().spawn(async move {
apply_edits.await?;
Ok(())
}))
Some(apply_edits)
}
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()
});
cx.assert_editor_state(indoc! {"
one.second_completionˇ
one.ˇ
two
three
"});
@ -8029,9 +8029,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state(indoc! {"
one.second_completionˇ
two
three
additional edit
"});
thoverlapping additional editree
additional edit"});
cx.set_state(indoc! {"
one.second_completion
@ -8091,8 +8091,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
});
cx.assert_editor_state(indoc! {"
one.second_completion
two sixth_completionˇ
three sixth_completionˇ
two siˇ
three siˇ
additional edit
"});
@ -8133,9 +8133,11 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
cx.assert_editor_state("editor.closeˇ");
cx.assert_editor_state("editor.cloˇ");
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
cx.assert_editor_state(indoc! {"
editor.closeˇ"});
}
#[gpui::test]
@ -10140,7 +10142,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let task_completion_item = completion_item.clone();

View file

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

View file

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