fix(lsp): requery empty isIncomplete completion lists #40100

Problem:
an empty `{ isIncomplete = true, items = {} }` ends completion
instead of requerying.

Solution:
keep isIncomplete on empty lists and retrigger on keypress while incomplete.
Reset on <C-e> so it doesn't immediately re-query.
This commit is contained in:
glepnir
2026-06-16 06:29:27 +08:00
committed by GitHub
parent 724e1421f8
commit 02b7415324
2 changed files with 79 additions and 32 deletions

View File

@@ -937,6 +937,9 @@ local function register_completedone(bufnr)
local reason = api.nvim_get_vvar('event').reason ---@type string
if reason == 'accept' then
on_complete_done()
elseif reason == 'cancel' then
-- <C-e> dismissed the pum; stop re-querying an incomplete list.
Context:reset()
end
end)
@@ -1002,23 +1005,27 @@ local function trigger(bufnr, clients, ctx)
client and client.name or 'UNKNOWN'
)
)
elseif not vim.isnil(result) and #(result.items or result) > 0 then
-- result is CompletionItem[] or CompletionList; result.items may be empty, and
-- an empty incomplete list means request again when needed.
elseif not vim.isnil(result) then
Context.isIncomplete = Context.isIncomplete or result.isIncomplete
local encoding = client and client.offset_encoding or 'utf-16'
local client_matches, tmp_server_start_boundary
client_matches, tmp_server_start_boundary = M._convert_results(
line,
cursor_row - 1,
cursor_col,
client_id,
word_boundary,
nil,
result,
encoding
)
if #(result.items or result) > 0 then
local encoding = client and client.offset_encoding or 'utf-16'
local client_matches, tmp_server_start_boundary
client_matches, tmp_server_start_boundary = M._convert_results(
line,
cursor_row - 1,
cursor_col,
client_id,
word_boundary,
nil,
result,
encoding
)
server_start_boundary = tmp_server_start_boundary or server_start_boundary
vim.list_extend(matches, client_matches)
server_start_boundary = tmp_server_start_boundary or server_start_boundary
vim.list_extend(matches, client_matches)
end
end
end
@@ -1056,31 +1063,33 @@ end
--- @param handle vim.lsp.completion.BufHandle
local function on_insert_char_pre(handle)
if vim.fn.pumvisible() ~= 0 then
if Context.isIncomplete then
reset_timer()
if Context.isIncomplete then
reset_timer()
local debounce_ms = adaptive_debounce(Context.last_request_time, rtt_ms)
local ctx = { triggerKind = protocol.CompletionTriggerKind.TriggerForIncompleteCompletions }
if debounce_ms == 0 then
vim.schedule(function()
local debounce_ms = adaptive_debounce(Context.last_request_time, rtt_ms)
local ctx = { triggerKind = protocol.CompletionTriggerKind.TriggerForIncompleteCompletions }
if debounce_ms == 0 then
vim.schedule(function()
M.get({ ctx = ctx })
end)
else
completion_timer = new_timer()
completion_timer:start(
math.floor(debounce_ms),
0,
vim.schedule_wrap(function()
M.get({ ctx = ctx })
end)
else
completion_timer = new_timer()
completion_timer:start(
math.floor(debounce_ms),
0,
vim.schedule_wrap(function()
M.get({ ctx = ctx })
end)
)
end
)
end
return
end
if vim.fn.pumvisible() ~= 0 then
return
end
local char = vim.v.char
local matched_clients = handle.triggers[char]
-- Discard pending trigger char, complete the "latest" one.

View File

@@ -1241,6 +1241,44 @@ describe('vim.lsp.completion: protocol', function()
end)
t.matches('items=null', err)
end)
it('keeps requerying while the completion list is incomplete #40096', function()
exec_lua(function()
_G.contexts = {}
local server = _G._create_server({
capabilities = {
completionProvider = { triggerCharacters = { 'h' } },
},
handlers = {
['textDocument/completion'] = function(_, params, callback)
_G.contexts[#_G.contexts + 1] = params.context
callback(nil, { isIncomplete = true, items = { { label = 'hello' } } })
end,
},
})
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_win_set_buf(0, bufnr)
vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
on_attach = function(client, bufnr0)
vim.lsp.completion.enable(true, client.id, bufnr0, { autotrigger = true })
end,
})
end)
feed('ih')
assert_matches(function(matches)
eq('hello', matches[1].word)
end)
eq({ triggerKind = 2, triggerCharacter = 'h' }, exec_lua('return _G.contexts[1]'))
exec_lua('_G.capture = {}')
feed('e')
assert_matches(function(matches)
eq('hello', matches[1].word)
end)
eq({ triggerKind = 3 }, exec_lua('return _G.contexts[2]'))
end)
end)
describe('vim.lsp.completion: integration', function()