neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

completion.lua (32675B)


      1 --- @brief
      2 --- The `vim.lsp.completion` module enables insert-mode completion driven by an LSP server. Call
      3 --- `enable()` to make it available through Nvim builtin completion (via the |CompleteDone| event).
      4 --- Specify `autotrigger=true` to activate "auto-completion" when you type any of the server-defined
      5 --- `triggerCharacters`. Use CTRL-Y to select an item from the completion menu. |complete_CTRL-Y|
      6 ---
      7 --- Example: activate LSP-driven auto-completion:
      8 --- ```lua
      9 --- -- Works best with completeopt=noselect.
     10 --- -- Use CTRL-Y to select an item. |complete_CTRL-Y|
     11 --- vim.cmd[[set completeopt+=menuone,noselect,popup]]
     12 --- vim.lsp.start({
     13 ---   name = 'ts_ls',
     14 ---   cmd = …,
     15 ---   on_attach = function(client, bufnr)
     16 ---     vim.lsp.completion.enable(true, client.id, bufnr, {
     17 ---       autotrigger = true,
     18 ---       convert = function(item)
     19 ---         return { abbr = item.label:gsub('%b()', '') }
     20 ---       end,
     21 ---     })
     22 ---   end,
     23 --- })
     24 --- ```
     25 ---
     26 --- [lsp-autocompletion]()
     27 ---
     28 --- The LSP `triggerCharacters` field decides when to trigger autocompletion. If you want to trigger
     29 --- on EVERY keypress you can either:
     30 --- - Extend `client.server_capabilities.completionProvider.triggerCharacters` on `LspAttach`,
     31 ---   before you call `vim.lsp.completion.enable(… {autotrigger=true})`. See the |lsp-attach| example.
     32 --- - Call `vim.lsp.completion.get()` from an |InsertCharPre| autocommand.
     33 
     34 local M = {}
     35 
     36 local api = vim.api
     37 local lsp = vim.lsp
     38 local protocol = lsp.protocol
     39 
     40 local rtt_ms = 50.0
     41 local ns_to_ms = 0.000001
     42 
     43 --- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
     44 
     45 -- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
     46 -- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
     47 --- @nodoc
     48 --- @class lsp.ItemDefaults
     49 --- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
     50 --- @field insertTextFormat lsp.InsertTextFormat?
     51 --- @field insertTextMode lsp.InsertTextMode?
     52 --- @field data any
     53 
     54 --- @nodoc
     55 --- @class vim.lsp.completion.BufHandle
     56 --- @field clients table<integer, vim.lsp.Client>
     57 --- @field triggers table<string, vim.lsp.Client[]>
     58 --- @field convert? fun(item: lsp.CompletionItem): table
     59 
     60 --- @type table<integer, vim.lsp.completion.BufHandle>
     61 local buf_handles = {}
     62 
     63 --- @nodoc
     64 --- @class vim.lsp.completion.Context
     65 local Context = {
     66  cursor = nil, --- @type [integer, integer]?
     67  last_request_time = nil, --- @type integer?
     68  pending_requests = {}, --- @type function[]
     69  isIncomplete = false,
     70 }
     71 
     72 --- @nodoc
     73 function Context:cancel_pending()
     74  for _, cancel in ipairs(self.pending_requests) do
     75    cancel()
     76  end
     77 
     78  self.pending_requests = {}
     79 end
     80 
     81 --- @nodoc
     82 function Context:reset()
     83  -- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event.
     84  self.isIncomplete = false
     85  self.last_request_time = nil
     86  self:cancel_pending()
     87 end
     88 
     89 --- @type uv.uv_timer_t?
     90 local completion_timer = nil
     91 
     92 --- @return uv.uv_timer_t
     93 local function new_timer()
     94  return (assert(vim.uv.new_timer()))
     95 end
     96 
     97 local function reset_timer()
     98  if completion_timer then
     99    completion_timer:stop()
    100    completion_timer:close()
    101  end
    102 
    103  completion_timer = nil
    104 end
    105 
    106 --- @param window integer
    107 --- @param warmup integer
    108 --- @return fun(sample: number): number
    109 local function exp_avg(window, warmup)
    110  local count = 0
    111  local sum = 0
    112  local value = 0.0
    113 
    114  return function(sample)
    115    if count < warmup then
    116      count = count + 1
    117      sum = sum + sample
    118      value = sum / count
    119    else
    120      local factor = 2.0 / (window + 1)
    121      value = value * (1 - factor) + sample * factor
    122    end
    123    return value
    124  end
    125 end
    126 local compute_new_average = exp_avg(10, 10)
    127 
    128 --- @return number
    129 local function next_debounce()
    130  if not Context.last_request_time then
    131    return rtt_ms
    132  end
    133 
    134  local ms_since_request = (vim.uv.hrtime() - Context.last_request_time) * ns_to_ms
    135  return math.max((ms_since_request - rtt_ms) * -1, 0)
    136 end
    137 
    138 --- @param input string Unparsed snippet
    139 --- @return string # Parsed snippet if successful, else returns its input
    140 local function parse_snippet(input)
    141  local ok, parsed = pcall(function()
    142    return lsp._snippet_grammar.parse(input)
    143  end)
    144  return ok and tostring(parsed) or input
    145 end
    146 
    147 --- @param item lsp.CompletionItem
    148 local function apply_snippet(item)
    149  if item.textEdit then
    150    vim.snippet.expand(item.textEdit.newText)
    151  elseif item.insertText then
    152    vim.snippet.expand(item.insertText)
    153  end
    154 end
    155 
    156 --- Returns text that should be inserted when a selecting completion item. The
    157 --- precedence is as follows: textEdit.newText > insertText > label
    158 ---
    159 --- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
    160 ---
    161 --- @param item lsp.CompletionItem
    162 --- @param prefix string
    163 --- @param match fun(text: string, prefix: string):boolean
    164 --- @return string
    165 local function get_completion_word(item, prefix, match)
    166  if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
    167    if item.textEdit or (item.insertText and item.insertText ~= '') then
    168      -- Use label instead of text if text has different starting characters.
    169      -- label is used as abbr (=displayed), but word is used for filtering
    170      -- This is required for things like postfix completion.
    171      -- E.g. in lua:
    172      --
    173      --    local f = {}
    174      --    f@|
    175      --      ▲
    176      --      └─ cursor
    177      --
    178      --    item.textEdit.newText: table.insert(f, $0)
    179      --    label: insert
    180      --
    181      -- Typing `i` would remove the candidate because newText starts with `t`.
    182      local text = parse_snippet(item.insertText or item.textEdit.newText)
    183      local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
    184      if item.filterText and not match(word, prefix) then
    185        return item.filterText
    186      else
    187        return word
    188      end
    189    else
    190      return item.label
    191    end
    192  elseif item.textEdit then
    193    local word = item.textEdit.newText
    194    word = string.gsub(word, '\r\n?', '\n')
    195    return word:match('([^\n]*)') or word
    196  elseif item.insertText and item.insertText ~= '' then
    197    return item.insertText
    198  end
    199  return item.label
    200 end
    201 
    202 --- Applies the given defaults to the completion item, modifying it in place.
    203 ---
    204 --- @param item lsp.CompletionItem
    205 --- @param defaults lsp.ItemDefaults?
    206 local function apply_defaults(item, defaults)
    207  if not defaults then
    208    return
    209  end
    210 
    211  item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
    212  item.insertTextMode = item.insertTextMode or defaults.insertTextMode
    213  item.data = item.data or defaults.data
    214  if defaults.editRange then
    215    local textEdit = item.textEdit or {}
    216    item.textEdit = textEdit
    217    textEdit.newText = textEdit.newText or item.textEditText or item.insertText or item.label
    218    if defaults.editRange.start then
    219      textEdit.range = textEdit.range or defaults.editRange
    220    elseif defaults.editRange.insert then
    221      textEdit.insert = defaults.editRange.insert
    222      textEdit.replace = defaults.editRange.replace
    223    end
    224  end
    225 end
    226 
    227 --- @param result vim.lsp.CompletionResult
    228 --- @return lsp.CompletionItem[]
    229 local function get_items(result)
    230  if result.items then
    231    -- When we have a list, apply the defaults and return an array of items.
    232    for _, item in ipairs(result.items) do
    233      ---@diagnostic disable-next-line: param-type-mismatch
    234      apply_defaults(item, result.itemDefaults)
    235    end
    236    return result.items
    237  else
    238    -- Else just return the items as they are.
    239    return result
    240  end
    241 end
    242 
    243 ---@param item lsp.CompletionItem
    244 ---@return string
    245 local function get_doc(item)
    246  local doc = item.documentation
    247  if not doc then
    248    return ''
    249  end
    250  if type(doc) == 'string' then
    251    return doc
    252  end
    253  if type(doc) == 'table' and type(doc.value) == 'string' then
    254    return doc.value
    255  end
    256 
    257  vim.notify('invalid documentation value: ' .. vim.inspect(doc), vim.log.levels.WARN)
    258  return ''
    259 end
    260 
    261 ---@param value string
    262 ---@param prefix string
    263 ---@return boolean
    264 ---@return integer?
    265 local function match_item_by_value(value, prefix)
    266  if prefix == '' then
    267    return true, nil
    268  end
    269  if vim.o.completeopt:find('fuzzy') ~= nil then
    270    local score = vim.fn.matchfuzzypos({ value }, prefix)[3] ---@type table
    271    return #score > 0, score[1]
    272  end
    273 
    274  if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
    275    return vim.startswith(value:lower(), prefix:lower()), nil
    276  end
    277  return vim.startswith(value, prefix), nil
    278 end
    279 
    280 --- Generate kind text for completion color items
    281 --- Parse color from doc and return colored symbol ■
    282 ---
    283 ---@param item table completion item with kind and documentation
    284 ---@return string? kind text or "■" for colors
    285 ---@return string? highlight group for colors
    286 local function generate_kind(item)
    287  if not lsp.protocol.CompletionItemKind[item.kind] then
    288    return 'Unknown'
    289  end
    290  if item.kind ~= lsp.protocol.CompletionItemKind.Color then
    291    return lsp.protocol.CompletionItemKind[item.kind]
    292  end
    293  local doc = get_doc(item)
    294  if #doc == 0 then
    295    return
    296  end
    297 
    298  -- extract hex from RGB format
    299  local r, g, b = doc:match('rgb%((%d+)%s*,?%s*(%d+)%s*,?%s*(%d+)%)')
    300  local hex = r and string.format('%02x%02x%02x', tonumber(r), tonumber(g), tonumber(b))
    301    or doc:match('#?([%da-fA-F]+)')
    302 
    303  if not hex then
    304    return
    305  end
    306 
    307  -- expand 3-digit hex to 6-digit
    308  if #hex == 3 then
    309    hex = hex:gsub('.', '%1%1')
    310  end
    311 
    312  if #hex ~= 6 then
    313    return
    314  end
    315 
    316  hex = hex:lower()
    317  local group = ('@lsp.color.%s'):format(hex)
    318  if #api.nvim_get_hl(0, { name = group }) == 0 then
    319    api.nvim_set_hl(0, group, { fg = '#' .. hex })
    320  end
    321 
    322  return '■', group
    323 end
    324 
    325 --- Turns the result of a `textDocument/completion` request into vim-compatible
    326 --- |complete-items|.
    327 ---
    328 --- @param result vim.lsp.CompletionResult Result of `textDocument/completion`
    329 --- @param prefix string prefix to filter the completion items
    330 --- @param client_id integer? Client ID
    331 --- @param server_start_boundary integer? server start boundary
    332 --- @param line string? current line content
    333 --- @param lnum integer? 0-indexed line number
    334 --- @param encoding string? encoding
    335 --- @return table[]
    336 --- @see complete-items
    337 function M._lsp_to_complete_items(
    338  result,
    339  prefix,
    340  client_id,
    341  server_start_boundary,
    342  line,
    343  lnum,
    344  encoding
    345 )
    346  local items = get_items(result)
    347  if vim.tbl_isempty(items) then
    348    return {}
    349  end
    350 
    351  ---@type fun(item: lsp.CompletionItem):boolean
    352  local matches
    353  if not prefix:find('%w') then
    354    matches = function(_)
    355      return true
    356    end
    357  else
    358    ---@param item lsp.CompletionItem
    359    matches = function(item)
    360      if item.filterText then
    361        return match_item_by_value(item.filterText, prefix)
    362      end
    363 
    364      if item.textEdit and not item.textEdit.newText then
    365        -- server took care of filtering
    366        return true
    367      end
    368 
    369      return match_item_by_value(item.label, prefix)
    370    end
    371  end
    372 
    373  local candidates = {}
    374  local bufnr = api.nvim_get_current_buf()
    375  local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
    376  local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
    377  for _, item in ipairs(items) do
    378    local match, score = matches(item)
    379    if match then
    380      local word = get_completion_word(item, prefix, match_item_by_value)
    381 
    382      if server_start_boundary and line and lnum and encoding and item.textEdit then
    383        --- @type integer?
    384        local item_start_char
    385        if item.textEdit.range and item.textEdit.range.start.line == lnum then
    386          item_start_char = item.textEdit.range.start.character
    387        elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
    388          item_start_char = item.textEdit.insert.start.character
    389        end
    390 
    391        if item_start_char then
    392          local item_start_byte = vim.str_byteindex(line, encoding, item_start_char, false)
    393          if item_start_byte > server_start_boundary then
    394            local missing_prefix = line:sub(server_start_boundary + 1, item_start_byte)
    395            word = missing_prefix .. word
    396          end
    397        end
    398      end
    399 
    400      local hl_group = ''
    401      if
    402        item.deprecated
    403        or vim.list_contains((item.tags or {}), protocol.CompletionTag.Deprecated)
    404      then
    405        hl_group = 'DiagnosticDeprecated'
    406      end
    407      local kind, kind_hlgroup = generate_kind(item)
    408      local completion_item = {
    409        word = word,
    410        abbr = item.label,
    411        kind = kind,
    412        menu = item.detail or '',
    413        info = get_doc(item),
    414        icase = 1,
    415        dup = 1,
    416        empty = 1,
    417        abbr_hlgroup = hl_group,
    418        kind_hlgroup = kind_hlgroup,
    419        user_data = {
    420          nvim = {
    421            lsp = {
    422              completion_item = item,
    423              client_id = client_id,
    424            },
    425          },
    426        },
    427        _fuzzy_score = score,
    428      }
    429      if user_convert then
    430        completion_item = vim.tbl_extend('keep', user_convert(item), completion_item)
    431      end
    432      table.insert(candidates, completion_item)
    433    end
    434  end
    435 
    436  if not user_cmp then
    437    local compare_by_sortText_and_label = function(a, b)
    438      ---@type lsp.CompletionItem
    439      local itema = a.user_data.nvim.lsp.completion_item
    440      ---@type lsp.CompletionItem
    441      local itemb = b.user_data.nvim.lsp.completion_item
    442      return (itema.sortText or itema.label) < (itemb.sortText or itemb.label)
    443    end
    444 
    445    local use_fuzzy_sort = vim.o.completeopt:find('fuzzy') ~= nil
    446      and vim.o.completeopt:find('nosort') == nil
    447      and not result.isIncomplete
    448      and #prefix > 0
    449 
    450    local compare_fn = use_fuzzy_sort
    451        and function(a, b)
    452          local score_a = a._fuzzy_score or 0
    453          local score_b = b._fuzzy_score or 0
    454          if score_a ~= score_b then
    455            return score_a > score_b
    456          end
    457          return compare_by_sortText_and_label(a, b)
    458        end
    459      or compare_by_sortText_and_label
    460 
    461    table.sort(candidates, compare_fn)
    462  end
    463  return candidates
    464 end
    465 
    466 --- @param lnum integer 0-indexed
    467 --- @param line string
    468 --- @param items lsp.CompletionItem[]
    469 --- @param encoding 'utf-8'|'utf-16'|'utf-32'
    470 --- @return integer?
    471 local function adjust_start_col(lnum, line, items, encoding)
    472  local min_start_char = nil
    473  for _, item in pairs(items) do
    474    if item.textEdit then
    475      local start_char = nil
    476      if item.textEdit.range and item.textEdit.range.start.line == lnum then
    477        start_char = item.textEdit.range.start.character
    478      elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
    479        start_char = item.textEdit.insert.start.character
    480      end
    481      if start_char then
    482        if not min_start_char or start_char < min_start_char then
    483          min_start_char = start_char
    484        end
    485      end
    486    end
    487  end
    488  if min_start_char then
    489    return vim.str_byteindex(line, encoding, min_start_char, false)
    490  else
    491    return nil
    492  end
    493 end
    494 
    495 --- @param line string line content
    496 --- @param lnum integer 0-indexed line number
    497 --- @param cursor_col integer
    498 --- @param client_id integer client ID
    499 --- @param client_start_boundary integer 0-indexed word boundary
    500 --- @param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
    501 --- @param result vim.lsp.CompletionResult
    502 --- @param encoding 'utf-8'|'utf-16'|'utf-32'
    503 --- @return table[] matches
    504 --- @return integer? server_start_boundary
    505 function M._convert_results(
    506  line,
    507  lnum,
    508  cursor_col,
    509  client_id,
    510  client_start_boundary,
    511  server_start_boundary,
    512  result,
    513  encoding
    514 )
    515  -- Completion response items may be relative to a position different than `client_start_boundary`.
    516  -- Concrete example, with lua-language-server:
    517  --
    518  -- require('plenary.asy|
    519  --         ▲       ▲   ▲
    520  --         │       │   └── cursor_pos:                     20
    521  --         │       └────── client_start_boundary:          17
    522  --         └────────────── textEdit.range.start.character: 9
    523  --                                 .newText = 'plenary.async'
    524  --                  ^^^
    525  --                  prefix (We'd remove everything not starting with `asy`,
    526  --                  so we'd eliminate the `plenary.async` result
    527  --
    528  -- `adjust_start_col` is used to prefer the language server boundary.
    529  --
    530  local candidates = get_items(result)
    531  local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
    532  if server_start_boundary == nil then
    533    server_start_boundary = curstartbyte
    534  elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
    535    server_start_boundary = client_start_boundary
    536  end
    537  local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
    538  local matches =
    539    M._lsp_to_complete_items(result, prefix, client_id, server_start_boundary, line, lnum, encoding)
    540 
    541  return matches, server_start_boundary
    542 end
    543 
    544 -- NOTE: The reason we don't use `lsp.buf_request_all` here is because we want to filter the clients
    545 -- that received the request based on the trigger characters.
    546 --- @param clients table<integer, vim.lsp.Client> # keys != client_id
    547 --- @param bufnr integer
    548 --- @param win integer
    549 --- @param ctx? lsp.CompletionContext
    550 --- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
    551 --- @return function # Cancellation function
    552 local function request(clients, bufnr, win, ctx, callback)
    553  local responses = {} --- @type table<integer, { err: lsp.ResponseError, result: any }>
    554  local request_ids = {} --- @type table<integer, integer>
    555  local remaining_requests = vim.tbl_count(clients)
    556 
    557  for _, client in pairs(clients) do
    558    local client_id = client.id
    559    local params = lsp.util.make_position_params(win, client.offset_encoding)
    560    --- @cast params lsp.CompletionParams
    561    params.context = ctx
    562    local ok, request_id = client:request('textDocument/completion', params, function(err, result)
    563      responses[client_id] = { err = err, result = result }
    564      remaining_requests = remaining_requests - 1
    565      if remaining_requests == 0 then
    566        callback(responses)
    567      end
    568    end, bufnr)
    569 
    570    if ok then
    571      request_ids[client_id] = request_id
    572    end
    573  end
    574 
    575  return function()
    576    for client_id, request_id in pairs(request_ids) do
    577      local client = lsp.get_client_by_id(client_id)
    578      if client then
    579        client:cancel_request(request_id)
    580      end
    581    end
    582  end
    583 end
    584 
    585 --- @param bufnr integer
    586 --- @param clients vim.lsp.Client[]
    587 --- @param ctx? lsp.CompletionContext
    588 local function trigger(bufnr, clients, ctx)
    589  reset_timer()
    590  Context:cancel_pending()
    591 
    592  if tonumber(vim.fn.pumvisible()) == 1 and not Context.isIncomplete then
    593    return
    594  end
    595 
    596  local win = api.nvim_get_current_win()
    597  local cursor_row = api.nvim_win_get_cursor(win)[1]
    598  local start_time = vim.uv.hrtime() --[[@as integer]]
    599  Context.last_request_time = start_time
    600 
    601  local cancel_request = request(clients, bufnr, win, ctx, function(responses)
    602    local end_time = vim.uv.hrtime()
    603    rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms)
    604 
    605    Context.pending_requests = {}
    606    Context.isIncomplete = false
    607 
    608    local new_cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer
    609    local row_changed = new_cursor_row ~= cursor_row
    610    local mode = api.nvim_get_mode().mode
    611    if row_changed or not (mode == 'i' or mode == 'ic') then
    612      return
    613    end
    614 
    615    local line = api.nvim_get_current_line()
    616    local line_to_cursor = line:sub(1, cursor_col)
    617    local word_boundary = vim.fn.match(line_to_cursor, '\\k*$')
    618 
    619    local matches = {}
    620 
    621    local server_start_boundary --- @type integer?
    622    for client_id, response in pairs(responses) do
    623      local client = lsp.get_client_by_id(client_id)
    624      if response.err then
    625        local msg = ('%s: %s %s'):format(
    626          client and client.name or 'UNKNOWN',
    627          response.err.code or 'NO_CODE',
    628          response.err.message
    629        )
    630        vim.notify_once(msg, vim.log.levels.WARN)
    631      end
    632 
    633      local result = response.result
    634      if result and #(result.items or result) > 0 then
    635        Context.isIncomplete = Context.isIncomplete or result.isIncomplete
    636        local encoding = client and client.offset_encoding or 'utf-16'
    637        local client_matches, tmp_server_start_boundary
    638        client_matches, tmp_server_start_boundary = M._convert_results(
    639          line,
    640          cursor_row - 1,
    641          cursor_col,
    642          client_id,
    643          word_boundary,
    644          nil,
    645          result,
    646          encoding
    647        )
    648 
    649        server_start_boundary = tmp_server_start_boundary or server_start_boundary
    650        vim.list_extend(matches, client_matches)
    651      end
    652    end
    653 
    654    --- @type table[]
    655    local prev_matches = vim.fn.complete_info({ 'items', 'matches' })['items']
    656 
    657    --- @param prev_match table
    658    prev_matches = vim.tbl_filter(function(prev_match)
    659      local client_id = vim.tbl_get(prev_match, 'user_data', 'nvim', 'lsp', 'client_id')
    660      if client_id and responses[client_id] ~= nil then
    661        return false
    662      end
    663      return vim.tbl_get(prev_match, 'match')
    664    end, prev_matches)
    665 
    666    matches = vim.list_extend(prev_matches, matches)
    667    local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
    668    if user_cmp then
    669      table.sort(matches, user_cmp)
    670    end
    671 
    672    local start_col = (server_start_boundary or word_boundary) + 1
    673    Context.cursor = { cursor_row, start_col }
    674    vim.fn.complete(start_col, matches)
    675  end)
    676 
    677  table.insert(Context.pending_requests, cancel_request)
    678 end
    679 
    680 --- @param handle vim.lsp.completion.BufHandle
    681 local function on_insert_char_pre(handle)
    682  if tonumber(vim.fn.pumvisible()) == 1 then
    683    if Context.isIncomplete then
    684      reset_timer()
    685 
    686      local debounce_ms = next_debounce()
    687      local ctx = { triggerKind = protocol.CompletionTriggerKind.TriggerForIncompleteCompletions }
    688      if debounce_ms == 0 then
    689        vim.schedule(function()
    690          M.get({ ctx = ctx })
    691        end)
    692      else
    693        completion_timer = new_timer()
    694        completion_timer:start(
    695          math.floor(debounce_ms),
    696          0,
    697          vim.schedule_wrap(function()
    698            M.get({ ctx = ctx })
    699          end)
    700        )
    701      end
    702    end
    703 
    704    return
    705  end
    706 
    707  local char = api.nvim_get_vvar('char')
    708  local matched_clients = handle.triggers[char]
    709  -- Discard pending trigger char, complete the "latest" one.
    710  -- Can happen if a mapping inputs multiple trigger chars simultaneously.
    711  reset_timer()
    712  if matched_clients then
    713    completion_timer = assert(vim.uv.new_timer())
    714    completion_timer:start(25, 0, function()
    715      reset_timer()
    716      vim.schedule(function()
    717        trigger(
    718          api.nvim_get_current_buf(),
    719          matched_clients,
    720          { triggerKind = protocol.CompletionTriggerKind.TriggerCharacter, triggerCharacter = char }
    721        )
    722      end)
    723    end)
    724  end
    725 end
    726 
    727 local function on_insert_leave()
    728  reset_timer()
    729  Context.cursor = nil
    730  Context:reset()
    731 end
    732 
    733 local function on_complete_done()
    734  local completed_item = api.nvim_get_vvar('completed_item')
    735  if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then
    736    Context:reset()
    737    return
    738  end
    739 
    740  local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer
    741  cursor_row = cursor_row - 1
    742  local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem
    743  local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer
    744  if not completion_item or not client_id then
    745    Context:reset()
    746    return
    747  end
    748 
    749  local bufnr = api.nvim_get_current_buf()
    750  local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet
    751    and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil)
    752 
    753  Context:reset()
    754 
    755  local client = lsp.get_client_by_id(client_id)
    756  if not client then
    757    return
    758  end
    759 
    760  local position_encoding = client.offset_encoding or 'utf-16'
    761  local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider
    762 
    763  local function clear_word()
    764    if not expand_snippet then
    765      return nil
    766    end
    767 
    768    -- Remove the already inserted word.
    769    api.nvim_buf_set_text(
    770      bufnr,
    771      Context.cursor[1] - 1,
    772      Context.cursor[2] - 1,
    773      cursor_row,
    774      cursor_col,
    775      { '' }
    776    )
    777  end
    778 
    779  local function apply_snippet_and_command()
    780    if expand_snippet then
    781      apply_snippet(completion_item)
    782    end
    783 
    784    local command = completion_item.command
    785    if command then
    786      client:exec_cmd(command, { bufnr = bufnr })
    787    end
    788  end
    789 
    790  if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
    791    clear_word()
    792    lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding)
    793    apply_snippet_and_command()
    794  elseif resolve_provider and type(completion_item) == 'table' then
    795    local changedtick = vim.b[bufnr].changedtick
    796 
    797    --- @param result lsp.CompletionItem
    798    client:request('completionItem/resolve', completion_item, function(err, result)
    799      if changedtick ~= vim.b[bufnr].changedtick then
    800        return
    801      end
    802 
    803      clear_word()
    804      if err then
    805        vim.notify_once(err.message, vim.log.levels.WARN)
    806      elseif result then
    807        if result.additionalTextEdits then
    808          lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding)
    809        end
    810        if result.command then
    811          completion_item.command = result.command
    812        end
    813      end
    814      apply_snippet_and_command()
    815    end, bufnr)
    816  else
    817    clear_word()
    818    apply_snippet_and_command()
    819  end
    820 end
    821 
    822 ---@param bufnr integer
    823 ---@return string
    824 local function get_augroup(bufnr)
    825  return string.format('nvim.lsp.completion_%d', bufnr)
    826 end
    827 
    828 --- @param client_id integer
    829 --- @param bufnr integer
    830 local function disable_completions(client_id, bufnr)
    831  local handle = buf_handles[bufnr]
    832  if not handle then
    833    return
    834  end
    835 
    836  handle.clients[client_id] = nil
    837  if not next(handle.clients) then
    838    buf_handles[bufnr] = nil
    839    api.nvim_del_augroup_by_name(get_augroup(bufnr))
    840  else
    841    for char, clients in pairs(handle.triggers) do
    842      --- @param c vim.lsp.Client
    843      handle.triggers[char] = vim.tbl_filter(function(c)
    844        return c.id ~= client_id
    845      end, clients)
    846    end
    847  end
    848 end
    849 
    850 --- @inlinedoc
    851 --- @class vim.lsp.completion.BufferOpts
    852 --- @field autotrigger? boolean  (default: false) When true, completion triggers automatically based on the server's `triggerCharacters`.
    853 --- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|.
    854 --- @field cmp? fun(a: table, b: table): boolean Comparator for sorting merged completion items from all servers.
    855 
    856 ---@param client_id integer
    857 ---@param bufnr integer
    858 ---@param opts vim.lsp.completion.BufferOpts
    859 local function enable_completions(client_id, bufnr, opts)
    860  local buf_handle = buf_handles[bufnr]
    861  if not buf_handle then
    862    buf_handle = { clients = {}, triggers = {}, convert = opts.convert, cmp = opts.cmp }
    863    buf_handles[bufnr] = buf_handle
    864 
    865    -- Attach to buffer events.
    866    api.nvim_buf_attach(bufnr, false, {
    867      on_detach = function(_, buf)
    868        buf_handles[buf] = nil
    869      end,
    870      on_reload = function(_, buf)
    871        M.enable(true, client_id, buf, opts)
    872      end,
    873    })
    874 
    875    -- Set up autocommands.
    876    local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = true })
    877    api.nvim_create_autocmd('LspDetach', {
    878      group = group,
    879      buffer = bufnr,
    880      desc = 'vim.lsp.completion: clean up client on detach',
    881      callback = function(args)
    882        disable_completions(args.data.client_id, args.buf)
    883      end,
    884    })
    885    api.nvim_create_autocmd('CompleteDone', {
    886      group = group,
    887      buffer = bufnr,
    888      callback = function()
    889        local reason = api.nvim_get_vvar('event').reason --- @type string
    890        if reason == 'accept' then
    891          on_complete_done()
    892        end
    893      end,
    894    })
    895    if opts.autotrigger then
    896      api.nvim_create_autocmd('InsertCharPre', {
    897        group = group,
    898        buffer = bufnr,
    899        callback = function()
    900          on_insert_char_pre(buf_handles[bufnr])
    901        end,
    902      })
    903      api.nvim_create_autocmd('InsertLeave', {
    904        group = group,
    905        buffer = bufnr,
    906        callback = on_insert_leave,
    907      })
    908    end
    909  end
    910 
    911  if not buf_handle.clients[client_id] then
    912    local client = lsp.get_client_by_id(client_id)
    913    assert(client, 'invalid client ID')
    914 
    915    -- Add the new client to the buffer's clients.
    916    buf_handle.clients[client_id] = client
    917 
    918    -- Add the new client to the clients that should be triggered by its trigger characters.
    919    --- @type string[]
    920    local triggers = vim.tbl_get(
    921      client.server_capabilities,
    922      'completionProvider',
    923      'triggerCharacters'
    924    ) or {}
    925    for _, char in ipairs(triggers) do
    926      local clients_for_trigger = buf_handle.triggers[char]
    927      if not clients_for_trigger then
    928        clients_for_trigger = {}
    929        buf_handle.triggers[char] = clients_for_trigger
    930      end
    931      local client_exists = vim.iter(clients_for_trigger):any(function(c)
    932        return c.id == client_id
    933      end)
    934      if not client_exists then
    935        table.insert(clients_for_trigger, client)
    936      end
    937    end
    938  end
    939 end
    940 
    941 --- Enables or disables completions from the given language client in the given
    942 --- buffer. Effects of enabling completions are:
    943 ---
    944 --- - Calling |vim.lsp.completion.get()| uses the enabled clients to retrieve
    945 ---   completion candidates
    946 ---
    947 --- - Accepting a completion candidate using `<c-y>` applies side effects like
    948 ---   expanding snippets, text edits (e.g. insert import statements) and
    949 ---   executing associated commands. This works for completions triggered via
    950 ---   autotrigger, omnifunc or completion.get()
    951 ---
    952 --- Example: |lsp-attach| |lsp-completion|
    953 ---
    954 --- Note: the behavior of `autotrigger=true` is controlled by the LSP `triggerCharacters` field. You
    955 --- can override it on LspAttach, see |lsp-autocompletion|.
    956 ---
    957 --- @param enable boolean True to enable, false to disable
    958 --- @param client_id integer Client ID
    959 --- @param bufnr integer Buffer handle, or 0 for the current buffer
    960 --- @param opts? vim.lsp.completion.BufferOpts
    961 function M.enable(enable, client_id, bufnr, opts)
    962  bufnr = vim._resolve_bufnr(bufnr)
    963 
    964  if enable then
    965    enable_completions(client_id, bufnr, opts or {})
    966  else
    967    disable_completions(client_id, bufnr)
    968  end
    969 end
    970 
    971 --- @inlinedoc
    972 --- @class vim.lsp.completion.get.Opts
    973 --- @field ctx? lsp.CompletionContext Completion context. Defaults to a trigger kind of `invoked`.
    974 
    975 --- Triggers LSP completion once in the current buffer, if LSP completion is enabled
    976 --- (see |lsp-attach| |lsp-completion|).
    977 ---
    978 --- Used by the default LSP |omnicompletion| provider |vim.lsp.omnifunc()|, thus |i_CTRL-X_CTRL-O|
    979 --- invokes this in LSP-enabled buffers. Use CTRL-Y to select an item from the completion menu.
    980 --- |complete_CTRL-Y|
    981 ---
    982 --- To invoke manually with CTRL-space, use this mapping:
    983 --- ```lua
    984 --- -- Use CTRL-space to trigger LSP completion.
    985 --- -- Use CTRL-Y to select an item. |complete_CTRL-Y|
    986 --- vim.keymap.set('i', '<c-space>', function()
    987 ---   vim.lsp.completion.get()
    988 --- end)
    989 --- ```
    990 ---
    991 --- @param opts? vim.lsp.completion.get.Opts
    992 function M.get(opts)
    993  opts = opts or {}
    994  local ctx = opts.ctx or { triggerKind = protocol.CompletionTriggerKind.Invoked }
    995  local bufnr = api.nvim_get_current_buf()
    996  local clients = (buf_handles[bufnr] or {}).clients or {}
    997 
    998  trigger(bufnr, clients, ctx)
    999 end
   1000 
   1001 --- Implements 'omnifunc' compatible LSP completion.
   1002 ---
   1003 --- @see |complete-functions|
   1004 --- @see |complete-items|
   1005 --- @see |CompleteDone|
   1006 ---
   1007 --- @param findstart integer 0 or 1, decides behavior
   1008 --- @param base integer findstart=0, text to match against
   1009 ---
   1010 --- @return integer|table Decided by {findstart}:
   1011 --- - findstart=0: column where the completion starts, or -2 or -3
   1012 --- - findstart=1: list of matches (actually just calls |complete()|)
   1013 function M._omnifunc(findstart, base)
   1014  lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base })
   1015  assert(base) -- silence luals
   1016  local bufnr = api.nvim_get_current_buf()
   1017  local clients = lsp.get_clients({ bufnr = bufnr, method = 'textDocument/completion' })
   1018  local remaining = #clients
   1019  if remaining == 0 then
   1020    return findstart == 1 and -1 or {}
   1021  end
   1022 
   1023  trigger(bufnr, clients, { triggerKind = protocol.CompletionTriggerKind.Invoked })
   1024 
   1025  -- Return -2 to signal that we should continue completion so that we can
   1026  -- async complete.
   1027  return -2
   1028 end
   1029 
   1030 return M