mirror of
https://github.com/neovim/neovim.git
synced 2026-03-28 11:22:03 +00:00
refactor(lua): add integer coercion helpers Add vim._tointeger() and vim._ensure_integer(), including optional base support, and switch integer-only tonumber()/assert call sites in the Lua runtime to use them. This also cleans up related integer parsing in LSP, health, loader, URI, tohtml, and Treesitter code. supported by AI
1268 lines
39 KiB
Lua
1268 lines
39 KiB
Lua
--- @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
|
|
if item.textEdit or (item.insertText and item.insertText ~= '') then
|
|
-- Use label instead of text if text has different starting characters.
|
|
-- label is used as abbr (=displayed), but word is used for filtering
|
|
-- This is required for things like postfix completion.
|
|
-- E.g. in lua:
|
|
--
|
|
-- local f = {}
|
|
-- f@|
|
|
-- ▲
|
|
-- └─ cursor
|
|
--
|
|
-- item.textEdit.newText: table.insert(f, $0)
|
|
-- label: insert
|
|
--
|
|
-- Typing `i` would remove the candidate because newText starts with `t`.
|
|
local text = parse_snippet(item.insertText or item.textEdit.newText)
|
|
local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
|
|
if item.filterText and not match(word, prefix) then
|
|
return item.filterText
|
|
else
|
|
return word
|
|
end
|
|
else
|
|
return item.label
|
|
end
|
|
elseif item.textEdit then
|
|
local word = item.textEdit.newText
|
|
word = string.gsub(word, '\r\n?', '\n')
|
|
return word:match('([^\n]*)') or word
|
|
elseif item.insertText and item.insertText ~= '' then
|
|
return item.insertText
|
|
end
|
|
return item.label
|
|
end
|
|
|
|
--- Applies the given defaults to the completion item, modifying it in place.
|
|
---
|
|
--- @param item lsp.CompletionItem
|
|
--- @param defaults lsp.ItemDefaults?
|
|
local function apply_defaults(item, defaults)
|
|
if not defaults then
|
|
return
|
|
end
|
|
|
|
item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
|
|
item.insertTextMode = item.insertTextMode or defaults.insertTextMode
|
|
item.data = item.data or defaults.data
|
|
if defaults.editRange then
|
|
local textEdit = item.textEdit or {}
|
|
item.textEdit = textEdit
|
|
textEdit.newText = textEdit.newText or item.textEditText or item.insertText or item.label
|
|
if defaults.editRange.start then
|
|
textEdit.range = textEdit.range or defaults.editRange
|
|
elseif defaults.editRange.insert then
|
|
textEdit.insert = defaults.editRange.insert
|
|
textEdit.replace = defaults.editRange.replace
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param result vim.lsp.CompletionResult
|
|
--- @return lsp.CompletionItem[]
|
|
local function get_items(result)
|
|
if result.items then
|
|
-- When we have a list, apply the defaults and return an array of items.
|
|
for _, item in ipairs(result.items) do
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
apply_defaults(item, result.itemDefaults)
|
|
end
|
|
return result.items
|
|
else
|
|
-- Else just return the items as they are.
|
|
return result
|
|
end
|
|
end
|
|
|
|
---@param item lsp.CompletionItem
|
|
---@return string
|
|
local function get_doc(item)
|
|
if
|
|
has_completeopt('popup')
|
|
and item.insertTextFormat == protocol.InsertTextFormat.Snippet
|
|
and #(item.documentation or '') == 0
|
|
and vim.bo.filetype ~= ''
|
|
and (item.textEdit or (item.insertText and item.insertText ~= ''))
|
|
then
|
|
-- Shows snippet preview in doc popup if completeopt=popup.
|
|
local text = parse_snippet(item.insertText or item.textEdit.newText)
|
|
return ('```%s\n%s\n```'):format(vim.bo.filetype, text)
|
|
end
|
|
|
|
local doc = item.documentation
|
|
if not doc then
|
|
return ''
|
|
end
|
|
if type(doc) == 'string' then
|
|
return doc
|
|
end
|
|
if type(doc) == 'table' and type(doc.value) == 'string' then
|
|
return doc.value
|
|
end
|
|
|
|
vim.notify('invalid documentation value: ' .. vim.inspect(doc), vim.log.levels.WARN)
|
|
return ''
|
|
end
|
|
|
|
---@param value string
|
|
---@param prefix string
|
|
---@return boolean
|
|
---@return integer?
|
|
local function match_item_by_value(value, prefix)
|
|
if prefix == '' then
|
|
return true, nil
|
|
end
|
|
if has_completeopt('fuzzy') then
|
|
local score = vim.fn.matchfuzzypos({ value }, prefix)[3] ---@type table
|
|
return #score > 0, score[1]
|
|
end
|
|
|
|
if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
|
|
return vim.startswith(value:lower(), prefix:lower()), nil
|
|
end
|
|
return vim.startswith(value, prefix), nil
|
|
end
|
|
|
|
--- Generate kind text for completion color items
|
|
--- Parse color from doc and return colored symbol ■
|
|
---
|
|
---@param item table completion item with kind and documentation
|
|
---@return string? kind text or "■" for colors
|
|
---@return string? highlight group for colors
|
|
local function generate_kind(item)
|
|
if not lsp.protocol.CompletionItemKind[item.kind] then
|
|
return 'Unknown'
|
|
end
|
|
if item.kind ~= lsp.protocol.CompletionItemKind.Color then
|
|
return lsp.protocol.CompletionItemKind[item.kind]
|
|
end
|
|
local doc = get_doc(item)
|
|
if #doc == 0 then
|
|
return
|
|
end
|
|
|
|
-- extract hex from RGB format
|
|
local r, g, b = doc:match('rgb%((%d+)%s*,?%s*(%d+)%s*,?%s*(%d+)%)')
|
|
local hex = r
|
|
and string.format(
|
|
'%02x%02x%02x',
|
|
vim._ensure_integer(r),
|
|
vim._ensure_integer(g),
|
|
vim._ensure_integer(b)
|
|
)
|
|
or doc:match('#?([%da-fA-F]+)')
|
|
|
|
if not hex then
|
|
return
|
|
end
|
|
|
|
-- expand 3-digit hex to 6-digit
|
|
if #hex == 3 then
|
|
hex = hex:gsub('.', '%1%1')
|
|
end
|
|
|
|
if #hex ~= 6 then
|
|
return
|
|
end
|
|
|
|
hex = hex:lower()
|
|
local group = ('@lsp.color.%s'):format(hex)
|
|
if #api.nvim_get_hl(0, { name = group }) == 0 then
|
|
api.nvim_set_hl(0, group, { fg = '#' .. hex })
|
|
end
|
|
|
|
return '■', group
|
|
end
|
|
|
|
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
|
--- |complete-items|.
|
|
---
|
|
--- @param result vim.lsp.CompletionResult Result of `textDocument/completion`
|
|
--- @param prefix string prefix to filter the completion items
|
|
--- @param client_id integer? Client ID
|
|
--- @param server_start_boundary integer? server start boundary
|
|
--- @param line string? current line content
|
|
--- @param lnum integer? 0-indexed line number
|
|
--- @param encoding string? encoding
|
|
--- @return table[]
|
|
--- @see complete-items
|
|
function M._lsp_to_complete_items(
|
|
result,
|
|
prefix,
|
|
client_id,
|
|
server_start_boundary,
|
|
line,
|
|
lnum,
|
|
encoding
|
|
)
|
|
local items = get_items(result)
|
|
if vim.tbl_isempty(items) then
|
|
return {}
|
|
end
|
|
|
|
---@type fun(item: lsp.CompletionItem):boolean
|
|
local matches
|
|
if not prefix:find('%w') then
|
|
matches = function(_)
|
|
return true
|
|
end
|
|
else
|
|
---@param item lsp.CompletionItem
|
|
matches = function(item)
|
|
if item.filterText then
|
|
return match_item_by_value(item.filterText, prefix)
|
|
end
|
|
|
|
if item.textEdit and not item.textEdit.newText then
|
|
-- server took care of filtering
|
|
return true
|
|
end
|
|
|
|
return match_item_by_value(item.label, prefix)
|
|
end
|
|
end
|
|
|
|
local candidates = {}
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
|
|
local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
|
|
for _, item in ipairs(items) do
|
|
local match, score = matches(item)
|
|
if match then
|
|
local word = get_completion_word(item, prefix, match_item_by_value)
|
|
|
|
if server_start_boundary and line and lnum and encoding and item.textEdit then
|
|
--- @type integer?
|
|
local item_start_char
|
|
if item.textEdit.range and item.textEdit.range.start.line == lnum then
|
|
item_start_char = item.textEdit.range.start.character
|
|
elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
|
|
item_start_char = item.textEdit.insert.start.character
|
|
end
|
|
|
|
if item_start_char then
|
|
local item_start_byte = vim.str_byteindex(line, encoding, item_start_char, false)
|
|
if item_start_byte > server_start_boundary then
|
|
local missing_prefix = line:sub(server_start_boundary + 1, item_start_byte)
|
|
word = missing_prefix .. word
|
|
end
|
|
end
|
|
end
|
|
|
|
local hl_group = ''
|
|
if
|
|
item.deprecated
|
|
or vim.list_contains((item.tags or {}), protocol.CompletionTag.Deprecated)
|
|
then
|
|
hl_group = 'DiagnosticDeprecated'
|
|
end
|
|
local kind, kind_hlgroup = generate_kind(item)
|
|
local completion_item = {
|
|
word = word,
|
|
abbr = item.label,
|
|
kind = kind,
|
|
menu = item.detail or '',
|
|
info = get_doc(item),
|
|
icase = 1,
|
|
dup = 1,
|
|
empty = 1,
|
|
abbr_hlgroup = hl_group,
|
|
kind_hlgroup = kind_hlgroup,
|
|
user_data = {
|
|
nvim = {
|
|
lsp = {
|
|
completion_item = item,
|
|
client_id = client_id,
|
|
},
|
|
},
|
|
},
|
|
_fuzzy_score = score,
|
|
}
|
|
if user_convert then
|
|
completion_item = vim.tbl_extend('keep', user_convert(item), completion_item)
|
|
end
|
|
table.insert(candidates, completion_item)
|
|
end
|
|
end
|
|
|
|
if not user_cmp then
|
|
local compare_by_sortText_and_label = function(a, b)
|
|
---@type lsp.CompletionItem
|
|
local itema = a.user_data.nvim.lsp.completion_item
|
|
---@type lsp.CompletionItem
|
|
local itemb = b.user_data.nvim.lsp.completion_item
|
|
return (itema.sortText or itema.label) < (itemb.sortText or itemb.label)
|
|
end
|
|
|
|
local use_fuzzy_sort = has_completeopt('fuzzy')
|
|
and not has_completeopt('nosort')
|
|
and not result.isIncomplete
|
|
and #prefix > 0
|
|
|
|
local compare_fn = use_fuzzy_sort
|
|
and function(a, b)
|
|
local score_a = a._fuzzy_score or 0
|
|
local score_b = b._fuzzy_score or 0
|
|
if score_a ~= score_b then
|
|
return score_a > score_b
|
|
end
|
|
return compare_by_sortText_and_label(a, b)
|
|
end
|
|
or compare_by_sortText_and_label
|
|
|
|
table.sort(candidates, compare_fn)
|
|
end
|
|
return candidates
|
|
end
|
|
|
|
--- @param lnum integer 0-indexed
|
|
--- @param line string
|
|
--- @param items lsp.CompletionItem[]
|
|
--- @param encoding 'utf-8'|'utf-16'|'utf-32'
|
|
--- @return integer?
|
|
local function adjust_start_col(lnum, line, items, encoding)
|
|
local min_start_char = nil
|
|
for _, item in pairs(items) do
|
|
if item.textEdit then
|
|
local start_char = nil
|
|
if item.textEdit.range and item.textEdit.range.start.line == lnum then
|
|
start_char = item.textEdit.range.start.character
|
|
elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
|
|
start_char = item.textEdit.insert.start.character
|
|
end
|
|
if start_char then
|
|
if not min_start_char or start_char < min_start_char then
|
|
min_start_char = start_char
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if min_start_char then
|
|
return vim.str_byteindex(line, encoding, min_start_char, false)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
--- @param line string line content
|
|
--- @param lnum integer 0-indexed line number
|
|
--- @param cursor_col integer
|
|
--- @param client_id integer client ID
|
|
--- @param client_start_boundary integer 0-indexed word boundary
|
|
--- @param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
|
|
--- @param result vim.lsp.CompletionResult
|
|
--- @param encoding 'utf-8'|'utf-16'|'utf-32'
|
|
--- @return table[] matches
|
|
--- @return integer? server_start_boundary
|
|
function M._convert_results(
|
|
line,
|
|
lnum,
|
|
cursor_col,
|
|
client_id,
|
|
client_start_boundary,
|
|
server_start_boundary,
|
|
result,
|
|
encoding
|
|
)
|
|
-- Completion response items may be relative to a position different than `client_start_boundary`.
|
|
-- Concrete example, with lua-language-server:
|
|
--
|
|
-- require('plenary.asy|
|
|
-- ▲ ▲ ▲
|
|
-- │ │ └── cursor_pos: 20
|
|
-- │ └────── client_start_boundary: 17
|
|
-- └────────────── textEdit.range.start.character: 9
|
|
-- .newText = 'plenary.async'
|
|
-- ^^^
|
|
-- prefix (We'd remove everything not starting with `asy`,
|
|
-- so we'd eliminate the `plenary.async` result
|
|
--
|
|
-- `adjust_start_col` is used to prefer the language server boundary.
|
|
--
|
|
local candidates = get_items(result)
|
|
local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
|
|
if server_start_boundary == nil then
|
|
server_start_boundary = curstartbyte
|
|
elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
|
|
server_start_boundary = client_start_boundary
|
|
end
|
|
local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
|
|
local matches =
|
|
M._lsp_to_complete_items(result, prefix, client_id, server_start_boundary, line, lnum, encoding)
|
|
|
|
return matches, server_start_boundary
|
|
end
|
|
|
|
-- NOTE: The reason we don't use `lsp.buf_request_all` here is because we want to filter the clients
|
|
-- that received the request based on the trigger characters.
|
|
--- @param clients table<integer, vim.lsp.Client> # keys != client_id
|
|
--- @param bufnr integer
|
|
--- @param win integer
|
|
--- @param ctx? lsp.CompletionContext
|
|
--- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
|
|
--- @return function # Cancellation function
|
|
local function request(clients, bufnr, win, ctx, callback)
|
|
local responses = {} --- @type table<integer, { err: lsp.ResponseError, result: any }>
|
|
local request_ids = {} --- @type table<integer, integer>
|
|
local remaining_requests = vim.tbl_count(clients)
|
|
|
|
for _, client in pairs(clients) do
|
|
local client_id = client.id
|
|
local params = lsp.util.make_position_params(win, client.offset_encoding)
|
|
--- @cast params lsp.CompletionParams
|
|
params.context = ctx
|
|
local ok, request_id = client:request('textDocument/completion', params, function(err, result)
|
|
responses[client_id] = { err = err, result = result }
|
|
remaining_requests = remaining_requests - 1
|
|
if remaining_requests == 0 then
|
|
callback(responses)
|
|
end
|
|
end, bufnr)
|
|
|
|
if ok then
|
|
request_ids[client_id] = request_id
|
|
end
|
|
end
|
|
|
|
return function()
|
|
for client_id, request_id in pairs(request_ids) do
|
|
local client = lsp.get_client_by_id(client_id)
|
|
if client then
|
|
client:cancel_request(request_id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return string
|
|
local function get_augroup(bufnr)
|
|
return string.format('nvim.lsp.completion_%d', bufnr)
|
|
end
|
|
|
|
--- Updates the completion preview popup: configures conceal level, applies Treesitter or
|
|
--- fallback syntax, and resizes height to fit content
|
|
---
|
|
--- @param winid integer
|
|
--- @param bufnr integer
|
|
--- @param kind? string
|
|
local function update_popup_window(winid, bufnr, kind)
|
|
if winid and api.nvim_win_is_valid(winid) and bufnr and api.nvim_buf_is_valid(bufnr) then
|
|
if kind == lsp.protocol.MarkupKind.Markdown then
|
|
vim.wo[winid].conceallevel = 2
|
|
vim.treesitter.start(bufnr, kind)
|
|
end
|
|
local all = api.nvim_win_text_height(winid, {}).all
|
|
api.nvim_win_set_height(winid, all)
|
|
end
|
|
end
|
|
|
|
--- Handles the LSP "completionItem/resolve"
|
|
---
|
|
--- @nodoc
|
|
--- @class CompletionResolver
|
|
--- @field timer uv.uv_timer_t? Timer used for debouncing
|
|
--- @field bufnr integer? Buffer number for which the resolution is triggered
|
|
--- @field word string? Word being completed
|
|
--- @field last_request_time integer? Last request timestamp
|
|
--- @field doc_rtt_ms integer Last request timestamp
|
|
--- @field doc_compute_new_average fun(sample: integer): integer Last request timestamp
|
|
local CompletionResolver = {}
|
|
CompletionResolver.__index = CompletionResolver
|
|
|
|
--- @nodoc
|
|
---
|
|
--- Creates a new instance of `resolve_completion_item`.
|
|
---
|
|
--- @return CompletionResolver
|
|
function CompletionResolver.new()
|
|
local self = setmetatable({}, CompletionResolver)
|
|
self.timer = nil
|
|
self.bufnr = nil
|
|
self.word = nil
|
|
self.last_request_time = nil
|
|
self.doc_rtt_ms = 100
|
|
self.doc_compute_new_average = exp_avg(10, 5)
|
|
return self
|
|
end
|
|
|
|
--- @nodoc
|
|
---
|
|
--- Cancels any pending requests.
|
|
function CompletionResolver:cancel_pending_requests()
|
|
if self.bufnr then
|
|
lsp.util._cancel_requests({
|
|
method = 'completionItem/resolve',
|
|
bufnr = self.bufnr,
|
|
})
|
|
end
|
|
end
|
|
|
|
--- @nodoc
|
|
---
|
|
--- Cleans up the timer and cancels any ongoing requests.
|
|
function CompletionResolver:cleanup()
|
|
if self.timer and not self.timer:is_closing() then
|
|
self.timer:stop()
|
|
self.timer:close()
|
|
end
|
|
self.timer = nil
|
|
self:cancel_pending_requests()
|
|
end
|
|
|
|
--- @nodoc
|
|
---
|
|
--- Checks if the completionItem/resolve request is valid by ensuring the buffer
|
|
--- and word are valid.
|
|
---
|
|
--- @return boolean, table Validity of the request and the completion info
|
|
function CompletionResolver:is_valid()
|
|
local cmp_info = vim.fn.complete_info({ 'selected', 'completed' })
|
|
return vim.api.nvim_buf_is_valid(self.bufnr)
|
|
and vim.api.nvim_get_current_buf() == self.bufnr
|
|
and vim.startswith(vim.api.nvim_get_mode().mode, 'i')
|
|
and vim.fn.pumvisible() ~= 0
|
|
and (vim.tbl_get(cmp_info, 'completed', 'word') or '') == self.word,
|
|
cmp_info
|
|
end
|
|
|
|
--- @nodoc
|
|
---
|
|
--- Invokes and handles "completionItem/resolve".
|
|
---
|
|
--- @param bufnr integer The buffer number where the request is triggered
|
|
--- @param param table The parameters for the LSP request
|
|
--- @param selected_word string The word being completed
|
|
function CompletionResolver:request(bufnr, param, selected_word)
|
|
self:cleanup()
|
|
|
|
self.bufnr = bufnr
|
|
self.word = selected_word
|
|
local debounce_time = adaptive_debounce(self.last_request_time, self.doc_rtt_ms)
|
|
|
|
self.timer = vim.defer_fn(function()
|
|
local valid, cmp_info = self:is_valid()
|
|
if not valid then
|
|
self:cleanup()
|
|
return
|
|
end
|
|
self:cancel_pending_requests()
|
|
|
|
local client_id = vim.tbl_get(cmp_info.completed, 'user_data', 'nvim', 'lsp', 'client_id')
|
|
local client = client_id and vim.lsp.get_client_by_id(client_id)
|
|
if not client or not client:supports_method('completionItem/resolve') then
|
|
return
|
|
end
|
|
|
|
local start_time = vim.uv.hrtime()
|
|
self.last_request_time = start_time
|
|
|
|
client:request('completionItem/resolve', param, function(err, result)
|
|
local end_time = vim.uv.hrtime()
|
|
local response_time = (end_time - start_time) * ns_to_ms
|
|
self.doc_rtt_ms = self.doc_compute_new_average(response_time)
|
|
|
|
if err or not result or next(result) == nil then
|
|
if err then
|
|
vim.notify(err.message, vim.log.levels.WARN)
|
|
end
|
|
return
|
|
end
|
|
|
|
valid, cmp_info = self:is_valid()
|
|
if not valid then
|
|
return
|
|
end
|
|
|
|
local value = vim.tbl_get(result, 'documentation', 'value')
|
|
if not value then
|
|
return
|
|
end
|
|
local windata = vim.api.nvim__complete_set(cmp_info.selected, {
|
|
info = value,
|
|
})
|
|
local kind = vim.tbl_get(result, 'documentation', 'kind')
|
|
update_popup_window(windata.winid, windata.bufnr, kind)
|
|
end, bufnr)
|
|
end, debounce_time)
|
|
end
|
|
|
|
--- Defines a CompleteChanged handler to request and display LSP completion item documentation
|
|
--- via completionItem/resolve
|
|
local function on_completechanged(group, bufnr)
|
|
api.nvim_create_autocmd('CompleteChanged', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
callback = function(ev)
|
|
local completed_item = vim.v.event.completed_item or {}
|
|
if (completed_item.info or '') ~= '' then
|
|
local data = vim.fn.complete_info({ 'selected' })
|
|
update_popup_window(data.preview_winid, data.preview_bufnr)
|
|
return
|
|
end
|
|
|
|
if
|
|
#lsp.get_clients({
|
|
id = vim.tbl_get(completed_item, 'user_data', 'nvim', 'lsp', 'client_id'),
|
|
method = 'completionItem/resolve',
|
|
bufnr = ev.buf,
|
|
}) == 0
|
|
then
|
|
return
|
|
end
|
|
|
|
-- Retrieve the raw LSP completionItem from completed_item as the parameter for
|
|
-- the completionItem/resolve request
|
|
local param = vim.tbl_get(completed_item, 'user_data', 'nvim', 'lsp', 'completion_item')
|
|
if param then
|
|
Context.resolve_handler = Context.resolve_handler or CompletionResolver.new()
|
|
Context.resolve_handler:request(ev.buf, param, completed_item.word)
|
|
end
|
|
end,
|
|
desc = 'Request and display LSP completion item documentation via completionItem/resolve',
|
|
})
|
|
end
|
|
|
|
local function on_complete_done()
|
|
local completed_item = api.nvim_get_vvar('completed_item')
|
|
if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then
|
|
Context:reset()
|
|
return
|
|
end
|
|
|
|
local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer
|
|
cursor_row = cursor_row - 1
|
|
local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem
|
|
local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer
|
|
if not completion_item or not client_id then
|
|
Context:reset()
|
|
return
|
|
end
|
|
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet
|
|
and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil)
|
|
|
|
Context:reset()
|
|
|
|
local client = lsp.get_client_by_id(client_id)
|
|
if not client then
|
|
return
|
|
end
|
|
|
|
local position_encoding = client.offset_encoding or 'utf-16'
|
|
local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider
|
|
|
|
local function clear_word()
|
|
if not expand_snippet then
|
|
return nil
|
|
end
|
|
|
|
-- Remove the already inserted word.
|
|
api.nvim_buf_set_text(
|
|
bufnr,
|
|
Context.cursor[1] - 1,
|
|
Context.cursor[2] - 1,
|
|
cursor_row,
|
|
cursor_col,
|
|
{ '' }
|
|
)
|
|
end
|
|
|
|
local function apply_snippet_and_command()
|
|
if expand_snippet then
|
|
apply_snippet(completion_item)
|
|
end
|
|
|
|
local command = completion_item.command
|
|
if command then
|
|
client:exec_cmd(command, { bufnr = bufnr })
|
|
end
|
|
end
|
|
|
|
if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
|
|
clear_word()
|
|
lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding)
|
|
apply_snippet_and_command()
|
|
elseif resolve_provider and type(completion_item) == 'table' then
|
|
local changedtick = vim.b[bufnr].changedtick
|
|
|
|
--- @param result lsp.CompletionItem
|
|
client:request('completionItem/resolve', completion_item, function(err, result)
|
|
if changedtick ~= vim.b[bufnr].changedtick then
|
|
return
|
|
end
|
|
|
|
clear_word()
|
|
if err then
|
|
vim.notify_once(err.message, vim.log.levels.WARN)
|
|
elseif result then
|
|
if result.additionalTextEdits then
|
|
lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding)
|
|
end
|
|
if result.command then
|
|
completion_item.command = result.command
|
|
end
|
|
end
|
|
apply_snippet_and_command()
|
|
end, bufnr)
|
|
else
|
|
clear_word()
|
|
apply_snippet_and_command()
|
|
end
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return integer
|
|
local function register_completedone(bufnr)
|
|
local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = false })
|
|
if #api.nvim_get_autocmds({ buffer = bufnr, event = 'CompleteDone', group = group }) > 0 then
|
|
return group
|
|
end
|
|
|
|
api.nvim_create_autocmd('CompleteDone', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
callback = function()
|
|
local reason = api.nvim_get_vvar('event').reason ---@type string
|
|
if reason == 'accept' then
|
|
on_complete_done()
|
|
end
|
|
end,
|
|
})
|
|
|
|
return group
|
|
end
|
|
|
|
--- @param bufnr integer
|
|
--- @param clients vim.lsp.Client[]
|
|
--- @param ctx lsp.CompletionContext
|
|
local function trigger(bufnr, clients, ctx)
|
|
reset_timer()
|
|
Context:cancel_pending()
|
|
|
|
if vim.fn.pumvisible() ~= 0 and not Context.isIncomplete then
|
|
return
|
|
end
|
|
|
|
if ctx and ctx.triggerKind == protocol.CompletionTriggerKind.Invoked then
|
|
register_completedone(bufnr)
|
|
end
|
|
|
|
local win = api.nvim_get_current_win()
|
|
local cursor_row = api.nvim_win_get_cursor(win)[1]
|
|
local start_time = vim.uv.hrtime() --[[@as integer]]
|
|
Context.last_request_time = start_time
|
|
|
|
local cancel_request = request(clients, bufnr, win, ctx, function(responses)
|
|
local end_time = vim.uv.hrtime()
|
|
rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms)
|
|
|
|
Context.pending_requests = {}
|
|
Context.isIncomplete = false
|
|
|
|
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 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)
|
|
if response.err then
|
|
local msg = ('%s: %s %s'):format(
|
|
client and client.name or 'UNKNOWN',
|
|
response.err.code or 'NO_CODE',
|
|
response.err.message
|
|
)
|
|
vim.notify_once(msg, vim.log.levels.WARN)
|
|
end
|
|
|
|
local result = response.result
|
|
if result and #(result.items or result) > 0 then
|
|
Context.isIncomplete = Context.isIncomplete or result.isIncomplete
|
|
local encoding = client and client.offset_encoding or 'utf-16'
|
|
local client_matches, tmp_server_start_boundary
|
|
client_matches, tmp_server_start_boundary = M._convert_results(
|
|
line,
|
|
cursor_row - 1,
|
|
cursor_col,
|
|
client_id,
|
|
word_boundary,
|
|
nil,
|
|
result,
|
|
encoding
|
|
)
|
|
|
|
server_start_boundary = tmp_server_start_boundary or server_start_boundary
|
|
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 user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
|
|
if user_cmp then
|
|
table.sort(matches, user_cmp)
|
|
end
|
|
|
|
local start_col = (server_start_boundary or word_boundary) + 1
|
|
Context.cursor = { cursor_row, start_col }
|
|
if #matches > 0 and has_completeopt('popup') then
|
|
local group = get_augroup(bufnr)
|
|
if
|
|
#api.nvim_get_autocmds({ buffer = bufnr, event = 'CompleteChanged', group = group }) == 0
|
|
then
|
|
on_completechanged(group, bufnr)
|
|
end
|
|
end
|
|
vim.fn.complete(start_col, matches)
|
|
end)
|
|
|
|
table.insert(Context.pending_requests, cancel_request)
|
|
end
|
|
|
|
--- @param handle vim.lsp.completion.BufHandle
|
|
local function on_insert_char_pre(handle)
|
|
if vim.fn.pumvisible() ~= 0 then
|
|
if Context.isIncomplete then
|
|
reset_timer()
|
|
|
|
local debounce_ms = adaptive_debounce(Context.last_request_time, rtt_ms)
|
|
local ctx = { triggerKind = protocol.CompletionTriggerKind.TriggerForIncompleteCompletions }
|
|
if debounce_ms == 0 then
|
|
vim.schedule(function()
|
|
M.get({ ctx = ctx })
|
|
end)
|
|
else
|
|
completion_timer = new_timer()
|
|
completion_timer:start(
|
|
math.floor(debounce_ms),
|
|
0,
|
|
vim.schedule_wrap(function()
|
|
M.get({ ctx = ctx })
|
|
end)
|
|
)
|
|
end
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
local char = vim.v.char
|
|
local matched_clients = handle.triggers[char]
|
|
-- Discard pending trigger char, complete the "latest" one.
|
|
-- Can happen if a mapping inputs multiple trigger chars simultaneously.
|
|
reset_timer()
|
|
if matched_clients then
|
|
completion_timer = assert(vim.uv.new_timer())
|
|
completion_timer:start(25, 0, function()
|
|
reset_timer()
|
|
vim.schedule(function()
|
|
trigger(api.nvim_get_current_buf(), matched_clients, {
|
|
triggerKind = protocol.CompletionTriggerKind.TriggerCharacter,
|
|
triggerCharacter = char,
|
|
})
|
|
end)
|
|
end)
|
|
end
|
|
end
|
|
|
|
local function on_insert_leave()
|
|
reset_timer()
|
|
Context.cursor = nil
|
|
Context:reset()
|
|
end
|
|
|
|
--- @param client_id integer
|
|
--- @param bufnr integer
|
|
local function disable_completions(client_id, bufnr)
|
|
local handle = buf_handles[bufnr]
|
|
if not handle then
|
|
return
|
|
end
|
|
|
|
handle.clients[client_id] = nil
|
|
if not next(handle.clients) then
|
|
buf_handles[bufnr] = nil
|
|
api.nvim_del_augroup_by_name(get_augroup(bufnr))
|
|
else
|
|
for char, clients in pairs(handle.triggers) do
|
|
--- @param c vim.lsp.Client
|
|
handle.triggers[char] = vim.tbl_filter(function(c)
|
|
return c.id ~= client_id
|
|
end, clients)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @inlinedoc
|
|
--- @class vim.lsp.completion.BufferOpts
|
|
--- @field autotrigger? boolean (default: false) When true, completion triggers automatically based on the server's `triggerCharacters`.
|
|
--- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|.
|
|
--- @field cmp? fun(a: table, b: table): boolean Comparator for sorting merged completion items from all servers.
|
|
|
|
---@param client_id integer
|
|
---@param bufnr integer
|
|
---@param opts vim.lsp.completion.BufferOpts
|
|
local function enable_completions(client_id, bufnr, opts)
|
|
local buf_handle = buf_handles[bufnr]
|
|
if not buf_handle then
|
|
buf_handle = { clients = {}, triggers = {}, convert = opts.convert, cmp = opts.cmp }
|
|
buf_handles[bufnr] = buf_handle
|
|
|
|
-- Attach to buffer events.
|
|
api.nvim_buf_attach(bufnr, false, {
|
|
on_detach = function(_, buf)
|
|
buf_handles[buf] = nil
|
|
end,
|
|
on_reload = function(_, buf)
|
|
M.enable(true, client_id, buf, opts)
|
|
end,
|
|
})
|
|
|
|
-- Set up autocommands.
|
|
local group = register_completedone(bufnr)
|
|
api.nvim_create_autocmd('LspDetach', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
desc = 'vim.lsp.completion: clean up client on detach',
|
|
callback = function(ev)
|
|
disable_completions(ev.data.client_id, ev.buf)
|
|
end,
|
|
})
|
|
|
|
if opts.autotrigger then
|
|
api.nvim_create_autocmd('InsertCharPre', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
callback = function()
|
|
on_insert_char_pre(buf_handles[bufnr])
|
|
end,
|
|
})
|
|
api.nvim_create_autocmd('InsertLeave', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
callback = on_insert_leave,
|
|
})
|
|
end
|
|
end
|
|
|
|
if not buf_handle.clients[client_id] then
|
|
local client = assert(lsp.get_client_by_id(client_id), 'invalid client ID')
|
|
|
|
-- Add the new client to the buffer's clients.
|
|
buf_handle.clients[client_id] = client
|
|
|
|
-- Add the new client to the clients that should be triggered by its trigger characters.
|
|
--- @type string[]
|
|
local triggers = vim.tbl_get(
|
|
client.server_capabilities,
|
|
'completionProvider',
|
|
'triggerCharacters'
|
|
) or {}
|
|
for _, char in ipairs(triggers) do
|
|
local clients_for_trigger = buf_handle.triggers[char]
|
|
if not clients_for_trigger then
|
|
clients_for_trigger = {}
|
|
buf_handle.triggers[char] = clients_for_trigger
|
|
end
|
|
local client_exists = vim.iter(clients_for_trigger):any(function(c)
|
|
return c.id == client_id
|
|
end)
|
|
if not client_exists then
|
|
table.insert(clients_for_trigger, client)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Enables or disables completions from the given language client in the given
|
|
--- buffer. Effects of enabling completions are:
|
|
---
|
|
--- - Calling |vim.lsp.completion.get()| uses the enabled clients to retrieve completion candidates.
|
|
--- - Selecting a completion item shows a preview popup ("completionItem/resolve") if 'completeopt'
|
|
--- has "popup".
|
|
--- - Accepting a completion item using `<c-y>` applies side effects like expanding snippets,
|
|
--- text edits (e.g. insert import statements) and executing associated commands. This works for
|
|
--- completions triggered via autotrigger, 'omnifunc' or [vim.lsp.completion.get()].
|
|
---
|
|
--- Examples: |lsp-attach| |lsp-completion|
|
|
---
|
|
--- Note: the behavior of `autotrigger=true` is controlled by the LSP `triggerCharacters` field. You
|
|
--- can override it on LspAttach, see |lsp-autocompletion|.
|
|
---
|
|
--- @param enable boolean True to enable, false to disable
|
|
--- @param client_id integer Client ID
|
|
--- @param bufnr integer Buffer handle, or 0 for the current buffer
|
|
--- @param opts? vim.lsp.completion.BufferOpts
|
|
function M.enable(enable, client_id, bufnr, opts)
|
|
bufnr = vim._resolve_bufnr(bufnr)
|
|
|
|
if enable then
|
|
enable_completions(client_id, bufnr, opts or {})
|
|
else
|
|
disable_completions(client_id, bufnr)
|
|
end
|
|
end
|
|
|
|
--- @inlinedoc
|
|
--- @class vim.lsp.completion.get.Opts
|
|
--- @field ctx? lsp.CompletionContext Completion context. Defaults to a trigger kind of `invoked`.
|
|
|
|
--- Triggers LSP completion once in the current buffer, if LSP completion is enabled
|
|
--- (see |lsp-attach| |lsp-completion|).
|
|
---
|
|
--- Used by the default LSP |omnicompletion| provider |vim.lsp.omnifunc()|, thus |i_CTRL-X_CTRL-O|
|
|
--- invokes this in LSP-enabled buffers. Use CTRL-Y to select an item from the completion menu.
|
|
--- |complete_CTRL-Y|
|
|
---
|
|
--- To invoke manually with CTRL-space, use this mapping:
|
|
--- ```lua
|
|
--- -- Use CTRL-space to trigger LSP completion.
|
|
--- -- Use CTRL-Y to select an item. |complete_CTRL-Y|
|
|
--- vim.keymap.set('i', '<c-space>', function()
|
|
--- vim.lsp.completion.get()
|
|
--- end)
|
|
--- ```
|
|
---
|
|
--- @param opts? vim.lsp.completion.get.Opts
|
|
function M.get(opts)
|
|
opts = opts or {}
|
|
local ctx = opts.ctx or { triggerKind = protocol.CompletionTriggerKind.Invoked }
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local clients = (buf_handles[bufnr] or {}).clients or {}
|
|
|
|
trigger(bufnr, clients, ctx)
|
|
end
|
|
|
|
--- Implements 'omnifunc' compatible LSP completion.
|
|
---
|
|
--- @see |complete-functions|
|
|
--- @see |complete-items|
|
|
--- @see |CompleteDone|
|
|
---
|
|
--- @param findstart integer 0 or 1, decides behavior
|
|
--- @param base integer findstart=0, text to match against
|
|
---
|
|
--- @return integer|table Decided by {findstart}:
|
|
--- - findstart=0: column where the completion starts, or -2 or -3
|
|
--- - findstart=1: list of matches (actually just calls |complete()|)
|
|
function M._omnifunc(findstart, base)
|
|
lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base })
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local clients = lsp.get_clients({ bufnr = bufnr, method = 'textDocument/completion' })
|
|
local remaining = #clients
|
|
if remaining == 0 then
|
|
return findstart == 1 and -1 or {}
|
|
end
|
|
|
|
trigger(bufnr, clients, { triggerKind = protocol.CompletionTriggerKind.Invoked })
|
|
|
|
-- Return -2 to signal that we should continue completion so that we can
|
|
-- async complete.
|
|
return -2
|
|
end
|
|
|
|
return M
|