From f168d215cfa2372c71e231f457b62b255f00a6e2 Mon Sep 17 00:00:00 2001 From: glepnir Date: Thu, 12 Mar 2026 01:57:19 +0800 Subject: [PATCH] fix(lsp): ensure augroup before querying autocmds #38254 --- runtime/lua/vim/lsp/completion.lua | 218 ++++++++++-------- .../functional/plugin/lsp/completion_spec.lua | 37 +++ 2 files changed, 154 insertions(+), 101 deletions(-) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index e495af4240..a8fc61807c 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -798,9 +798,120 @@ local function on_completechanged(group, bufnr) }) end +local function on_complete_done() + local completed_item = api.nvim_get_vvar('completed_item') + if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then + Context:reset() + return + end + + local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer + cursor_row = cursor_row - 1 + local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem + local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer + if not completion_item or not client_id then + Context:reset() + return + end + + local bufnr = api.nvim_get_current_buf() + local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet + and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil) + + Context:reset() + + local client = lsp.get_client_by_id(client_id) + if not client then + return + end + + local position_encoding = client.offset_encoding or 'utf-16' + local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider + + local function clear_word() + if not expand_snippet then + return nil + end + + -- Remove the already inserted word. + api.nvim_buf_set_text( + bufnr, + Context.cursor[1] - 1, + Context.cursor[2] - 1, + cursor_row, + cursor_col, + { '' } + ) + end + + local function apply_snippet_and_command() + if expand_snippet then + apply_snippet(completion_item) + end + + local command = completion_item.command + if command then + client:exec_cmd(command, { bufnr = bufnr }) + end + end + + if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then + clear_word() + lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding) + apply_snippet_and_command() + elseif resolve_provider and type(completion_item) == 'table' then + local changedtick = vim.b[bufnr].changedtick + + --- @param result lsp.CompletionItem + client:request('completionItem/resolve', completion_item, function(err, result) + if changedtick ~= vim.b[bufnr].changedtick then + return + end + + clear_word() + if err then + vim.notify_once(err.message, vim.log.levels.WARN) + elseif result then + if result.additionalTextEdits then + lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding) + end + if result.command then + completion_item.command = result.command + end + end + apply_snippet_and_command() + end, bufnr) + else + clear_word() + apply_snippet_and_command() + end +end + +---@param bufnr integer +---@return integer +local function register_completedone(bufnr) + local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = false }) + if #api.nvim_get_autocmds({ buffer = bufnr, event = 'CompleteDone', group = group }) > 0 then + return group + end + + api.nvim_create_autocmd('CompleteDone', { + group = group, + buffer = bufnr, + callback = function() + local reason = api.nvim_get_vvar('event').reason ---@type string + if reason == 'accept' then + on_complete_done() + end + end, + }) + + return group +end + --- @param bufnr integer --- @param clients vim.lsp.Client[] ---- @param ctx? lsp.CompletionContext +--- @param ctx lsp.CompletionContext local function trigger(bufnr, clients, ctx) reset_timer() Context:cancel_pending() @@ -809,6 +920,10 @@ local function trigger(bufnr, clients, ctx) return end + if ctx and ctx.triggerKind == protocol.CompletionTriggerKind.Invoked then + register_completedone(bufnr) + end + local win = api.nvim_get_current_win() local cursor_row = api.nvim_win_get_cursor(win)[1] local start_time = vim.uv.hrtime() --[[@as integer]] @@ -953,95 +1068,6 @@ local function on_insert_leave() Context:reset() end -local function on_complete_done() - local completed_item = api.nvim_get_vvar('completed_item') - if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then - Context:reset() - return - end - - local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer - cursor_row = cursor_row - 1 - local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem - local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer - if not completion_item or not client_id then - Context:reset() - return - end - - local bufnr = api.nvim_get_current_buf() - local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet - and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil) - - Context:reset() - - local client = lsp.get_client_by_id(client_id) - if not client then - return - end - - local position_encoding = client.offset_encoding or 'utf-16' - local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider - - local function clear_word() - if not expand_snippet then - return nil - end - - -- Remove the already inserted word. - api.nvim_buf_set_text( - bufnr, - Context.cursor[1] - 1, - Context.cursor[2] - 1, - cursor_row, - cursor_col, - { '' } - ) - end - - local function apply_snippet_and_command() - if expand_snippet then - apply_snippet(completion_item) - end - - local command = completion_item.command - if command then - client:exec_cmd(command, { bufnr = bufnr }) - end - end - - if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then - clear_word() - lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding) - apply_snippet_and_command() - elseif resolve_provider and type(completion_item) == 'table' then - local changedtick = vim.b[bufnr].changedtick - - --- @param result lsp.CompletionItem - client:request('completionItem/resolve', completion_item, function(err, result) - if changedtick ~= vim.b[bufnr].changedtick then - return - end - - clear_word() - if err then - vim.notify_once(err.message, vim.log.levels.WARN) - elseif result then - if result.additionalTextEdits then - lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding) - end - if result.command then - completion_item.command = result.command - end - end - apply_snippet_and_command() - end, bufnr) - else - clear_word() - apply_snippet_and_command() - end -end - --- @param client_id integer --- @param bufnr integer local function disable_completions(client_id, bufnr) @@ -1090,7 +1116,7 @@ local function enable_completions(client_id, bufnr, opts) }) -- Set up autocommands. - local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = true }) + local group = register_completedone(bufnr) api.nvim_create_autocmd('LspDetach', { group = group, buffer = bufnr, @@ -1099,16 +1125,6 @@ local function enable_completions(client_id, bufnr, opts) disable_completions(args.data.client_id, args.buf) end, }) - api.nvim_create_autocmd('CompleteDone', { - group = group, - buffer = bufnr, - callback = function() - local reason = api.nvim_get_vvar('event').reason --- @type string - if reason == 'accept' then - on_complete_done() - end - end, - }) if opts.autotrigger then api.nvim_create_autocmd('InsertCharPre', { diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 787d1e7425..ac650f58e2 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -1421,6 +1421,43 @@ describe('vim.lsp.completion: integration', function() {5:-- INSERT --} | ]]) end) + + it('omnifunc works without enable() #38252', function() + local completion_list = { + isIncomplete = false, + items = { + { label = 'hello' }, + { label = 'hallo' }, + }, + } + exec_lua(function() + local server = _G._create_server({ + capabilities = { + completionProvider = { + triggerCharacters = { '.' }, + }, + }, + handlers = { + ['textDocument/completion'] = function(_, _, callback) + callback(nil, completion_list) + end, + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + local id = vim.lsp.start({ + name = 'dummy', + cmd = server.cmd, + }) + if id then + vim.lsp.buf_attach_client(bufnr, id) + vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' + end + end) + feed('ih') + wait_for_pum() + feed('') + eq('hallo', n.api.nvim_get_current_line()) + end) end) describe("vim.lsp.completion: omnifunc + 'autocomplete'", function()