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.
This commit is contained in:
Jeff Martin
2025-11-18 23:03:40 -08:00
committed by GitHub
parent b65aadc03e
commit ff792f8e69
3 changed files with 60 additions and 35 deletions

View File

@@ -301,7 +301,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
return match_item_by_value(item.filterText, prefix) return match_item_by_value(item.filterText, prefix)
end end
if item.textEdit then if item.textEdit and not item.textEdit.newText then
-- server took care of filtering -- server took care of filtering
return true return true
end end
@@ -370,11 +370,18 @@ end
local function adjust_start_col(lnum, line, items, encoding) local function adjust_start_col(lnum, line, items, encoding)
local min_start_char = nil local min_start_char = nil
for _, item in pairs(items) do for _, item in pairs(items) do
if item.textEdit and item.textEdit.range and item.textEdit.range.start.line == lnum then if item.textEdit then
if min_start_char and min_start_char ~= item.textEdit.range.start.character then if item.textEdit.range and item.textEdit.range.start.line == lnum then
return nil 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 end
min_start_char = item.textEdit.range.start.character
end end
end end
if min_start_char then if min_start_char then

View File

@@ -478,6 +478,7 @@ function protocol.make_client_capabilities()
preselectSupport = false, preselectSupport = false,
deprecatedSupport = true, deprecatedSupport = true,
documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText }, documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText },
insertReplaceSupport = true,
resolveSupport = { resolveSupport = {
properties = { properties = {
'additionalTextEdits', 'additionalTextEdits',

View File

@@ -149,10 +149,6 @@ describe('vim.lsp.completion: item conversion', function()
abbr = 'foo', abbr = 'foo',
word = 'foo', word = 'foo',
}, },
{
abbr = 'bar',
word = 'bar',
},
} }
result = vim.tbl_map(function(x) result = vim.tbl_map(function(x)
return { 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 = { local expected = {
@@ -647,17 +628,6 @@ describe('vim.lsp.completion: item conversion', function()
abbr_hlgroup = '', abbr_hlgroup = '',
word = 'this_thread', 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) local result = complete(' std::this|', completion_list)
eq(7, result.server_start_boundary) eq(7, result.server_start_boundary)
@@ -806,6 +776,53 @@ describe('vim.lsp.completion: item conversion', function()
eq('hello', text) eq('hello', text)
end 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) end)
--- @param name string --- @param name string