commit 927927e143bd365264d7a4ef59ad1d2425006cba
parent 27bb814cb1b70b4e041eae22665bc93350f4fe56
Author: Evan Hahn <me@evanhahn.com>
Date: Thu, 22 May 2025 08:22:47 -0500
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]: https://github.com/neovim/neovim/blob/2c07428966a74c76003e00e2a37bf98eb8802c93/runtime/lua/vim/lsp/_meta/protocol.lua#L1099
Diffstat:
2 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua
@@ -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
diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua
@@ -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()