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:
Kirill Bulatov 2024-12-27 18:43:01 +02:00 committed by GitHub
parent d71180abc2
commit ed61abb8b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 371 additions and 201 deletions

View file

@ -149,6 +149,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0), server_id: LanguageServerId(0),
lsp_completion: Default::default(), lsp_completion: Default::default(),
confirm, confirm,
resolved: true,
}) })
}) })
.collect() .collect()
@ -242,6 +243,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0), server_id: LanguageServerId(0),
lsp_completion: Default::default(), lsp_completion: Default::default(),
confirm, confirm,
resolved: true,
} }
}) })
.collect()) .collect())
@ -330,16 +332,6 @@ impl CompletionProvider for SlashCommandCompletionProvider {
Task::ready(Ok(true)) Task::ready(Ok(true))
} }
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger( fn is_completion_trigger(
&self, &self,
buffer: &Model<Buffer>, buffer: &Model<Buffer>,

View file

@ -79,16 +79,6 @@ impl CompletionProvider for MessageEditorCompletionProvider {
Task::ready(Ok(false)) Task::ready(Ok(false))
} }
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>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger( fn is_completion_trigger(
&self, &self,
_buffer: &Model<Buffer>, _buffer: &Model<Buffer>,
@ -319,6 +309,7 @@ impl MessageEditor {
server_id: LanguageServerId(0), // TODO: Make this optional or something? server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something? lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None, confirm: None,
resolved: true,
} }
}) })
.collect() .collect()

View file

@ -224,6 +224,7 @@ impl CompletionsMenu {
documentation: None, documentation: None,
lsp_completion: Default::default(), lsp_completion: Default::default(),
confirm: None, confirm: None,
resolved: true,
}) })
.collect(); .collect();

View file

@ -3830,8 +3830,11 @@ impl Editor {
}; };
let buffer_handle = completions_menu.buffer; let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.borrow_mut(); let completion = completions_menu
let completion = completions.get(mat.candidate_id)?; .completions
.borrow()
.get(mat.candidate_id)?
.clone();
cx.stop_propagation(); cx.stop_propagation();
let snippet; let snippet;
@ -3975,9 +3978,11 @@ impl Editor {
} }
let provider = self.completion_provider.as_ref()?; let provider = self.completion_provider.as_ref()?;
drop(completion);
let apply_edits = provider.apply_additional_edits_for_completion( let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle, buffer_handle,
completion.clone(), completions_menu.completions.clone(),
mat.candidate_id,
true, true,
cx, cx,
); );
@ -5087,7 +5092,7 @@ impl Editor {
})) }))
} }
#[cfg(feature = "test-support")] #[cfg(any(feature = "test-support", test))]
pub fn context_menu_visible(&self) -> bool { pub fn context_menu_visible(&self) -> bool {
self.context_menu self.context_menu
.borrow() .borrow()
@ -13447,11 +13452,14 @@ pub trait CompletionProvider {
fn apply_additional_edits_for_completion( fn apply_additional_edits_for_completion(
&self, &self,
buffer: Model<Buffer>, _buffer: Model<Buffer>,
completion: Completion, _completions: Rc<RefCell<Box<[Completion]>>>,
push_to_history: bool, _completion_index: usize,
cx: &mut ViewContext<Editor>, _push_to_history: bool,
) -> Task<Result<Option<language::Transaction>>>; _cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger( fn is_completion_trigger(
&self, &self,
@ -13610,6 +13618,7 @@ fn snippet_completions(
Some(Completion { Some(Completion {
old_range: range, old_range: range,
new_text: snippet.body.clone(), new_text: snippet.body.clone(),
resolved: false,
label: CodeLabel { label: CodeLabel {
text: matching_prefix.clone(), text: matching_prefix.clone(),
runs: vec![], runs: vec![],
@ -13675,19 +13684,30 @@ impl CompletionProvider for Model<Project> {
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
self.update(cx, |project, cx| { 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( fn apply_additional_edits_for_completion(
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
completion: Completion, completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
push_to_history: bool, push_to_history: bool,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> { ) -> Task<Result<Option<language::Transaction>>> {
self.update(cx, |project, cx| { 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,
)
})
}) })
} }

View file

@ -8402,7 +8402,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit additional edit
"}); "});
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap(); apply_additional_edits.await.unwrap();
update_test_language_settings(&mut cx, |settings| { 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() ..lsp::CompletionItem::default()
}; };
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| { let item1 = item1.clone();
cx.handle_request::<lsp::request::Completion, _, _>({
let item1 = item1.clone();
move |_, _, _| {
let item1 = item1.clone(); let item1 = item1.clone();
let item2 = item2.clone(); let item2 = item2.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) } async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
}
}) })
.next() .next()
.await; .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 { 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 { Ok(lsp::CompletionItem {
label: "method id()".to_string(), label: "method id()".to_string(),
filter_text: Some("id".to_string()), filter_text: Some("id".to_string()),
detail: Some("Now resolved!".to_string()), detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())), documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), range: lsp::Range::new(
lsp::Position::new(0, 22),
lsp::Position::new(0, 22),
),
new_text: ".id".to_string(), new_text: ".id".to_string(),
})), })),
..lsp::CompletionItem::default() ..lsp::CompletionItem::default()
}) })
} else {
Ok(item_to_resolve)
}
}
}
}) })
.next() .next()
.await; .await
.unwrap();
cx.run_until_parked(); cx.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), 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, _| { cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut(); let context_menu = editor.context_menu.borrow_mut();
let context_menu = context_menu 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] #[gpui::test]
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) { async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
@ -10950,12 +11117,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
// Completions that have already been resolved are skipped. // Completions that have already been resolved are skipped.
assert_eq!( assert_eq!(
*resolved_items.lock(), *resolved_items.lock(),
[ items_out[items_out.len() - 16..items_out.len() - 4]
// 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() .iter()
.cloned() .cloned()
.collect::<Vec<lsp::CompletionItem>>() .collect::<Vec<lsp::CompletionItem>>()

View file

@ -1918,6 +1918,7 @@ impl LspCommand for GetCompletions {
new_text, new_text,
server_id, server_id,
lsp_completion, lsp_completion,
resolved: false,
} }
}) })
.collect()) .collect())

View file

@ -4152,38 +4152,27 @@ impl LspStore {
let mut did_resolve = false; let mut did_resolve = false;
if let Some((client, project_id)) = client { if let Some((client, project_id)) = client {
for completion_index in completion_indices { for completion_index in completion_indices {
let (server_id, completion) = { let server_id = completions.borrow()[completion_index].server_id;
let completions = completions.borrow_mut();
let completion = &completions[completion_index];
did_resolve = true;
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
(server_id, completion) if Self::resolve_completion_remote(
};
Self::resolve_completion_remote(
project_id, project_id,
server_id, server_id,
buffer_id, buffer_id,
completions.clone(), completions.clone(),
completion_index, completion_index,
completion,
client.clone(), client.clone(),
language_registry.clone(), language_registry.clone(),
) )
.await; .await
.log_err()
.is_some()
{
did_resolve = true;
}
} }
} else { } else {
for completion_index in completion_indices { for completion_index in completion_indices {
let (server_id, completion) = { let server_id = completions.borrow()[completion_index].server_id;
let completions = completions.borrow_mut();
let completion = &completions[completion_index];
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
(server_id, completion)
};
let server_and_adapter = this let server_and_adapter = this
.read_with(&cx, |lsp_store, _| { .read_with(&cx, |lsp_store, _| {
@ -4198,17 +4187,27 @@ impl LspStore {
continue; continue;
}; };
did_resolve = true; let resolved = Self::resolve_completion_local(
Self::resolve_completion_local(
server, server,
&buffer_snapshot,
completions.clone(),
completion_index,
)
.await
.log_err()
.is_some();
if resolved {
Self::regenerate_completion_labels(
adapter, adapter,
&buffer_snapshot, &buffer_snapshot,
completions.clone(), completions.clone(),
completion_index, completion_index,
completion,
language_registry.clone(), language_registry.clone(),
) )
.await; .await
.log_err();
did_resolve = true;
}
} }
} }
@ -4218,13 +4217,10 @@ impl LspStore {
async fn resolve_completion_local( async fn resolve_completion_local(
server: Arc<lsp::LanguageServer>, server: Arc<lsp::LanguageServer>,
adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot, snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize, completion_index: usize,
completion: lsp::CompletionItem, ) -> Result<()> {
language_registry: Arc<LanguageRegistry>,
) {
let can_resolve = server let can_resolve = server
.capabilities() .capabilities()
.completion_provider .completion_provider
@ -4232,30 +4228,17 @@ impl LspStore {
.and_then(|options| options.resolve_provider) .and_then(|options| options.resolve_provider)
.unwrap_or(false); .unwrap_or(false);
if !can_resolve { if !can_resolve {
return; return Ok(());
} }
let request = server.request::<lsp::request::ResolveCompletionItem>(completion); let request = {
let Some(completion_item) = request.await.log_err() else { let completion = &completions.borrow()[completion_index];
return; if completion.resolved {
return Ok(());
}
server.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion.clone())
}; };
let completion_item = request.await?;
if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
if let Some(text_edit) = completion_item.text_edit.as_ref() { if let Some(text_edit) = completion_item.text_edit.as_ref() {
// Technically we don't have to parse the whole `text_edit`, since the only // Technically we don't have to parse the whole `text_edit`, since the only
@ -4283,28 +4266,61 @@ impl LspStore {
} }
} }
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item;
completion.resolved = true;
Ok(())
}
async fn regenerate_completion_labels(
adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
language_registry: Arc<LanguageRegistry>,
) -> Result<()> {
let completion_item = completions.borrow()[completion_index]
.lsp_completion
.clone();
if let Some(lsp_documentation) = completion_item.documentation.as_ref() {
let documentation = language::prepare_completion_documentation(
lsp_documentation,
&language_registry,
snapshot.language().cloned(),
)
.await;
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213 // NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
// So we have to update the label here anyway... // So we have to update the label here anyway...
let new_label = match snapshot.language() { let new_label = match snapshot.language() {
Some(language) => adapter Some(language) => {
adapter
.labels_for_completions(&[completion_item.clone()], language) .labels_for_completions(&[completion_item.clone()], language)
.await .await?
.log_err() }
.unwrap_or_default(),
None => Vec::new(), None => Vec::new(),
} }
.pop() .pop()
.flatten() .flatten()
.unwrap_or_else(|| { .unwrap_or_else(|| {
CodeLabel::plain( CodeLabel::plain(
completion_item.label.clone(), completion_item.label,
completion_item.filter_text.as_deref(), completion_item.filter_text.as_deref(),
) )
}); });
let mut completions = completions.borrow_mut(); let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item;
if completion.label.filter_text() == new_label.filter_text() { if completion.label.filter_text() == new_label.filter_text() {
completion.label = new_label; completion.label = new_label;
} else { } else {
@ -4317,6 +4333,8 @@ impl LspStore {
new_label.filter_text() new_label.filter_text()
); );
} }
Ok(())
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -4326,29 +4344,30 @@ impl LspStore {
buffer_id: BufferId, buffer_id: BufferId,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize, completion_index: usize,
completion: lsp::CompletionItem,
client: AnyProtoClient, client: AnyProtoClient,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
) { ) -> Result<()> {
let lsp_completion = {
let completion = &completions.borrow()[completion_index];
if completion.resolved {
return Ok(());
}
serde_json::to_string(&completion.lsp_completion)
.unwrap()
.into_bytes()
};
let request = proto::ResolveCompletionDocumentation { let request = proto::ResolveCompletionDocumentation {
project_id, project_id,
language_server_id: server_id.0 as u64, language_server_id: server_id.0 as u64,
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), lsp_completion,
buffer_id: buffer_id.into(), buffer_id: buffer_id.into(),
}; };
let Some(response) = client let response = client
.request(request) .request(request)
.await .await
.context("completion documentation resolve proto request") .context("completion documentation resolve proto request")?;
.log_err() let lsp_completion = serde_json::from_slice(&response.lsp_completion)?;
else {
return;
};
let Some(lsp_completion) = serde_json::from_slice(&response.lsp_completion).log_err()
else {
return;
};
let documentation = if response.documentation.is_empty() { let documentation = if response.documentation.is_empty() {
Documentation::Undocumented Documentation::Undocumented
@ -4366,6 +4385,7 @@ impl LspStore {
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.documentation = Some(documentation); completion.documentation = Some(documentation);
completion.lsp_completion = lsp_completion; completion.lsp_completion = lsp_completion;
completion.resolved = true;
let old_range = response let old_range = response
.old_start .old_start
@ -4377,12 +4397,15 @@ impl LspStore {
completion.old_range = old_start..old_end; completion.old_range = old_start..old_end;
} }
} }
Ok(())
} }
pub fn apply_additional_edits_for_completion( pub fn apply_additional_edits_for_completion(
&self, &self,
buffer_handle: Model<Buffer>, buffer_handle: Model<Buffer>,
completion: Completion, completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
push_to_history: bool, push_to_history: bool,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> { ) -> Task<Result<Option<Transaction>>> {
@ -4391,8 +4414,9 @@ impl LspStore {
if let Some((client, project_id)) = self.upstream_client() { if let Some((client, project_id)) = self.upstream_client() {
cx.spawn(move |_, mut cx| async move { cx.spawn(move |_, mut cx| async move {
let response = client let request = {
.request(proto::ApplyCompletionAdditionalEdits { let completion = completions.borrow()[completion_index].clone();
proto::ApplyCompletionAdditionalEdits {
project_id, project_id,
buffer_id: buffer_id.into(), buffer_id: buffer_id.into(),
completion: Some(Self::serialize_completion(&CoreCompletion { completion: Some(Self::serialize_completion(&CoreCompletion {
@ -4400,9 +4424,13 @@ impl LspStore {
new_text: completion.new_text, new_text: completion.new_text,
server_id: completion.server_id, server_id: completion.server_id,
lsp_completion: completion.lsp_completion, lsp_completion: completion.lsp_completion,
resolved: completion.resolved,
})), })),
}) }
.await?; };
let response = client.request(request).await?;
completions.borrow_mut()[completion_index].resolved = true;
if let Some(transaction) = response.transaction { if let Some(transaction) = response.transaction {
let transaction = language::proto::deserialize_transaction(transaction)?; let transaction = language::proto::deserialize_transaction(transaction)?;
@ -4422,34 +4450,31 @@ impl LspStore {
} }
}) })
} else { } else {
let server_id = completion.server_id; let server_id = completions.borrow()[completion_index].server_id;
let lang_server = match self.language_server_for_local_buffer(buffer, server_id, cx) { let server = match self.language_server_for_local_buffer(buffer, server_id, cx) {
Some((_, server)) => server.clone(), Some((_, server)) => server.clone(),
_ => return Task::ready(Ok(Default::default())), _ => return Task::ready(Ok(None)),
}; };
let snapshot = buffer_handle.read(&cx).snapshot();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let can_resolve = lang_server Self::resolve_completion_local(
.capabilities() server.clone(),
.completion_provider &snapshot,
.as_ref() completions.clone(),
.and_then(|options| options.resolve_provider) completion_index,
.unwrap_or(false); )
let additional_text_edits = if can_resolve { .await
lang_server .context("resolving completion")?;
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion) let completion = completions.borrow()[completion_index].clone();
.await? let additional_text_edits = completion.lsp_completion.additional_text_edits;
.additional_text_edits
} else {
completion.lsp_completion.additional_text_edits
};
if let Some(edits) = additional_text_edits { if let Some(edits) = additional_text_edits {
let edits = this let edits = this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
this.as_local_mut().unwrap().edits_from_lsp( this.as_local_mut().unwrap().edits_from_lsp(
&buffer_handle, &buffer_handle,
edits, edits,
lang_server.server_id(), server.server_id(),
None, None,
cx, cx,
) )
@ -6803,7 +6828,7 @@ impl LspStore {
let apply_additional_edits = this.update(&mut cx, |this, cx| { let apply_additional_edits = this.update(&mut cx, |this, cx| {
this.apply_additional_edits_for_completion( this.apply_additional_edits_for_completion(
buffer, buffer,
Completion { Rc::new(RefCell::new(Box::new([Completion {
old_range: completion.old_range, old_range: completion.old_range,
new_text: completion.new_text, new_text: completion.new_text,
lsp_completion: completion.lsp_completion, lsp_completion: completion.lsp_completion,
@ -6815,7 +6840,9 @@ impl LspStore {
filter_range: Default::default(), filter_range: Default::default(),
}, },
confirm: None, confirm: None,
}, resolved: completion.resolved,
}]))),
0,
false, false,
cx, cx,
) )
@ -7780,6 +7807,7 @@ impl LspStore {
new_text: completion.new_text.clone(), new_text: completion.new_text.clone(),
server_id: completion.server_id.0 as u64, server_id: completion.server_id.0 as u64,
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
resolved: completion.resolved,
} }
} }
@ -7799,6 +7827,7 @@ impl LspStore {
new_text: completion.new_text, new_text: completion.new_text,
server_id: LanguageServerId(completion.server_id as usize), server_id: LanguageServerId(completion.server_id as usize),
lsp_completion, lsp_completion,
resolved: completion.resolved,
}) })
} }
@ -7900,6 +7929,7 @@ async fn populate_labels_for_completions(
documentation, documentation,
lsp_completion, lsp_completion,
confirm: None, confirm: None,
resolved: false,
}) })
} }
} }

View file

@ -73,10 +73,8 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider; use snippet_provider::SnippetProvider;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell,
ops::Range, ops::Range,
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
rc::Rc,
str, str,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
@ -353,6 +351,8 @@ pub struct Completion {
pub documentation: Option<Documentation>, pub documentation: Option<Documentation>,
/// The raw completion provided by the language server. /// The raw completion provided by the language server.
pub lsp_completion: lsp::CompletionItem, pub lsp_completion: lsp::CompletionItem,
/// Whether this completion has been resolved, to ensure it happens once per completion.
pub resolved: bool,
/// An optional callback to invoke when this completion is confirmed. /// An optional callback to invoke when this completion is confirmed.
/// Returns, whether new completions should be retriggered after the current one. /// Returns, whether new completions should be retriggered after the current one.
/// If `true` is returned, the editor will show a new completion menu after this completion is confirmed. /// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.
@ -380,6 +380,7 @@ pub(crate) struct CoreCompletion {
new_text: String, new_text: String,
server_id: LanguageServerId, server_id: LanguageServerId,
lsp_completion: lsp::CompletionItem, lsp_completion: lsp::CompletionItem,
resolved: bool,
} }
/// A code action provided by a language server. /// A code action provided by a language server.
@ -2863,35 +2864,6 @@ impl Project {
}) })
} }
pub fn resolve_completions(
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.resolve_completions(buffer, completion_indices, completions, cx)
})
}
pub fn apply_additional_edits_for_completion(
&self,
buffer_handle: Model<Buffer>,
completion: Completion,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.apply_additional_edits_for_completion(
buffer_handle,
completion,
push_to_history,
cx,
)
})
}
pub fn code_actions<T: Clone + ToOffset>( pub fn code_actions<T: Clone + ToOffset>(
&mut self, &mut self,
buffer_handle: &Model<Buffer>, buffer_handle: &Model<Buffer>,

View file

@ -927,6 +927,7 @@ message Completion {
string new_text = 3; string new_text = 3;
uint64 server_id = 4; uint64 server_id = 4;
bytes lsp_completion = 5; bytes lsp_completion = 5;
bool resolved = 6;
} }
message GetCodeActions { message GetCodeActions {