mirror of
https://github.com/neovim/neovim.git
synced 2025-11-06 02:34:28 +00:00
fix(lsp): deduplicate completion items #36166
The current implementation has a race condition where items are appended to the completion list twice when a second completion runs while the first is still going. This hotfix just deduplicates the entire list. Co-authored-by: Tomasz N <przepompownia@users.noreply.github.com>
This commit is contained in:
@@ -482,10 +482,7 @@ local function trigger(bufnr, clients, ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local win = api.nvim_get_current_win()
|
local win = api.nvim_get_current_win()
|
||||||
local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer
|
local cursor_row = api.nvim_win_get_cursor(win)[1]
|
||||||
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 start_time = vim.uv.hrtime() --[[@as integer]]
|
local start_time = vim.uv.hrtime() --[[@as integer]]
|
||||||
Context.last_request_time = start_time
|
Context.last_request_time = start_time
|
||||||
|
|
||||||
@@ -496,13 +493,19 @@ local function trigger(bufnr, clients, ctx)
|
|||||||
Context.pending_requests = {}
|
Context.pending_requests = {}
|
||||||
Context.isIncomplete = false
|
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
|
local mode = api.nvim_get_mode().mode
|
||||||
if row_changed or not (mode == 'i' or mode == 'ic') then
|
if row_changed or not (mode == 'i' or mode == 'ic') then
|
||||||
return
|
return
|
||||||
end
|
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?
|
local server_start_boundary --- @type integer?
|
||||||
for client_id, response in pairs(responses) do
|
for client_id, response in pairs(responses) do
|
||||||
local client = lsp.get_client_by_id(client_id)
|
local client = lsp.get_client_by_id(client_id)
|
||||||
@@ -530,9 +533,25 @@ local function trigger(bufnr, clients, ctx)
|
|||||||
result,
|
result,
|
||||||
encoding
|
encoding
|
||||||
)
|
)
|
||||||
|
|
||||||
vim.list_extend(matches, client_matches)
|
vim.list_extend(matches, client_matches)
|
||||||
end
|
end
|
||||||
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
|
local start_col = (server_start_boundary or word_boundary) + 1
|
||||||
Context.cursor = { cursor_row, start_col }
|
Context.cursor = { cursor_row, start_col }
|
||||||
vim.fn.complete(start_col, matches)
|
vim.fn.complete(start_col, matches)
|
||||||
|
|||||||
@@ -810,7 +810,7 @@ end)
|
|||||||
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param completion_result lsp.CompletionList
|
--- @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
|
--- @return integer
|
||||||
local function create_server(name, completion_result, opts)
|
local function create_server(name, completion_result, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@@ -824,7 +824,14 @@ local function create_server(name, completion_result, opts)
|
|||||||
},
|
},
|
||||||
handlers = {
|
handlers = {
|
||||||
['textDocument/completion'] = function(_, _, callback)
|
['textDocument/completion'] = function(_, _, callback)
|
||||||
|
if opts.delay then
|
||||||
|
-- simulate delay in completion request, needed for some of these tests
|
||||||
|
vim.defer_fn(function()
|
||||||
callback(nil, completion_result)
|
callback(nil, completion_result)
|
||||||
|
end, opts.delay)
|
||||||
|
else
|
||||||
|
callback(nil, completion_result)
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
['completionItem/resolve'] = function(_, _, callback)
|
['completionItem/resolve'] = function(_, _, callback)
|
||||||
callback(nil, opts.resolve_result)
|
callback(nil, opts.resolve_result)
|
||||||
@@ -1343,3 +1350,49 @@ describe('vim.lsp.completion: integration', function()
|
|||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
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('ihillo<cr><esc>ih')
|
||||||
|
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('ihillo<cr>h')
|
||||||
|
vim.uv.sleep(1)
|
||||||
|
feed('e')
|
||||||
|
|
||||||
|
assert_matches({ 'hello' })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user