From d1b811b6c2d93fe13442d61331a9276fea417f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Robert?= Date: Mon, 24 Aug 2026 16:51:39 +0200 Subject: [PATCH] fix(lsp): advance inline completion range over matching input #41434 Problem: `Completor:accept()` applies the item against the range the server answered with. Characters typed after the request but before accepting lie inside neither that range nor the replacement, so they are left behind by the insertion: typing `a` after `foob` and accepting `foobar` within the 200 ms debounce yields `foobara`. Solution: Grow the item's range over the characters typed since the request, so that `accept()` replaces them. Only while the candidate still matches, decided by the longest common prefix `show()` already computes, so input that contradicts it is left alone. Snippet items are unaffected, since `accept()` ignores the range for them. --- runtime/lua/vim/lsp/inline_completion.lua | 13 ++++++- .../plugin/lsp/inline_completion_spec.lua | 37 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/runtime/lua/vim/lsp/inline_completion.lua b/runtime/lua/vim/lsp/inline_completion.lua index bd7b771d52..685dbbdc36 100644 --- a/runtime/lua/vim/lsp/inline_completion.lua +++ b/runtime/lua/vim/lsp/inline_completion.lua @@ -63,6 +63,7 @@ local namespace = api.nvim_create_namespace('nvim.lsp.inline_completion') ---@field active table ---@field timer? uv.uv_timer_t Timer for debouncing automatic requests ---@field current? vim.lsp.inline_completion.Item Currently selected item +---@field _overlay? boolean Whether `current` replaces buffer text, per its original range ---@field client_state table local Completor = { name = 'inline_completion', @@ -184,6 +185,8 @@ function Completor:select(index, show_index) _filter_text = item.filterText, command = item.command, } + -- Recorded here because `show()` may grow the range over typed characters. + self._overlay = range ~= nil and not range:is_empty() local hint = show_index and (' (%d/%d)'):format(index, self:count_items()) or nil self:show(hint) @@ -240,6 +243,14 @@ function Completor:show(hint) local cursor_row, cursor_col = vim.pos.cursor(self.bufnr, api.nvim_win_get_cursor(winid)):to_extmark() if row == cursor_row then + -- Characters typed since the request lie past the item's range, so `accept()` + -- would leave them behind. `skip` stops where the buffer stops matching. + if current.range and col + skip - 1 >= cursor_col then + local start_row, start_col, _, end_col = current.range:to_extmark() + if end_col < cursor_col then + current.range = vim.range.extmark(self.bufnr, start_row, start_col, row, cursor_col) + end + end skip = math.max(skip, cursor_col - col + 1) end end @@ -256,7 +267,7 @@ function Completor:show(hint) api.nvim_buf_set_extmark(self.bufnr, namespace, row, col, { virt_text = virt_text, virt_lines = virt_lines, - virt_text_pos = (current.range and not current.range:is_empty() and 'overlay') or 'inline', + virt_text_pos = (self._overlay and 'overlay') or 'inline', hl_mode = 'combine', }) end diff --git a/test/functional/plugin/lsp/inline_completion_spec.lua b/test/functional/plugin/lsp/inline_completion_spec.lua index 5383480437..c137f249e6 100644 --- a/test/functional/plugin/lsp/inline_completion_spec.lua +++ b/test/functional/plugin/lsp/inline_completion_spec.lua @@ -84,6 +84,11 @@ describe('vim.lsp.inline_completion', function() }, handlers = { ['textDocument/inlineCompletion'] = function(_, _, callback) + if _G.items then + callback(nil, { items = _G.items }) + return + end + if _G.empty then callback(nil, { items = { @@ -293,6 +298,38 @@ describe('vim.lsp.inline_completion', function() feed('') screen:expect({ grid = grid_applied_candidates }) end) + + it('does not leave behind text typed since the request', function() + exec_lua(function() + _G.items = { + { + insertText = 'foobar', + range = { + start = { line = 1, character = 0 }, + ['end'] = { line = 1, character = 1 }, + }, + }, + } + end) + feed('ifo') + screen:expect([[ + function fibonacci() | + fo{1:^obar} | + {1:~ }|*11 + {3:-- INSERT --} | + ]]) + exec_lua(function() + vim.lsp.inline_completion.get() + end) + n.poke_eventloop() + feed('') + screen:expect([[ + function fibonacci() | + fooba^r | + {1:~ }|*11 + | + ]]) + end) end) describe('select()', function()