diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 729cada68a..0913179af8 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -482,10 +482,7 @@ local function trigger(bufnr, clients, ctx) end local win = api.nvim_get_current_win() - local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer - local line = api.nvim_get_current_line() - local line_to_cursor = line:sub(1, cursor_col) - local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') + local cursor_row = api.nvim_win_get_cursor(win)[1] local start_time = vim.uv.hrtime() --[[@as integer]] Context.last_request_time = start_time @@ -496,13 +493,19 @@ local function trigger(bufnr, clients, ctx) Context.pending_requests = {} Context.isIncomplete = false - local row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row + local new_cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer + local row_changed = new_cursor_row ~= cursor_row local mode = api.nvim_get_mode().mode if row_changed or not (mode == 'i' or mode == 'ic') then return end - local matches --[[@type table]] = vim.fn.complete_info({ 'items' })['items'] + local line = api.nvim_get_current_line() + local line_to_cursor = line:sub(1, cursor_col) + local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') + + local matches = {} + local server_start_boundary --- @type integer? for client_id, response in pairs(responses) do local client = lsp.get_client_by_id(client_id) @@ -530,9 +533,25 @@ local function trigger(bufnr, clients, ctx) result, encoding ) + vim.list_extend(matches, client_matches) end end + + --- @type table[] + local prev_matches = vim.fn.complete_info({ 'items', 'matches' })['items'] + + --- @param prev_match table + prev_matches = vim.tbl_filter(function(prev_match) + local client_id = vim.tbl_get(prev_match, 'user_data', 'nvim', 'lsp', 'client_id') + if client_id and responses[client_id] ~= nil then + return false + end + return vim.tbl_get(prev_match, 'match') + end, prev_matches) + + matches = vim.list_extend(prev_matches, matches) + local start_col = (server_start_boundary or word_boundary) + 1 Context.cursor = { cursor_row, start_col } vim.fn.complete(start_col, matches) diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index bb06e40500..dfdfb2533e 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -810,7 +810,7 @@ end) --- @param name string --- @param completion_result lsp.CompletionList ---- @param opts? {trigger_chars?: string[], resolve_result?: lsp.CompletionItem} +--- @param opts? {trigger_chars?: string[], resolve_result?: lsp.CompletionItem, delay?: integer} --- @return integer local function create_server(name, completion_result, opts) opts = opts or {} @@ -824,7 +824,14 @@ local function create_server(name, completion_result, opts) }, handlers = { ['textDocument/completion'] = function(_, _, callback) - callback(nil, completion_result) + if opts.delay then + -- simulate delay in completion request, needed for some of these tests + vim.defer_fn(function() + callback(nil, completion_result) + end, opts.delay) + else + callback(nil, completion_result) + end end, ['completionItem/resolve'] = function(_, _, callback) callback(nil, opts.resolve_result) @@ -1343,3 +1350,49 @@ describe('vim.lsp.completion: integration', function() ) end) end) + +describe("vim.lsp.completion: omnifunc + 'autocomplete'", function() + before_each(function() + clear() + exec_lua(create_server_definition) + exec_lua(function() + -- enable buffer and omnifunc autocompletion + -- omnifunc will be the lsp omnifunc + vim.o.complete = '.,o' + vim.o.autocomplete = true + end) + + local completion_list = { + isIncomplete = false, + items = { + { label = 'hello' }, + { label = 'hallo' }, + }, + } + create_server('dummy', completion_list, { delay = 50 }) + end) + + local function assert_matches(expected) + retry(nil, nil, function() + local matches = vim.tbl_map(function(m) + return m.word + end, exec_lua('return vim.fn.complete_info({ "items" })').items) + eq(expected, matches) + end) + end + + it('merges with other completions', function() + feed('ihilloih') + assert_matches({ 'hillo', 'hallo', 'hello' }) + end) + + it('fuzzy matches without duplication', function() + -- wait for one completion request to start and then request another before + -- the first one finishes, then wait for both to finish + feed('ihilloh') + vim.uv.sleep(1) + feed('e') + + assert_matches({ 'hello' }) + end) +end)