Skip to content

Commit a62a2fa

Browse files
Always wait for completion resolve before applying the completion edits (#18907)
After rust-lang/rust-analyzer#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
1 parent f50bca7 commit a62a2fa

File tree

8 files changed

+143
-62
lines changed

8 files changed

+143
-62
lines changed

crates/collab/src/tests/editor_tests.rs

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -379,51 +379,75 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
379379
.next()
380380
.await
381381
.unwrap();
382-
cx_a.executor().finish_waiting();
383-
384382
// Open the buffer on the host.
385383
let buffer_a = project_a
386384
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
387385
.await
388386
.unwrap();
389-
cx_a.executor().run_until_parked();
390387

391388
buffer_a.read_with(cx_a, |buffer, _| {
392389
assert_eq!(buffer.text(), "fn main() { a. }")
393390
});
394391

395-
// Confirm a completion on the guest.
396-
editor_b.update(cx_b, |editor, cx| {
397-
assert!(editor.context_menu_visible());
398-
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
399-
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
400-
});
401-
402392
// Return a resolved completion from the host's language server.
403393
// The resolved completion has an additional text edit.
404394
fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
405395
|params, _| async move {
406-
assert_eq!(params.label, "first_method(…)");
407-
Ok(lsp::CompletionItem {
408-
label: "first_method(…)".into(),
409-
detail: Some("fn(&mut self, B) -> C".into()),
410-
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
411-
new_text: "first_method($1)".to_string(),
412-
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
413-
})),
414-
additional_text_edits: Some(vec![lsp::TextEdit {
415-
new_text: "use d::SomeTrait;\n".to_string(),
416-
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
417-
}]),
418-
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
419-
..Default::default()
396+
Ok(match params.label.as_str() {
397+
"first_method(…)" => lsp::CompletionItem {
398+
label: "first_method(…)".into(),
399+
detail: Some("fn(&mut self, B) -> C".into()),
400+
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
401+
new_text: "first_method($1)".to_string(),
402+
range: lsp::Range::new(
403+
lsp::Position::new(0, 14),
404+
lsp::Position::new(0, 14),
405+
),
406+
})),
407+
additional_text_edits: Some(vec![lsp::TextEdit {
408+
new_text: "use d::SomeTrait;\n".to_string(),
409+
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
410+
}]),
411+
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
412+
..Default::default()
413+
},
414+
"second_method(…)" => lsp::CompletionItem {
415+
label: "second_method(…)".into(),
416+
detail: Some("fn(&mut self, C) -> D<E>".into()),
417+
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
418+
new_text: "second_method()".to_string(),
419+
range: lsp::Range::new(
420+
lsp::Position::new(0, 14),
421+
lsp::Position::new(0, 14),
422+
),
423+
})),
424+
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
425+
additional_text_edits: Some(vec![lsp::TextEdit {
426+
new_text: "use d::SomeTrait;\n".to_string(),
427+
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
428+
}]),
429+
..Default::default()
430+
},
431+
_ => panic!("unexpected completion label: {:?}", params.label),
420432
})
421433
},
422434
);
435+
cx_a.executor().finish_waiting();
436+
cx_a.executor().run_until_parked();
423437

424-
// The additional edit is applied.
438+
// Confirm a completion on the guest.
439+
editor_b
440+
.update(cx_b, |editor, cx| {
441+
assert!(editor.context_menu_visible());
442+
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
443+
})
444+
.unwrap()
445+
.await
446+
.unwrap();
425447
cx_a.executor().run_until_parked();
448+
cx_b.executor().run_until_parked();
426449

450+
// The additional edit is applied.
427451
buffer_a.read_with(cx_a, |buffer, _| {
428452
assert_eq!(
429453
buffer.text(),
@@ -516,9 +540,15 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
516540
cx_b.executor().run_until_parked();
517541

518542
// When accepting the completion, the snippet is insert.
543+
editor_b
544+
.update(cx_b, |editor, cx| {
545+
assert!(editor.context_menu_visible());
546+
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
547+
})
548+
.unwrap()
549+
.await
550+
.unwrap();
519551
editor_b.update(cx_b, |editor, cx| {
520-
assert!(editor.context_menu_visible());
521-
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
522552
assert_eq!(
523553
editor.text(cx),
524554
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"

crates/copilot/src/copilot_completion_provider.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -363,10 +363,12 @@ mod tests {
363363

364364
// Confirming a completion inserts it and hides the context menu, without showing
365365
// the copilot suggestion afterwards.
366-
editor
367-
.confirm_completion(&Default::default(), cx)
368-
.unwrap()
369-
.detach();
366+
editor.confirm_completion(&Default::default(), cx).unwrap()
367+
})
368+
.await
369+
.unwrap();
370+
371+
cx.update_editor(|editor, cx| {
370372
assert!(!editor.context_menu_visible());
371373
assert!(!editor.has_active_inline_completion(cx));
372374
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");

crates/editor/src/debounced_delay.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::time::Duration;
1+
use std::{ops::ControlFlow, time::Duration};
22

33
use futures::{channel::oneshot, FutureExt};
44
use gpui::{Task, ViewContext};
@@ -7,7 +7,7 @@ use crate::Editor;
77

88
pub struct DebouncedDelay {
99
task: Option<Task<()>>,
10-
cancel_channel: Option<oneshot::Sender<()>>,
10+
cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>,
1111
}
1212

1313
impl DebouncedDelay {
@@ -23,17 +23,22 @@ impl DebouncedDelay {
2323
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
2424
{
2525
if let Some(channel) = self.cancel_channel.take() {
26-
_ = channel.send(());
26+
channel.send(ControlFlow::Break(())).ok();
2727
}
2828

29-
let (sender, mut receiver) = oneshot::channel::<()>();
29+
let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>();
3030
self.cancel_channel = Some(sender);
3131

3232
drop(self.task.take());
3333
self.task = Some(cx.spawn(move |model, mut cx| async move {
3434
let mut timer = cx.background_executor().timer(delay).fuse();
3535
futures::select_biased! {
36-
_ = receiver => return,
36+
interrupt = receiver => {
37+
match interrupt {
38+
Ok(ControlFlow::Break(())) | Err(_) => return,
39+
Ok(ControlFlow::Continue(())) => {},
40+
}
41+
}
3742
_ = timer => {}
3843
}
3944

@@ -42,4 +47,11 @@ impl DebouncedDelay {
4247
}
4348
}));
4449
}
50+
51+
pub fn start_now(&mut self) -> Option<Task<()>> {
52+
if let Some(channel) = self.cancel_channel.take() {
53+
channel.send(ControlFlow::Continue(())).ok();
54+
}
55+
self.task.take()
56+
}
4557
}

crates/editor/src/editor.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4427,16 +4427,49 @@ impl Editor {
44274427
&mut self,
44284428
item_ix: Option<usize>,
44294429
intent: CompletionIntent,
4430-
cx: &mut ViewContext<Editor>,
4431-
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
4432-
use language::ToOffset as _;
4433-
4430+
cx: &mut ViewContext<Self>,
4431+
) -> Option<Task<anyhow::Result<()>>> {
44344432
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
44354433
menu
44364434
} else {
44374435
return None;
44384436
};
44394437

4438+
let mut resolve_task_store = completions_menu
4439+
.selected_completion_documentation_resolve_debounce
4440+
.lock();
4441+
let selected_completion_resolve = resolve_task_store.start_now();
4442+
let menu_pre_resolve = self
4443+
.completion_documentation_pre_resolve_debounce
4444+
.start_now();
4445+
drop(resolve_task_store);
4446+
4447+
Some(cx.spawn(|editor, mut cx| async move {
4448+
match (selected_completion_resolve, menu_pre_resolve) {
4449+
(None, None) => {}
4450+
(Some(resolve), None) | (None, Some(resolve)) => resolve.await,
4451+
(Some(resolve_1), Some(resolve_2)) => {
4452+
futures::join!(resolve_1, resolve_2);
4453+
}
4454+
}
4455+
if let Some(apply_edits_task) = editor.update(&mut cx, |editor, cx| {
4456+
editor.apply_resolved_completion(completions_menu, item_ix, intent, cx)
4457+
})? {
4458+
apply_edits_task.await?;
4459+
}
4460+
Ok(())
4461+
}))
4462+
}
4463+
4464+
fn apply_resolved_completion(
4465+
&mut self,
4466+
completions_menu: CompletionsMenu,
4467+
item_ix: Option<usize>,
4468+
intent: CompletionIntent,
4469+
cx: &mut ViewContext<'_, Editor>,
4470+
) -> Option<Task<anyhow::Result<Option<language::Transaction>>>> {
4471+
use language::ToOffset as _;
4472+
44404473
let mat = completions_menu
44414474
.matches
44424475
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
@@ -4595,11 +4628,7 @@ impl Editor {
45954628
// so we should automatically call signature_help
45964629
self.show_signature_help(&ShowSignatureHelp, cx);
45974630
}
4598-
4599-
Some(cx.foreground_executor().spawn(async move {
4600-
apply_edits.await?;
4601-
Ok(())
4602-
}))
4631+
Some(apply_edits)
46034632
}
46044633

46054634
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {

crates/editor/src/editor_tests.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7996,7 +7996,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
79967996
.unwrap()
79977997
});
79987998
cx.assert_editor_state(indoc! {"
7999-
one.second_completionˇ
7999+
one.ˇ
80008000
two
80018001
three
80028002
"});
@@ -8029,9 +8029,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
80298029
cx.assert_editor_state(indoc! {"
80308030
one.second_completionˇ
80318031
two
8032-
three
8033-
additional edit
8034-
"});
8032+
thoverlapping additional editree
8033+
8034+
additional edit"});
80358035

80368036
cx.set_state(indoc! {"
80378037
one.second_completion
@@ -8091,8 +8091,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
80918091
});
80928092
cx.assert_editor_state(indoc! {"
80938093
one.second_completion
8094-
two sixth_completionˇ
8095-
three sixth_completionˇ
8094+
two siˇ
8095+
three siˇ
80968096
additional edit
80978097
"});
80988098

@@ -8133,9 +8133,11 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
81338133
.confirm_completion(&ConfirmCompletion::default(), cx)
81348134
.unwrap()
81358135
});
8136-
cx.assert_editor_state("editor.closeˇ");
8136+
cx.assert_editor_state("editor.cloˇ");
81378137
handle_resolve_completion_request(&mut cx, None).await;
81388138
apply_additional_edits.await.unwrap();
8139+
cx.assert_editor_state(indoc! {"
8140+
editor.closeˇ"});
81398141
}
81408142

81418143
#[gpui::test]
@@ -10140,7 +10142,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
1014010142
.confirm_completion(&ConfirmCompletion::default(), cx)
1014110143
.unwrap()
1014210144
});
10143-
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
10145+
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
1014410146

1014510147
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
1014610148
let task_completion_item = completion_item.clone();

crates/editor/src/hover_popover.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ mod tests {
821821
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
822822
completion_provider: Some(lsp::CompletionOptions {
823823
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
824-
resolve_provider: Some(true),
824+
resolve_provider: Some(false),
825825
..Default::default()
826826
}),
827827
..Default::default()
@@ -913,12 +913,15 @@ mod tests {
913913
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
914914

915915
//apply a completion and check it was successfully applied
916-
let _apply_additional_edits = cx.update_editor(|editor, cx| {
917-
editor.context_menu_next(&Default::default(), cx);
918-
editor
919-
.confirm_completion(&ConfirmCompletion::default(), cx)
920-
.unwrap()
921-
});
916+
let () = cx
917+
.update_editor(|editor, cx| {
918+
editor.context_menu_next(&Default::default(), cx);
919+
editor
920+
.confirm_completion(&ConfirmCompletion::default(), cx)
921+
.unwrap()
922+
})
923+
.await
924+
.unwrap();
922925
cx.assert_editor_state(indoc! {"
923926
one.second_completionˇ
924927
two

crates/editor/src/test/editor_test_context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use itertools::Itertools;
1313
use language::{Buffer, BufferSnapshot, LanguageRegistry};
1414
use multi_buffer::{ExcerptRange, ToPoint};
1515
use parking_lot::RwLock;
16+
use pretty_assertions::assert_eq;
1617
use project::{FakeFs, Project};
1718
use std::{
1819
any::TypeId,

crates/vim/src/normal/repeat.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ mod test {
387387
lsp::ServerCapabilities {
388388
completion_provider: Some(lsp::CompletionOptions {
389389
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
390-
resolve_provider: Some(true),
390+
resolve_provider: Some(false),
391391
..Default::default()
392392
}),
393393
..Default::default()
@@ -432,7 +432,9 @@ mod test {
432432
request.next().await;
433433
cx.condition(|editor, _| editor.context_menu_visible())
434434
.await;
435-
cx.simulate_keystrokes("down enter ! escape");
435+
cx.simulate_keystrokes("down enter");
436+
cx.run_until_parked();
437+
cx.simulate_keystrokes("! escape");
436438

437439
cx.assert_state(
438440
indoc! {"

0 commit comments

Comments
 (0)