fix(lsp): fix error with InsertReplaceEdit events #33973

Problem:
Some LSPs cause the following completion error (reformatted slightly):

    Error executing vim.schedule lua callback:
    .../runtime/lua/vim/lsp/completion.lua:373
    attempt to index field 'range' (a nil value)

This is because an internal function assumes edits are either missing
or of type `TextEdit`, but there's a third [possibility][0] that's not
handled: the `InsertReplaceEdit`.

This was previously reported in at least two issues:

- https://github.com/neovim/neovim/issues/33142
- https://github.com/neovim/neovim/issues/33224

Solution:
Don't assume the edit is a `TextEdit`. This implicitly handles
`InsertReplaceEdit`s.

Also, add a test case for this, which previously caused an error.

[0]: 2c07428966/runtime/lua/vim/lsp/_meta/protocol.lua (L1099)

(cherry picked from commit 927927e143)
This commit is contained in:
Evan Hahn
2025-05-22 08:22:47 -05:00
committed by github-actions[bot]
parent e304677993
commit b868257ef9
2 changed files with 42 additions and 13 deletions

View File

@@ -370,7 +370,7 @@ 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.start.line == lnum then
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
end

View File

@@ -618,24 +618,53 @@ 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 = {
abbr = ' this_thread',
dup = 1,
empty = 1,
icase = 1,
info = '',
kind = 'Module',
menu = '',
abbr_hlgroup = '',
word = 'this_thread',
{
abbr = ' this_thread',
dup = 1,
empty = 1,
icase = 1,
info = '',
kind = 'Module',
menu = '',
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)
local item = result.items[1]
item.user_data = nil
eq(expected, item)
for _, item in ipairs(result.items) do
item.user_data = nil
end
eq(expected, result.items)
end)
it('should search from start boundary to cursor position', function()