From ff792f8e690854aaae9be627c583b596faf137c0 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Tue, 18 Nov 2025 23:03:40 -0800 Subject: [PATCH] fix(lsp): enable insertReplaceSupport for use in adjust_start_col #36569 Problem: With the typescript LSes typescript-language-server and vtsls, omnicompletion on partial tokens for certain types, such as array methods, and functions that are attached as attributes to other functions, either results in no entries populated in the completion menu (typescript-language-server), or an unfiltered completion menu with all array methods included, even if they don't share the same prefix as the partial token being completed (vtsls). Solution: Enable insertReplaceSupport and uses the insert portion of the lsp completion response in adjust_start_col if it's included in the response. Completion results are still filtered client side. --- runtime/lua/vim/lsp/completion.lua | 17 ++-- runtime/lua/vim/lsp/protocol.lua | 1 + .../functional/plugin/lsp/completion_spec.lua | 77 +++++++++++-------- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 5c4c8a4dd5..959422eecb 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -301,7 +301,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) return match_item_by_value(item.filterText, prefix) end - if item.textEdit then + if item.textEdit and not item.textEdit.newText then -- server took care of filtering return true end @@ -370,11 +370,18 @@ end local function adjust_start_col(lnum, line, items, encoding) local min_start_char = nil for _, item in pairs(items) do - if item.textEdit and item.textEdit.range and item.textEdit.range.start.line == lnum then - if min_start_char and min_start_char ~= item.textEdit.range.start.character then - return nil + if item.textEdit then + if item.textEdit.range and item.textEdit.range.start.line == lnum then + if min_start_char and min_start_char ~= item.textEdit.range.start.character then + return nil + end + min_start_char = item.textEdit.range.start.character + elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then + if min_start_char and min_start_char ~= item.textEdit.insert.start.character then + return nil + end + min_start_char = item.textEdit.insert.start.character end - min_start_char = item.textEdit.range.start.character end end if min_start_char then diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 509c203ef7..f367c0a7c3 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -478,6 +478,7 @@ function protocol.make_client_capabilities() preselectSupport = false, deprecatedSupport = true, documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText }, + insertReplaceSupport = true, resolveSupport = { properties = { 'additionalTextEdits', diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 22536b2606..b4decc90bc 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -149,10 +149,6 @@ describe('vim.lsp.completion: item conversion', function() abbr = 'foo', word = 'foo', }, - { - abbr = 'bar', - word = 'bar', - }, } result = vim.tbl_map(function(x) return { @@ -618,21 +614,6 @@ describe('vim.lsp.completion: item conversion', function() }, }, }, - { - label = 'insert_replace_edit', - kind = 9, - textEdit = { - newText = 'foobar', - insert = { - start = { line = 0, character = 7 }, - ['end'] = { line = 0, character = 11 }, - }, - replace = { - start = { line = 0, character = 0 }, - ['end'] = { line = 0, character = 0 }, - }, - }, - }, }, } local expected = { @@ -647,17 +628,6 @@ describe('vim.lsp.completion: item conversion', function() abbr_hlgroup = '', word = 'this_thread', }, - { - abbr = 'insert_replace_edit', - dup = 1, - empty = 1, - icase = 1, - info = '', - kind = 'Module', - menu = '', - abbr_hlgroup = '', - word = 'foobar', - }, } local result = complete(' std::this|', completion_list) eq(7, result.server_start_boundary) @@ -806,6 +776,53 @@ describe('vim.lsp.completion: item conversion', function() eq('hello', text) end ) + + it('uses the start boundary from an insertReplace response', function() + local completion_list = { + isIncomplete = false, + items = { + { + data = { cacheId = 1 }, + kind = 2, + label = 'foobar', + sortText = '11', + textEdit = { + insert = { + start = { character = 4, line = 4 }, + ['end'] = { character = 8, line = 4 }, + }, + newText = 'foobar', + replace = { + start = { character = 4, line = 4 }, + ['end'] = { character = 8, line = 4 }, + }, + }, + }, + { + data = { cacheId = 2 }, + kind = 2, + label = 'bazqux', + sortText = '11', + textEdit = { + insert = { + start = { character = 4, line = 4 }, + ['end'] = { character = 5, line = 4 }, + }, + newText = 'bazqux', + replace = { + start = { character = 4, line = 4 }, + ['end'] = { character = 5, line = 4 }, + }, + }, + }, + }, + } + + local result = complete('foo.f|', completion_list) + eq(1, #result.items) + local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText + eq('foobar', text) + end) end) --- @param name string