neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit ff792f8e690854aaae9be627c583b596faf137c0
parent b65aadc03ee7914081ca4055dc72778fd4d68e7b
Author: Jeff Martin <jeffmartin@gmail.com>
Date:   Tue, 18 Nov 2025 23:03:40 -0800

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.
Diffstat:
Mruntime/lua/vim/lsp/completion.lua | 17++++++++++++-----
Mruntime/lua/vim/lsp/protocol.lua | 1+
Mtest/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 @@ -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 @@ -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 @@ -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