Resolve completion items once exactly (#22448)
Closes https://github.com/zed-industries/zed/issues/19214 Closes https://github.com/zed-industries/zed/pull/22443 Adds `resolved` property into Zed completion item data, to ensure we resolve every completion item exactly once. There are 2 paths for singplayer Zed, and corresponding 2 analogues for multi player code, where resolve may happen: * completions menu display & selection, that ends up using `resolve_completions` in `lsp_store.rs` * applying a completion menu entry, that ends up using `apply_additional_edits_for_completion` in `lsp_store.rs` Now, all local counterparts check `enabled` field before resolving and set it to true afterwards, and reuse the same `resolve_completion_local` method for resolving the items. A logic for re-generating docs and item labels was moved out from the `resolve_completion_local` method into a separate method, as `apply_additional_edits_for_completion` does not need that, but needs the rest of the logic for resolving. During the extraction, I've noted that multiplayer clients are not getting the item labels, regenerated after the resolve — as the Zed protocol-based flow is not the exact copy of the local resolving. To improve that, `resolve_completion_remote` needs to be adjusted, but this change is omitted to avoid bloating the PR. Release Notes: - Fixed autocomplete inserting multiple imports
This commit is contained in:
parent
d71180abc2
commit
ed61abb8b8
9 changed files with 371 additions and 201 deletions
|
@ -224,6 +224,7 @@ impl CompletionsMenu {
|
|||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
@ -3830,8 +3830,11 @@ impl Editor {
|
|||
};
|
||||
|
||||
let buffer_handle = completions_menu.buffer;
|
||||
let completions = completions_menu.completions.borrow_mut();
|
||||
let completion = completions.get(mat.candidate_id)?;
|
||||
let completion = completions_menu
|
||||
.completions
|
||||
.borrow()
|
||||
.get(mat.candidate_id)?
|
||||
.clone();
|
||||
cx.stop_propagation();
|
||||
|
||||
let snippet;
|
||||
|
@ -3975,9 +3978,11 @@ impl Editor {
|
|||
}
|
||||
|
||||
let provider = self.completion_provider.as_ref()?;
|
||||
drop(completion);
|
||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
completion.clone(),
|
||||
completions_menu.completions.clone(),
|
||||
mat.candidate_id,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
|
@ -5087,7 +5092,7 @@ impl Editor {
|
|||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
#[cfg(any(feature = "test-support", test))]
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
self.context_menu
|
||||
.borrow()
|
||||
|
@ -13447,11 +13452,14 @@ pub trait CompletionProvider {
|
|||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion: Completion,
|
||||
push_to_history: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>>;
|
||||
_buffer: Model<Buffer>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completion_index: usize,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
|
@ -13610,6 +13618,7 @@ fn snippet_completions(
|
|||
Some(Completion {
|
||||
old_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
resolved: false,
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: vec![],
|
||||
|
@ -13675,19 +13684,30 @@ impl CompletionProvider for Model<Project> {
|
|||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.resolve_completions(buffer, completion_indices, completions, cx)
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion: Completion,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
push_to_history: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
lsp_store.apply_additional_edits_for_completion(
|
||||
buffer,
|
||||
completions,
|
||||
completion_index,
|
||||
push_to_history,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -8402,7 +8402,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
|||
additional edit
|
||||
"});
|
||||
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
update_test_language_settings(&mut cx, |settings| {
|
||||
|
@ -10698,10 +10697,14 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
|||
..lsp::CompletionItem::default()
|
||||
};
|
||||
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let item1 = item1.clone();
|
||||
cx.handle_request::<lsp::request::Completion, _, _>({
|
||||
let item1 = item1.clone();
|
||||
let item2 = item2.clone();
|
||||
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
|
||||
move |_, _, _| {
|
||||
let item1 = item1.clone();
|
||||
let item2 = item2.clone();
|
||||
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
@ -10728,43 +10731,41 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
|||
}
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
label: "method id()".to_string(),
|
||||
filter_text: Some("id".to_string()),
|
||||
detail: Some("Now resolved!".to_string()),
|
||||
documentation: Some(lsp::Documentation::String("Docs".to_string())),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
|
||||
new_text: ".id".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
})
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let item1 = item1.clone();
|
||||
move |_, item_to_resolve, _| {
|
||||
let item1 = item1.clone();
|
||||
async move {
|
||||
if item1 == item_to_resolve {
|
||||
Ok(lsp::CompletionItem {
|
||||
label: "method id()".to_string(),
|
||||
filter_text: Some("id".to_string()),
|
||||
detail: Some("Now resolved!".to_string()),
|
||||
documentation: Some(lsp::Documentation::String("Docs".to_string())),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 22),
|
||||
lsp::Position::new(0, 22),
|
||||
),
|
||||
new_text: ".id".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
})
|
||||
} else {
|
||||
Ok(item_to_resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
label: "invalid changed label".to_string(),
|
||||
detail: Some("Now resolved!".to_string()),
|
||||
documentation: Some(lsp::Documentation::String("Docs".to_string())),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
|
||||
new_text: ".id".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
})
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _| {
|
||||
let context_menu = editor.context_menu.borrow_mut();
|
||||
let context_menu = context_menu
|
||||
|
@ -10787,6 +10788,172 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_resolve_happens_once(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
let unresolved_item_1 = lsp::CompletionItem {
|
||||
label: "id".to_string(),
|
||||
filter_text: Some("id".to_string()),
|
||||
detail: None,
|
||||
documentation: None,
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
|
||||
new_text: ".id".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
let resolved_item_1 = lsp::CompletionItem {
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
|
||||
new_text: "!!".to_string(),
|
||||
}]),
|
||||
..unresolved_item_1.clone()
|
||||
};
|
||||
let unresolved_item_2 = lsp::CompletionItem {
|
||||
label: "other".to_string(),
|
||||
filter_text: Some("other".to_string()),
|
||||
detail: None,
|
||||
documentation: None,
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
|
||||
new_text: ".other".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
let resolved_item_2 = lsp::CompletionItem {
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
|
||||
new_text: "??".to_string(),
|
||||
}]),
|
||||
..unresolved_item_2.clone()
|
||||
};
|
||||
|
||||
let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
|
||||
let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
|
||||
let unresolved_item_1 = unresolved_item_1.clone();
|
||||
let resolved_item_1 = resolved_item_1.clone();
|
||||
let unresolved_item_2 = unresolved_item_2.clone();
|
||||
let resolved_item_2 = resolved_item_2.clone();
|
||||
let resolve_requests_1 = resolve_requests_1.clone();
|
||||
let resolve_requests_2 = resolve_requests_2.clone();
|
||||
move |unresolved_request, _| {
|
||||
let unresolved_item_1 = unresolved_item_1.clone();
|
||||
let resolved_item_1 = resolved_item_1.clone();
|
||||
let unresolved_item_2 = unresolved_item_2.clone();
|
||||
let resolved_item_2 = resolved_item_2.clone();
|
||||
let resolve_requests_1 = resolve_requests_1.clone();
|
||||
let resolve_requests_2 = resolve_requests_2.clone();
|
||||
async move {
|
||||
if unresolved_request == unresolved_item_1 {
|
||||
resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(resolved_item_1.clone())
|
||||
} else if unresolved_request == unresolved_item_2 {
|
||||
resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(resolved_item_2.clone())
|
||||
} else {
|
||||
panic!("Unexpected completion item {unresolved_request:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let unresolved_item_1 = unresolved_item_1.clone();
|
||||
let unresolved_item_2 = unresolved_item_2.clone();
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
unresolved_item_1,
|
||||
unresolved_item_2,
|
||||
])))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, _| {
|
||||
let context_menu = editor.context_menu.borrow_mut();
|
||||
let context_menu = context_menu
|
||||
.as_ref()
|
||||
.expect("Should have the context menu deployed");
|
||||
match context_menu {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
let completions = completions_menu.completions.borrow_mut();
|
||||
assert_eq!(
|
||||
completions
|
||||
.iter()
|
||||
.map(|completion| &completion.label.text)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["id", "other"]
|
||||
)
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
|
||||
}
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&ContextMenuNext, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_prev(&ContextMenuPrev, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&ContextMenuNext, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.compose_completion(&ComposeCompletion::default(), cx)
|
||||
.expect("No task returned")
|
||||
})
|
||||
.await
|
||||
.expect("Completion failed");
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
resolve_requests_1.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"Should always resolve once despite multiple selections"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_requests_2.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"Should always resolve once after multiple selections and applying the completion"
|
||||
);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"fn main() { let a = ??.other; }",
|
||||
"Should use resolved data when applying the completion"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -10950,15 +11117,10 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
|||
// Completions that have already been resolved are skipped.
|
||||
assert_eq!(
|
||||
*resolved_items.lock(),
|
||||
[
|
||||
// Selected item is always resolved even if it was resolved before.
|
||||
&items_out[items_out.len() - 1..items_out.len()],
|
||||
&items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
]
|
||||
.concat()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
items_out[items_out.len() - 16..items_out.len() - 4]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<lsp::CompletionItem>>()
|
||||
);
|
||||
resolved_items.lock().clear();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue