lsp: Update completion new_text from resolved completion item

Kunall Banerjee created

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 (#34094).

Closes #53275

Change summary

crates/project/src/lsp_store.rs | 30 +++++++++++++++++++++++++++---
1 file changed, 27 insertions(+), 3 deletions(-)

Detailed changes

crates/project/src/lsp_store.rs 🔗

@@ -6568,9 +6568,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 {
@@ -6589,6 +6586,33 @@ impl LspStore {
             );
             **lsp_completion = resolved_completion;
             *resolved = true;
+
+            // Re-derive new_text from the resolved completion. 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(())
     }