lsp: Fix vtsls completeFunctionCalls setting being ignored (#53285)

Some language servers (notably `vtsls` with `completeFunctionCalls`)
update `insertText/textEdit` during `completionItem/resolve` to add
snippet content like function call parentheses. Previously,
`Completion.new_text` was only set from the initial completion response
and never re-derived after resolve, so the snippet text was silently
discarded.

Re-derive `new_text` from the resolved `lsp_completion`. Only the text
content is updated -- replace/insert ranges are left as anchors from the
original response since the LSP ranges in the resolved text_edit are
stale when completions are cached across keystrokes (re: #34094).

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #53275.

Release Notes:

- Fixed the `vtsls` `completeFunctionCalls` setting being ignored
This commit is contained in:
Kunall Banerjee 2026-04-23 08:02:11 -04:00 committed by GitHub
parent 9e435c0fd6
commit 96eb05166f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -6688,9 +6688,6 @@ impl LspStore {
.into_response()
.context("resolve completion")?;
// We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve.
// Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
let mut completions = completions.borrow_mut();
let completion = &mut completions[completion_index];
if let CompletionSource::Lsp {
@ -6709,6 +6706,40 @@ impl LspStore {
);
**lsp_completion = resolved_completion;
*resolved = true;
// We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not supposed to change during resolve.
// Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
//
// We still re-derive new_text here as a workaround for the specific
// VS Code TypeScript completion resolve flow that vtsls wraps:
// https://github.com/microsoft/vscode/blob/838b48504cd9a2338e2ca9e854da9cec990c4d57/extensions/typescript-language-features/src/languageFeatures/completions.ts#L218
//
// Some servers (e.g. vtsls with completeFunctionCalls) update
// insertText/textEdit during resolve to add snippet content like
// function call parentheses.
//
// vtsls resolve flow:
// https://github.com/yioneko/vtsls/blob/fecf52324a30e72dfab1537047556076720c1a5f/packages/service/src/service/completion.ts#L228-L244
// vtsls converter (isSnippet / insertTextFormat):
// https://github.com/yioneko/vtsls/blob/28e075105d7711d635ebf8aefc971bb8e1d2fe65/packages/service/src/utils/converter.ts#L149-L200
//
// NB: We only update the text content here, NOT the replace/insert
// ranges on `Completion`. Those ranges were converted to anchors from
// the original response and stay valid across buffer edits. The LSP
// ranges in the resolved text_edit are stale when completions are
// cached across keystrokes (see #34094).
let resolved_new_text = lsp_completion
.text_edit
.as_ref()
.map(|edit| match edit {
lsp::CompletionTextEdit::Edit(e) => e.new_text.clone(),
lsp::CompletionTextEdit::InsertAndReplace(e) => e.new_text.clone(),
})
.or_else(|| lsp_completion.insert_text.clone());
if let Some(mut resolved_new_text) = resolved_new_text {
LineEnding::normalize(&mut resolved_new_text);
completion.new_text = resolved_new_text;
}
}
Ok(())
}