--- @brief
--- The `vim.lsp.completion` module enables insert-mode completion driven by an LSP server. Call
--- `enable()` to make it available through Nvim builtin completion (via the |CompleteDone| event).
--- Specify `autotrigger=true` to activate "auto-completion" when you type any of the server-defined
--- `triggerCharacters`. Use CTRL-Y to select an item from the completion menu. |complete_CTRL-Y|
---
--- Example: activate LSP-driven auto-completion:
--- ```lua
--- -- Works best if 'completeopt' has "noselect".
--- -- Use CTRL-Y to select an item. |complete_CTRL-Y|
--- vim.cmd[[set completeopt+=menuone,noselect,popup]]
--- vim.lsp.start({
--- name = 'ts_ls',
--- cmd = …,
--- on_attach = function(client, bufnr)
--- vim.lsp.completion.enable(true, client.id, bufnr, {
--- autotrigger = true,
--- convert = function(item)
--- return { abbr = item.label:gsub('%b()', '') }
--- end,
--- })
--- end,
--- })
--- ```
---
--- [lsp-autocompletion]()
---
--- The LSP `triggerCharacters` field decides when to trigger autocompletion. If you want to trigger
--- on EVERY keypress you can either:
--- - Extend `client.server_capabilities.completionProvider.triggerCharacters` on `LspAttach`,
--- before you call `vim.lsp.completion.enable(… {autotrigger=true})`. See the |lsp-attach| example.
--- - Call `vim.lsp.completion.get()` from an |InsertCharPre| autocommand.
local M = {}
local api = vim.api
local lsp = vim.lsp
local protocol = lsp.protocol
local rtt_ms = 50.0
local ns_to_ms = 0.000001
--- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
-- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
-- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
--- @nodoc
--- @class lsp.ItemDefaults
--- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
--- @field insertTextFormat lsp.InsertTextFormat?
--- @field insertTextMode lsp.InsertTextMode?
--- @field data any
--- @nodoc
--- @class vim.lsp.completion.BufHandle
--- @field clients table<integer, vim.lsp.Client>
--- @field triggers table<string, vim.lsp.Client[]>
--- @field convert? fun(item: lsp.CompletionItem): table
--- @type table<integer, vim.lsp.completion.BufHandle>
local buf_handles = {}
--- @nodoc
--- @class vim.lsp.completion.Context
local Context = {
cursor = nil, --- @type [integer, integer]?
last_request_time = nil, --- @type integer?
pending_requests = {}, --- @type function[]
isIncomplete = false,
-- Handles "completionItem/resolve".
resolve_handler = nil, --- @type CompletionResolver?
}
--- @nodoc
function Context:cancel_pending()
for _, cancel in ipairs(self.pending_requests) do
cancel()
end
self.pending_requests = {}
end
--- @nodoc
function Context:reset()
-- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event.
self.isIncomplete = false
self.last_request_time = nil
self:cancel_pending()
if self.resolve_handler then
self.resolve_handler:cleanup()
self.resolve_handler = nil
end
end
--- @type uv.uv_timer_t?
local completion_timer = nil
--- @return uv.uv_timer_t
local function new_timer()
return (assert(vim.uv.new_timer()))
end
local function reset_timer()
if completion_timer then
completion_timer:stop()
completion_timer:close()
end
completion_timer = nil
end
--- @param window integer
--- @param warmup integer
--- @return fun(sample: integer): integer
local function exp_avg(window, warmup)
local count = 0
local sum = 0
local value = 0.0
return function(sample)
if count < warmup then
count = count + 1
sum = sum + sample
value = sum / count
else
local factor = 2.0 / (window + 1)
value = value * (1 - factor) + sample * factor
end
return value
end
end
local compute_new_average = exp_avg(10, 10)
--- Calculates the adaptive debounce time based on the elapsed time since the last request.
---
--- @param last_request_time integer?
--- @param current_rtt_ms number
--- @return integer
local function adaptive_debounce(last_request_time, current_rtt_ms)
if not last_request_time then
return current_rtt_ms
end
local ms_since_request = (vim.uv.hrtime() - last_request_time) * ns_to_ms
return math.max((ms_since_request - current_rtt_ms) * -1, 0)
end
--- @param flag string
--- @return boolean
local function has_completeopt(flag)
return vim.list_contains(vim.opt.completeopt:get(), flag)
end
--- @param input string Unparsed snippet
--- @return string # Parsed snippet if successful, else returns its input
local function parse_snippet(input)
local ok, parsed = pcall(function()
return lsp._snippet_grammar.parse(input)
end)
return ok and tostring(parsed) or input
end
--- @param item lsp.CompletionItem
local function apply_snippet(item)
if item.textEdit then
vim.snippet.expand(item.textEdit.newText)
elseif item.insertText then
vim.snippet.expand(item.insertText)
end
end
--- Returns text that should be inserted when a selecting completion item. The
--- precedence is as follows: textEdit.newText > insertText > label
---
--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
---
--- @param item lsp.CompletionItem
--- @param prefix string
--- @param match fun(text: string, prefix: string):boolean
--- @return string
local function get_completion_word(item, prefix, match)
if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
|