mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +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
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
          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('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