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:
parent
f50bca7630
commit
a62a2fa8f7
8 changed files with 144 additions and 63 deletions
|
@ -379,51 +379,75 @@ 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 {
|
||||||
assert_eq!(params.label, "first_method(…)");
|
Ok(match params.label.as_str() {
|
||||||
Ok(lsp::CompletionItem {
|
"first_method(…)" => 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(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
range: lsp::Range::new(
|
||||||
})),
|
lsp::Position::new(0, 14),
|
||||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
lsp::Position::new(0, 14),
|
||||||
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 {
|
||||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
new_text: "use d::SomeTrait;\n".to_string(),
|
||||||
..Default::default()
|
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||||
|
}]),
|
||||||
|
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();
|
||||||
// The additional edit is applied.
|
|
||||||
cx_a.executor().run_until_parked();
|
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.
|
||||||
buffer_a.read_with(cx_a, |buffer, _| {
|
buffer_a.read_with(cx_a, |buffer, _| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.text(),
|
buffer.text(),
|
||||||
|
@ -516,9 +540,15 @@ 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(, , ) }"
|
||||||
|
|
|
@ -363,10 +363,12 @@ 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
|
editor.confirm_completion(&Default::default(), cx).unwrap()
|
||||||
.confirm_completion(&Default::default(), cx)
|
})
|
||||||
.unwrap()
|
.await
|
||||||
.detach();
|
.unwrap();
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::time::Duration;
|
use std::{ops::ControlFlow, 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<()>>,
|
cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DebouncedDelay {
|
impl DebouncedDelay {
|
||||||
|
@ -23,17 +23,22 @@ 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(());
|
channel.send(ControlFlow::Break(())).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>();
|
||||||
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! {
|
||||||
_ = receiver => return,
|
interrupt = receiver => {
|
||||||
|
match interrupt {
|
||||||
|
Ok(ControlFlow::Break(())) | Err(_) => return,
|
||||||
|
Ok(ControlFlow::Continue(())) => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
_ = timer => {}
|
_ = 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4427,16 +4427,49 @@ impl Editor {
|
||||||
&mut self,
|
&mut self,
|
||||||
item_ix: Option<usize>,
|
item_ix: Option<usize>,
|
||||||
intent: CompletionIntent,
|
intent: CompletionIntent,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
) -> Option<Task<anyhow::Result<()>>> {
|
||||||
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))?;
|
||||||
|
@ -4595,11 +4628,7 @@ 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>) {
|
||||||
|
|
|
@ -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.second_completionˇ
|
one.ˇ
|
||||||
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
|
||||||
three
|
thoverlapping additional editree
|
||||||
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 sixth_completionˇ
|
two siˇ
|
||||||
three sixth_completionˇ
|
three siˇ
|
||||||
additional edit
|
additional edit
|
||||||
"});
|
"});
|
||||||
|
|
||||||
|
@ -8133,9 +8133,11 @@ 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.closeˇ");
|
cx.assert_editor_state("editor.cloˇ");
|
||||||
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]
|
||||||
|
@ -10140,7 +10142,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.Some(2)ˇ; }"});
|
cx.assert_editor_state(indoc! {"fn main() { let a = 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();
|
||||||
|
|
|
@ -821,7 +821,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(true),
|
resolve_provider: Some(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -913,12 +913,15 @@ 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 _apply_additional_edits = cx.update_editor(|editor, cx| {
|
let () = cx
|
||||||
editor.context_menu_next(&Default::default(), cx);
|
.update_editor(|editor, cx| {
|
||||||
editor
|
editor.context_menu_next(&Default::default(), cx);
|
||||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
editor
|
||||||
.unwrap()
|
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||||
});
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
cx.assert_editor_state(indoc! {"
|
cx.assert_editor_state(indoc! {"
|
||||||
one.second_completionˇ
|
one.second_completionˇ
|
||||||
two
|
two
|
||||||
|
|
|
@ -13,6 +13,7 @@ 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,
|
||||||
|
|
|
@ -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(true),
|
resolve_provider: Some(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -432,7 +432,9 @@ 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 ! escape");
|
cx.simulate_keystrokes("down enter");
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.simulate_keystrokes("! escape");
|
||||||
|
|
||||||
cx.assert_state(
|
cx.assert_state(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue