mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 18:55:14 -07:00
Merge pull request #27339 from MariaSolOs/completion
feat(lsp): completion side effects
This commit is contained in:
commit
0bdd602bf9
@ -1604,6 +1604,32 @@ save({lenses}, {bufnr}, {client_id}) *vim.lsp.codelens.save()*
|
|||||||
• {client_id} (`integer`)
|
• {client_id} (`integer`)
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
Lua module: vim.lsp.completion *lsp-completion*
|
||||||
|
|
||||||
|
*vim.lsp.completion.BufferOpts*
|
||||||
|
|
||||||
|
Fields: ~
|
||||||
|
• {autotrigger}? (`boolean`) Whether to trigger completion
|
||||||
|
automatically. Default: false
|
||||||
|
|
||||||
|
|
||||||
|
*vim.lsp.completion.enable()*
|
||||||
|
enable({enable}, {client_id}, {bufnr}, {opts})
|
||||||
|
Enables or disables completions from the given language client in the
|
||||||
|
given buffer.
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {enable} (`boolean`) True to enable, false to disable
|
||||||
|
• {client_id} (`integer`) Client ID
|
||||||
|
• {bufnr} (`integer`) Buffer handle, or 0 for the current buffer
|
||||||
|
• {opts} (`vim.lsp.completion.BufferOpts?`) See
|
||||||
|
|vim.lsp.completion.BufferOpts|.
|
||||||
|
|
||||||
|
trigger() *vim.lsp.completion.trigger()*
|
||||||
|
Trigger LSP completion in the current buffer.
|
||||||
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
Lua module: vim.lsp.inlay_hint *lsp-inlay_hint*
|
Lua module: vim.lsp.inlay_hint *lsp-inlay_hint*
|
||||||
|
|
||||||
|
@ -85,6 +85,12 @@ DEFAULTS
|
|||||||
- |gra| in Normal and Visual mode maps to |vim.lsp.buf.code_action()|
|
- |gra| in Normal and Visual mode maps to |vim.lsp.buf.code_action()|
|
||||||
- CTRL-S in Insert mode maps to |vim.lsp.buf.signature_help()|
|
- CTRL-S in Insert mode maps to |vim.lsp.buf.signature_help()|
|
||||||
|
|
||||||
|
• Snippet:
|
||||||
|
- `<Tab>` in Insert and Select mode maps to |vim.snippet.jump({ direction = 1 })|
|
||||||
|
when a snippet is active and jumpable forwards.
|
||||||
|
- `<S-Tab>` in Insert and Select mode maps to |vim.snippet.jump({ direction = -1 })|
|
||||||
|
when a snippet is active and jumpable backwards.
|
||||||
|
|
||||||
EDITOR
|
EDITOR
|
||||||
|
|
||||||
* On Windows, filename arguments on the command-line prefixed with "~\" or
|
* On Windows, filename arguments on the command-line prefixed with "~\" or
|
||||||
@ -97,7 +103,8 @@ EVENTS
|
|||||||
|
|
||||||
LSP
|
LSP
|
||||||
|
|
||||||
• TODO
|
• Completion side effects (including snippet expansion, execution of commands
|
||||||
|
and application of additional text edits) is now built-in.
|
||||||
|
|
||||||
LUA
|
LUA
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ local validate = vim.validate
|
|||||||
|
|
||||||
local lsp = vim._defer_require('vim.lsp', {
|
local lsp = vim._defer_require('vim.lsp', {
|
||||||
_changetracking = ..., --- @module 'vim.lsp._changetracking'
|
_changetracking = ..., --- @module 'vim.lsp._changetracking'
|
||||||
_completion = ..., --- @module 'vim.lsp._completion'
|
|
||||||
_dynamic = ..., --- @module 'vim.lsp._dynamic'
|
_dynamic = ..., --- @module 'vim.lsp._dynamic'
|
||||||
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
|
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
|
||||||
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
|
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
|
||||||
@ -11,6 +10,7 @@ local lsp = vim._defer_require('vim.lsp', {
|
|||||||
buf = ..., --- @module 'vim.lsp.buf'
|
buf = ..., --- @module 'vim.lsp.buf'
|
||||||
client = ..., --- @module 'vim.lsp.client'
|
client = ..., --- @module 'vim.lsp.client'
|
||||||
codelens = ..., --- @module 'vim.lsp.codelens'
|
codelens = ..., --- @module 'vim.lsp.codelens'
|
||||||
|
completion = ..., --- @module 'vim.lsp.completion'
|
||||||
diagnostic = ..., --- @module 'vim.lsp.diagnostic'
|
diagnostic = ..., --- @module 'vim.lsp.diagnostic'
|
||||||
handlers = ..., --- @module 'vim.lsp.handlers'
|
handlers = ..., --- @module 'vim.lsp.handlers'
|
||||||
inlay_hint = ..., --- @module 'vim.lsp.inlay_hint'
|
inlay_hint = ..., --- @module 'vim.lsp.inlay_hint'
|
||||||
@ -1003,8 +1003,7 @@ end
|
|||||||
--- - findstart=0: column where the completion starts, or -2 or -3
|
--- - findstart=0: column where the completion starts, or -2 or -3
|
||||||
--- - findstart=1: list of matches (actually just calls |complete()|)
|
--- - findstart=1: list of matches (actually just calls |complete()|)
|
||||||
function lsp.omnifunc(findstart, base)
|
function lsp.omnifunc(findstart, base)
|
||||||
log.debug('omnifunc.findstart', { findstart = findstart, base = base })
|
return vim.lsp.completion._omnifunc(findstart, base)
|
||||||
return vim.lsp._completion.omnifunc(findstart, base)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @class vim.lsp.formatexpr.Opts
|
--- @class vim.lsp.formatexpr.Opts
|
||||||
|
@ -1,276 +0,0 @@
|
|||||||
local M = {}
|
|
||||||
local api = vim.api
|
|
||||||
local lsp = vim.lsp
|
|
||||||
local protocol = lsp.protocol
|
|
||||||
local ms = protocol.Methods
|
|
||||||
|
|
||||||
--- @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).
|
|
||||||
--- @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
|
|
||||||
|
|
||||||
---@param input string unparsed snippet
|
|
||||||
---@return string parsed snippet
|
|
||||||
local function parse_snippet(input)
|
|
||||||
local ok, parsed = pcall(function()
|
|
||||||
return vim.lsp._snippet_grammar.parse(input)
|
|
||||||
end)
|
|
||||||
return ok and tostring(parsed) or input
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns text that should be inserted when 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
|
|
||||||
---@return string
|
|
||||||
local function get_completion_word(item)
|
|
||||||
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
|
|
||||||
if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
|
|
||||||
return item.textEdit.newText
|
|
||||||
else
|
|
||||||
return parse_snippet(item.textEdit.newText)
|
|
||||||
end
|
|
||||||
elseif item.insertText ~= nil and item.insertText ~= '' then
|
|
||||||
if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
|
|
||||||
return item.insertText
|
|
||||||
else
|
|
||||||
return parse_snippet(item.insertText)
|
|
||||||
end
|
|
||||||
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
|
|
||||||
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
|
|
||||||
for _, item in ipairs(result.items) do
|
|
||||||
---@diagnostic disable-next-line: param-type-mismatch
|
|
||||||
apply_defaults(item, result.itemDefaults)
|
|
||||||
end
|
|
||||||
return result.items
|
|
||||||
else
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
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
|
|
||||||
---@return table[]
|
|
||||||
---@see complete-items
|
|
||||||
function M._lsp_to_complete_items(result, prefix)
|
|
||||||
local items = get_items(result)
|
|
||||||
if vim.tbl_isempty(items) then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function matches_prefix(item)
|
|
||||||
return vim.startswith(get_completion_word(item), prefix)
|
|
||||||
end
|
|
||||||
|
|
||||||
items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]]
|
|
||||||
table.sort(items, function(a, b)
|
|
||||||
return (a.sortText or a.label) < (b.sortText or b.label)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local matches = {}
|
|
||||||
for _, item in ipairs(items) do
|
|
||||||
local info = ''
|
|
||||||
local documentation = item.documentation
|
|
||||||
if documentation then
|
|
||||||
if type(documentation) == 'string' and documentation ~= '' then
|
|
||||||
info = documentation
|
|
||||||
elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
|
|
||||||
info = documentation.value
|
|
||||||
else
|
|
||||||
vim.notify(
|
|
||||||
('invalid documentation value %s'):format(vim.inspect(documentation)),
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local word = get_completion_word(item)
|
|
||||||
table.insert(matches, {
|
|
||||||
word = word,
|
|
||||||
abbr = item.label,
|
|
||||||
kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
|
|
||||||
menu = item.detail or '',
|
|
||||||
info = #info > 0 and info or nil,
|
|
||||||
icase = 1,
|
|
||||||
dup = 1,
|
|
||||||
empty = 1,
|
|
||||||
user_data = {
|
|
||||||
nvim = {
|
|
||||||
lsp = {
|
|
||||||
completion_item = item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
return matches
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param lnum integer 0-indexed
|
|
||||||
---@param items lsp.CompletionItem[]
|
|
||||||
local function adjust_start_col(lnum, line, items, encoding)
|
|
||||||
local min_start_char = nil
|
|
||||||
for _, item in pairs(items) do
|
|
||||||
if item.textEdit and item.textEdit.range.start.line == lnum then
|
|
||||||
if min_start_char and min_start_char ~= item.textEdit.range.start.character then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
min_start_char = item.textEdit.range.start.character
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if min_start_char then
|
|
||||||
return vim.lsp.util._str_byteindex_enc(line, min_start_char, encoding)
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@private
|
|
||||||
---@param line string line content
|
|
||||||
---@param lnum integer 0-indexed line number
|
|
||||||
---@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 string
|
|
||||||
---@return table[] matches
|
|
||||||
---@return integer? server_start_boundary
|
|
||||||
function M._convert_results(
|
|
||||||
line,
|
|
||||||
lnum,
|
|
||||||
cursor_col,
|
|
||||||
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)
|
|
||||||
return matches, server_start_boundary
|
|
||||||
end
|
|
||||||
|
|
||||||
---@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)
|
|
||||||
assert(base) -- silence luals
|
|
||||||
local bufnr = api.nvim_get_current_buf()
|
|
||||||
local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
|
|
||||||
local remaining = #clients
|
|
||||||
if remaining == 0 then
|
|
||||||
return findstart == 1 and -1 or {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local win = api.nvim_get_current_win()
|
|
||||||
local cursor = api.nvim_win_get_cursor(win)
|
|
||||||
local lnum = cursor[1] - 1
|
|
||||||
local cursor_col = cursor[2]
|
|
||||||
local line = api.nvim_get_current_line()
|
|
||||||
local line_to_cursor = line:sub(1, cursor_col)
|
|
||||||
local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]]
|
|
||||||
local server_start_boundary = nil
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
local function on_done()
|
|
||||||
local mode = api.nvim_get_mode()['mode']
|
|
||||||
if mode == 'i' or mode == 'ic' then
|
|
||||||
vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local util = vim.lsp.util
|
|
||||||
for _, client in ipairs(clients) do
|
|
||||||
local params = util.make_position_params(win, client.offset_encoding)
|
|
||||||
client.request(ms.textDocument_completion, params, function(err, result)
|
|
||||||
if err then
|
|
||||||
vim.lsp.log.warn(err.message)
|
|
||||||
end
|
|
||||||
if result and vim.fn.mode() == 'i' then
|
|
||||||
local matches
|
|
||||||
matches, server_start_boundary = M._convert_results(
|
|
||||||
line,
|
|
||||||
lnum,
|
|
||||||
cursor_col,
|
|
||||||
client_start_boundary,
|
|
||||||
server_start_boundary,
|
|
||||||
result,
|
|
||||||
client.offset_encoding
|
|
||||||
)
|
|
||||||
vim.list_extend(items, matches)
|
|
||||||
end
|
|
||||||
remaining = remaining - 1
|
|
||||||
if remaining == 0 then
|
|
||||||
vim.schedule(on_done)
|
|
||||||
end
|
|
||||||
end, bufnr)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Return -2 to signal that we should continue completion so that we can
|
|
||||||
-- async complete.
|
|
||||||
return -2
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
@ -868,7 +868,8 @@ end
|
|||||||
--- @param command lsp.Command
|
--- @param command lsp.Command
|
||||||
--- @param context? {bufnr: integer}
|
--- @param context? {bufnr: integer}
|
||||||
--- @param handler? lsp.Handler only called if a server command
|
--- @param handler? lsp.Handler only called if a server command
|
||||||
function Client:_exec_cmd(command, context, handler)
|
--- @param on_unsupported? function handler invoked when the command is not supported by the client.
|
||||||
|
function Client:_exec_cmd(command, context, handler, on_unsupported)
|
||||||
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
|
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
|
||||||
context.bufnr = context.bufnr or api.nvim_get_current_buf()
|
context.bufnr = context.bufnr or api.nvim_get_current_buf()
|
||||||
context.client_id = self.id
|
context.client_id = self.id
|
||||||
@ -882,14 +883,18 @@ function Client:_exec_cmd(command, context, handler)
|
|||||||
local command_provider = self.server_capabilities.executeCommandProvider
|
local command_provider = self.server_capabilities.executeCommandProvider
|
||||||
local commands = type(command_provider) == 'table' and command_provider.commands or {}
|
local commands = type(command_provider) == 'table' and command_provider.commands or {}
|
||||||
if not vim.list_contains(commands, cmdname) then
|
if not vim.list_contains(commands, cmdname) then
|
||||||
vim.notify_once(
|
if on_unsupported then
|
||||||
string.format(
|
on_unsupported()
|
||||||
'Language server `%s` does not support command `%s`. This command may require a client extension.',
|
else
|
||||||
self.name,
|
vim.notify_once(
|
||||||
cmdname
|
string.format(
|
||||||
),
|
'Language server `%s` does not support command `%s`. This command may require a client extension.',
|
||||||
vim.log.levels.WARN
|
self.name,
|
||||||
)
|
cmdname
|
||||||
|
),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- Not using command directly to exclude extra properties,
|
-- Not using command directly to exclude extra properties,
|
||||||
|
734
runtime/lua/vim/lsp/completion.lua
Normal file
734
runtime/lua/vim/lsp/completion.lua
Normal file
@ -0,0 +1,734 @@
|
|||||||
|
local M = {}
|
||||||
|
|
||||||
|
local api = vim.api
|
||||||
|
local lsp = vim.lsp
|
||||||
|
local protocol = lsp.protocol
|
||||||
|
local ms = protocol.Methods
|
||||||
|
|
||||||
|
local rtt_ms = 50
|
||||||
|
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[]>
|
||||||
|
|
||||||
|
--- @type table<integer, vim.lsp.completion.BufHandle>
|
||||||
|
local buf_handles = {}
|
||||||
|
|
||||||
|
--- @nodoc
|
||||||
|
--- @class vim.lsp.completion.Context
|
||||||
|
local Context = {
|
||||||
|
cursor = nil, --- @type { [1]: integer, [2]: integer }?
|
||||||
|
last_request_time = nil, --- @type integer?
|
||||||
|
pending_requests = {}, --- @type function[]
|
||||||
|
isIncomplete = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- @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()
|
||||||
|
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: number): number
|
||||||
|
local function exp_avg(window, warmup)
|
||||||
|
local count = 0
|
||||||
|
local sum = 0
|
||||||
|
local value = 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)
|
||||||
|
|
||||||
|
--- @return number
|
||||||
|
local function next_debounce()
|
||||||
|
if not Context.last_request_time then
|
||||||
|
return rtt_ms
|
||||||
|
end
|
||||||
|
|
||||||
|
local ms_since_request = (vim.uv.hrtime() - Context.last_request_time) * ns_to_ms
|
||||||
|
return math.max((ms_since_request - rtt_ms) * -1, 0)
|
||||||
|
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
|
||||||
|
--- @param suffix? string
|
||||||
|
local function apply_snippet(item, suffix)
|
||||||
|
if item.textEdit then
|
||||||
|
vim.snippet.expand(item.textEdit.newText .. suffix)
|
||||||
|
elseif item.insertText then
|
||||||
|
vim.snippet.expand(item.insertText .. suffix)
|
||||||
|
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
|
||||||
|
--- @return string
|
||||||
|
local function get_completion_word(item)
|
||||||
|
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
|
||||||
|
if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
|
||||||
|
return item.textEdit.newText
|
||||||
|
else
|
||||||
|
return parse_snippet(item.textEdit.newText)
|
||||||
|
end
|
||||||
|
elseif item.insertText ~= nil and item.insertText ~= '' then
|
||||||
|
if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
|
||||||
|
return item.insertText
|
||||||
|
else
|
||||||
|
return parse_snippet(item.insertText)
|
||||||
|
end
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
||||||
|
--- |complete-items|.
|
||||||
|
---
|
||||||
|
--- @private
|
||||||
|
--- @param result vim.lsp.CompletionResult Result of `textDocument/completion`
|
||||||
|
--- @param prefix string prefix to filter the completion items
|
||||||
|
--- @param client_id integer? Client ID
|
||||||
|
--- @return table[]
|
||||||
|
--- @see complete-items
|
||||||
|
function M._lsp_to_complete_items(result, prefix, client_id)
|
||||||
|
local items = get_items(result)
|
||||||
|
if vim.tbl_isempty(items) then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matches_prefix(item)
|
||||||
|
return vim.startswith(get_completion_word(item), prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]]
|
||||||
|
table.sort(items, function(a, b)
|
||||||
|
return (a.sortText or a.label) < (b.sortText or b.label)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local matches = {}
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
local info = ''
|
||||||
|
local documentation = item.documentation
|
||||||
|
if documentation then
|
||||||
|
if type(documentation) == 'string' and documentation ~= '' then
|
||||||
|
info = documentation
|
||||||
|
elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
|
||||||
|
info = documentation.value
|
||||||
|
else
|
||||||
|
vim.notify(
|
||||||
|
('invalid documentation value %s'):format(vim.inspect(documentation)),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local word = get_completion_word(item)
|
||||||
|
table.insert(matches, {
|
||||||
|
word = word,
|
||||||
|
abbr = item.label,
|
||||||
|
kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
|
||||||
|
menu = item.detail or '',
|
||||||
|
info = #info > 0 and info or '',
|
||||||
|
icase = 1,
|
||||||
|
dup = 1,
|
||||||
|
empty = 1,
|
||||||
|
user_data = {
|
||||||
|
nvim = {
|
||||||
|
lsp = {
|
||||||
|
completion_item = item,
|
||||||
|
client_id = client_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return matches
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param lnum integer 0-indexed
|
||||||
|
--- @param line string
|
||||||
|
--- @param items lsp.CompletionItem[]
|
||||||
|
--- @param encoding string
|
||||||
|
--- @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 and item.textEdit.range.start.line == lnum then
|
||||||
|
if min_start_char and min_start_char ~= item.textEdit.range.start.character then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
min_start_char = item.textEdit.range.start.character
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if min_start_char then
|
||||||
|
return lsp.util._str_byteindex_enc(line, min_start_char, encoding)
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @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 string
|
||||||
|
--- @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)
|
||||||
|
return matches, server_start_boundary
|
||||||
|
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)
|
||||||
|
vim.lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base })
|
||||||
|
assert(base) -- silence luals
|
||||||
|
local bufnr = api.nvim_get_current_buf()
|
||||||
|
local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
|
||||||
|
local remaining = #clients
|
||||||
|
if remaining == 0 then
|
||||||
|
return findstart == 1 and -1 or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local win = api.nvim_get_current_win()
|
||||||
|
local cursor = api.nvim_win_get_cursor(win)
|
||||||
|
local lnum = cursor[1] - 1
|
||||||
|
local cursor_col = cursor[2]
|
||||||
|
local line = api.nvim_get_current_line()
|
||||||
|
local line_to_cursor = line:sub(1, cursor_col)
|
||||||
|
local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]]
|
||||||
|
local server_start_boundary = nil
|
||||||
|
local items = {}
|
||||||
|
|
||||||
|
local function on_done()
|
||||||
|
local mode = api.nvim_get_mode()['mode']
|
||||||
|
if mode == 'i' or mode == 'ic' then
|
||||||
|
vim.fn.complete((server_start_boundary or client_start_boundary) + 1, items)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local util = vim.lsp.util
|
||||||
|
for _, client in ipairs(clients) do
|
||||||
|
local params = util.make_position_params(win, client.offset_encoding)
|
||||||
|
client.request(ms.textDocument_completion, params, function(err, result)
|
||||||
|
if err then
|
||||||
|
lsp.log.warn(err.message)
|
||||||
|
end
|
||||||
|
if result and vim.fn.mode() == 'i' then
|
||||||
|
local matches
|
||||||
|
matches, server_start_boundary = M._convert_results(
|
||||||
|
line,
|
||||||
|
lnum,
|
||||||
|
cursor_col,
|
||||||
|
client.id,
|
||||||
|
client_start_boundary,
|
||||||
|
server_start_boundary,
|
||||||
|
result,
|
||||||
|
client.offset_encoding
|
||||||
|
)
|
||||||
|
vim.list_extend(items, matches)
|
||||||
|
end
|
||||||
|
remaining = remaining - 1
|
||||||
|
if remaining == 0 then
|
||||||
|
vim.schedule(on_done)
|
||||||
|
end
|
||||||
|
end, bufnr)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return -2 to signal that we should continue completion so that we can
|
||||||
|
-- async complete.
|
||||||
|
return -2
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param clients table<integer, vim.lsp.Client>
|
||||||
|
--- @param bufnr integer
|
||||||
|
--- @param win integer
|
||||||
|
--- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
|
||||||
|
--- @return function # Cancellation function
|
||||||
|
local function request(clients, bufnr, win, 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_id, client in pairs(clients) do
|
||||||
|
local params = lsp.util.make_position_params(win, client.offset_encoding)
|
||||||
|
local ok, request_id = client.request(ms.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 handle vim.lsp.completion.BufHandle
|
||||||
|
local function on_insert_char_pre(handle)
|
||||||
|
if tonumber(vim.fn.pumvisible()) == 1 then
|
||||||
|
if Context.isIncomplete then
|
||||||
|
reset_timer()
|
||||||
|
|
||||||
|
local debounce_ms = next_debounce()
|
||||||
|
if debounce_ms == 0 then
|
||||||
|
vim.schedule(M.trigger)
|
||||||
|
else
|
||||||
|
completion_timer = new_timer()
|
||||||
|
completion_timer:start(debounce_ms, 0, vim.schedule_wrap(M.trigger))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local char = api.nvim_get_vvar('char')
|
||||||
|
if not completion_timer and handle.triggers[char] then
|
||||||
|
completion_timer = assert(vim.uv.new_timer())
|
||||||
|
completion_timer:start(25, 0, function()
|
||||||
|
reset_timer()
|
||||||
|
vim.schedule(M.trigger)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_insert_leave()
|
||||||
|
reset_timer()
|
||||||
|
Context.cursor = nil
|
||||||
|
Context:reset()
|
||||||
|
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 offset_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.
|
||||||
|
local start_char = cursor_col - #completed_item.word
|
||||||
|
local line = api.nvim_buf_get_lines(bufnr, cursor_row, cursor_row + 1, true)[1]
|
||||||
|
api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, #line, { '' })
|
||||||
|
return line:sub(cursor_col + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param suffix? string
|
||||||
|
local function apply_snippet_and_command(suffix)
|
||||||
|
if expand_snippet then
|
||||||
|
apply_snippet(completion_item, suffix)
|
||||||
|
end
|
||||||
|
|
||||||
|
local command = completion_item.command
|
||||||
|
if command then
|
||||||
|
client:_exec_cmd(command, { bufnr = bufnr }, nil, function()
|
||||||
|
vim.lsp.log.warn(
|
||||||
|
string.format(
|
||||||
|
'Language server `%s` does not support command `%s`. This command may require a client extension.',
|
||||||
|
client.name,
|
||||||
|
command.command
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
|
||||||
|
local suffix = clear_word()
|
||||||
|
lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding)
|
||||||
|
apply_snippet_and_command(suffix)
|
||||||
|
elseif resolve_provider and type(completion_item) == 'table' then
|
||||||
|
local changedtick = vim.b[bufnr].changedtick
|
||||||
|
|
||||||
|
--- @param result lsp.CompletionItem
|
||||||
|
client.request(ms.completionItem_resolve, completion_item, function(err, result)
|
||||||
|
if changedtick ~= vim.b[bufnr].changedtick then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local suffix = clear_word()
|
||||||
|
if err then
|
||||||
|
vim.notify_once(err.message, vim.log.levels.WARN)
|
||||||
|
elseif result and result.additionalTextEdits then
|
||||||
|
lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, offset_encoding)
|
||||||
|
if result.command then
|
||||||
|
completion_item.command = result.command
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
apply_snippet_and_command(suffix)
|
||||||
|
end, bufnr)
|
||||||
|
else
|
||||||
|
local suffix = clear_word()
|
||||||
|
apply_snippet_and_command(suffix)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @class vim.lsp.completion.BufferOpts
|
||||||
|
--- @field autotrigger? boolean Whether to trigger completion automatically. Default: false
|
||||||
|
|
||||||
|
--- @param client_id integer
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param opts vim.lsp.completion.BufferOpts
|
||||||
|
local function enable_completions(client_id, bufnr, opts)
|
||||||
|
if not buf_handles[bufnr] then
|
||||||
|
buf_handles[bufnr] = { clients = {}, triggers = {} }
|
||||||
|
|
||||||
|
-- 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 =
|
||||||
|
api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true })
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
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_handles[bufnr].clients[client_id] then
|
||||||
|
local client = lsp.get_client_by_id(client_id)
|
||||||
|
assert(client, 'invalid client ID')
|
||||||
|
|
||||||
|
-- Add the new client to the buffer's clients.
|
||||||
|
buf_handles[bufnr].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_handles[bufnr].triggers[char]
|
||||||
|
if not clients_for_trigger then
|
||||||
|
clients_for_trigger = {}
|
||||||
|
buf_handles[bufnr].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
|
||||||
|
|
||||||
|
--- @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(string.format('vim/lsp/completion-%d', 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
|
||||||
|
|
||||||
|
--- Enables or disables completions from the given language client in the given buffer.
|
||||||
|
---
|
||||||
|
--- @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 = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr
|
||||||
|
|
||||||
|
if enable then
|
||||||
|
enable_completions(client_id, bufnr, opts or {})
|
||||||
|
else
|
||||||
|
disable_completions(client_id, bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Trigger LSP completion in the current buffer.
|
||||||
|
function M.trigger()
|
||||||
|
reset_timer()
|
||||||
|
Context:cancel_pending()
|
||||||
|
|
||||||
|
local win = api.nvim_get_current_win()
|
||||||
|
local bufnr = api.nvim_get_current_buf()
|
||||||
|
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 clients = (buf_handles[bufnr] or {}).clients or {}
|
||||||
|
local word_boundary = vim.fn.match(line_to_cursor, '\\k*$')
|
||||||
|
local start_time = vim.uv.hrtime()
|
||||||
|
Context.last_request_time = start_time
|
||||||
|
|
||||||
|
local cancel_request = request(clients, bufnr, win, 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 row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row
|
||||||
|
local mode = api.nvim_get_mode().mode
|
||||||
|
if row_changed or not (mode == 'i' or mode == 'ic') then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local matches = {}
|
||||||
|
local server_start_boundary --- @type integer?
|
||||||
|
for client_id, response in pairs(responses) do
|
||||||
|
if response.err then
|
||||||
|
vim.notify_once(response.err.message, vim.log.levels.warn)
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = response.result
|
||||||
|
if result then
|
||||||
|
Context.isIncomplete = Context.isIncomplete or result.isIncomplete
|
||||||
|
local client = lsp.get_client_by_id(client_id)
|
||||||
|
local encoding = client and client.offset_encoding or 'utf-16'
|
||||||
|
local client_matches
|
||||||
|
client_matches, server_start_boundary = M._convert_results(
|
||||||
|
line,
|
||||||
|
cursor_row - 1,
|
||||||
|
cursor_col,
|
||||||
|
client_id,
|
||||||
|
word_boundary,
|
||||||
|
nil,
|
||||||
|
result,
|
||||||
|
encoding
|
||||||
|
)
|
||||||
|
vim.list_extend(matches, client_matches)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local start_col = (server_start_boundary or word_boundary) + 1
|
||||||
|
vim.fn.complete(start_col, matches)
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(Context.pending_requests, cancel_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
@ -3,7 +3,7 @@ local protocol = require('vim.lsp.protocol')
|
|||||||
local ms = protocol.Methods
|
local ms = protocol.Methods
|
||||||
local util = require('vim.lsp.util')
|
local util = require('vim.lsp.util')
|
||||||
local api = vim.api
|
local api = vim.api
|
||||||
local completion = require('vim.lsp._completion')
|
local completion = require('vim.lsp.completion')
|
||||||
|
|
||||||
--- @type table<string,lsp.Handler>
|
--- @type table<string,lsp.Handler>
|
||||||
local M = {}
|
local M = {}
|
||||||
|
@ -738,14 +738,16 @@ function protocol.make_client_capabilities()
|
|||||||
completion = {
|
completion = {
|
||||||
dynamicRegistration = false,
|
dynamicRegistration = false,
|
||||||
completionItem = {
|
completionItem = {
|
||||||
-- Until we can actually expand snippet, move cursor and allow for true snippet experience,
|
snippetSupport = true,
|
||||||
-- this should be disabled out of the box.
|
|
||||||
-- However, users can turn this back on if they have a snippet plugin.
|
|
||||||
snippetSupport = false,
|
|
||||||
commitCharactersSupport = false,
|
commitCharactersSupport = false,
|
||||||
preselectSupport = false,
|
preselectSupport = false,
|
||||||
deprecatedSupport = false,
|
deprecatedSupport = false,
|
||||||
documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText },
|
documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText },
|
||||||
|
resolveSupport = {
|
||||||
|
properties = {
|
||||||
|
'additionalTextEdits',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
completionItemKind = {
|
completionItemKind = {
|
||||||
valueSet = get_value_set(constants.CompletionItemKind),
|
valueSet = get_value_set(constants.CompletionItemKind),
|
||||||
|
@ -2,6 +2,8 @@ local G = vim.lsp._snippet_grammar
|
|||||||
local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {})
|
local snippet_group = vim.api.nvim_create_augroup('vim/snippet', {})
|
||||||
local snippet_ns = vim.api.nvim_create_namespace('vim/snippet')
|
local snippet_ns = vim.api.nvim_create_namespace('vim/snippet')
|
||||||
local hl_group = 'SnippetTabstop'
|
local hl_group = 'SnippetTabstop'
|
||||||
|
local jump_forward_key = '<tab>'
|
||||||
|
local jump_backward_key = '<s-tab>'
|
||||||
|
|
||||||
--- Returns the 0-based cursor position.
|
--- Returns the 0-based cursor position.
|
||||||
---
|
---
|
||||||
@ -182,6 +184,8 @@ end
|
|||||||
--- @field extmark_id integer
|
--- @field extmark_id integer
|
||||||
--- @field tabstops table<integer, vim.snippet.Tabstop[]>
|
--- @field tabstops table<integer, vim.snippet.Tabstop[]>
|
||||||
--- @field current_tabstop vim.snippet.Tabstop
|
--- @field current_tabstop vim.snippet.Tabstop
|
||||||
|
--- @field tab_keymaps { i: table<string, any>?, s: table<string, any>? }
|
||||||
|
--- @field shift_tab_keymaps { i: table<string, any>?, s: table<string, any>? }
|
||||||
local Session = {}
|
local Session = {}
|
||||||
|
|
||||||
--- Creates a new snippet session in the current buffer.
|
--- Creates a new snippet session in the current buffer.
|
||||||
@ -197,6 +201,8 @@ function Session.new(bufnr, snippet_extmark, tabstop_data)
|
|||||||
extmark_id = snippet_extmark,
|
extmark_id = snippet_extmark,
|
||||||
tabstops = {},
|
tabstops = {},
|
||||||
current_tabstop = Tabstop.new(0, bufnr, { 0, 0, 0, 0 }),
|
current_tabstop = Tabstop.new(0, bufnr, { 0, 0, 0, 0 }),
|
||||||
|
tab_keymaps = { i = nil, s = nil },
|
||||||
|
shift_tab_keymaps = { i = nil, s = nil },
|
||||||
}, { __index = Session })
|
}, { __index = Session })
|
||||||
|
|
||||||
-- Create the tabstops.
|
-- Create the tabstops.
|
||||||
@ -207,9 +213,64 @@ function Session.new(bufnr, snippet_extmark, tabstop_data)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:set_keymaps()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Sets the snippet navigation keymaps.
|
||||||
|
---
|
||||||
|
--- @package
|
||||||
|
function Session:set_keymaps()
|
||||||
|
local function maparg(key, mode)
|
||||||
|
local map = vim.fn.maparg(key, mode, false, true) --[[ @as table ]]
|
||||||
|
if not vim.tbl_isempty(map) and map.buffer == 1 then
|
||||||
|
return map
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set(jump_key, direction)
|
||||||
|
vim.keymap.set({ 'i', 's' }, jump_key, function()
|
||||||
|
return vim.snippet.active({ direction = direction })
|
||||||
|
and '<cmd>lua vim.snippet.jump(' .. direction .. ')<cr>'
|
||||||
|
or jump_key
|
||||||
|
end, { expr = true, silent = true, buffer = self.bufnr })
|
||||||
|
end
|
||||||
|
|
||||||
|
self.tab_keymaps = {
|
||||||
|
i = maparg(jump_forward_key, 'i'),
|
||||||
|
s = maparg(jump_forward_key, 's'),
|
||||||
|
}
|
||||||
|
self.shift_tab_keymaps = {
|
||||||
|
i = maparg(jump_backward_key, 'i'),
|
||||||
|
s = maparg(jump_backward_key, 's'),
|
||||||
|
}
|
||||||
|
set(jump_forward_key, 1)
|
||||||
|
set(jump_backward_key, -1)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Restores/deletes the keymaps used for snippet navigation.
|
||||||
|
---
|
||||||
|
--- @package
|
||||||
|
function Session:restore_keymaps()
|
||||||
|
local function restore(keymap, lhs, mode)
|
||||||
|
if keymap then
|
||||||
|
vim.api.nvim_buf_call(self.bufnr, function()
|
||||||
|
vim.fn.mapset(keymap)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
vim.api.nvim_buf_del_keymap(self.bufnr, mode, lhs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
restore(self.tab_keymaps.i, jump_forward_key, 'i')
|
||||||
|
restore(self.tab_keymaps.s, jump_forward_key, 's')
|
||||||
|
restore(self.shift_tab_keymaps.i, jump_backward_key, 'i')
|
||||||
|
restore(self.shift_tab_keymaps.s, jump_backward_key, 's')
|
||||||
|
end
|
||||||
|
|
||||||
--- Returns the destination tabstop index when jumping in the given direction.
|
--- Returns the destination tabstop index when jumping in the given direction.
|
||||||
---
|
---
|
||||||
--- @package
|
--- @package
|
||||||
@ -628,6 +689,8 @@ function M.stop()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M._session:restore_keymaps()
|
||||||
|
|
||||||
vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
|
vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
|
||||||
vim.api.nvim_buf_clear_namespace(M._session.bufnr, snippet_ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(M._session.bufnr, snippet_ns, 0, -1)
|
||||||
|
|
||||||
|
@ -273,6 +273,7 @@ local config = {
|
|||||||
'buf.lua',
|
'buf.lua',
|
||||||
'diagnostic.lua',
|
'diagnostic.lua',
|
||||||
'codelens.lua',
|
'codelens.lua',
|
||||||
|
'completion.lua',
|
||||||
'inlay_hint.lua',
|
'inlay_hint.lua',
|
||||||
'tagfunc.lua',
|
'tagfunc.lua',
|
||||||
'semantic_tokens.lua',
|
'semantic_tokens.lua',
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
---@diagnostic disable: no-unknown
|
||||||
|
|
||||||
local t = require('test.testutil')
|
local t = require('test.testutil')
|
||||||
local n = require('test.functional.testnvim')()
|
local n = require('test.functional.testnvim')()
|
||||||
|
|
||||||
@ -16,11 +18,6 @@ local retry = t.retry
|
|||||||
describe('vim.snippet', function()
|
describe('vim.snippet', function()
|
||||||
before_each(function()
|
before_each(function()
|
||||||
clear()
|
clear()
|
||||||
|
|
||||||
exec_lua([[
|
|
||||||
vim.keymap.set({ 'i', 's' }, '<Tab>', function() vim.snippet.jump(1) end, { buffer = true })
|
|
||||||
vim.keymap.set({ 'i', 's' }, '<S-Tab>', function() vim.snippet.jump(-1) end, { buffer = true })
|
|
||||||
]])
|
|
||||||
end)
|
end)
|
||||||
after_each(clear)
|
after_each(clear)
|
||||||
|
|
||||||
@ -286,4 +283,24 @@ describe('vim.snippet', function()
|
|||||||
]]
|
]]
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('restores snippet navigation keymaps', function()
|
||||||
|
-- Create a buffer keymap in insert mode that deletes all lines.
|
||||||
|
local curbuf = api.nvim_get_current_buf()
|
||||||
|
exec_lua('vim.api.nvim_buf_set_keymap(..., "i", "<Tab>", "<cmd>normal ggdG<cr>", {})', curbuf)
|
||||||
|
|
||||||
|
test_expand_success({ 'var $1 = $2' }, { 'var = ' })
|
||||||
|
|
||||||
|
-- While the snippet is active, <Tab> should navigate between tabstops.
|
||||||
|
feed('x')
|
||||||
|
poke_eventloop()
|
||||||
|
feed('<Tab>0')
|
||||||
|
eq({ 'var x = 0' }, buf_lines(0))
|
||||||
|
|
||||||
|
exec_lua('vim.snippet.stop()')
|
||||||
|
|
||||||
|
-- After exiting the snippet, the buffer keymap should be restored.
|
||||||
|
feed('<Esc>O<cr><Tab>')
|
||||||
|
eq({ '' }, buf_lines(0))
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
---@diagnostic disable: no-unknown
|
---@diagnostic disable: no-unknown
|
||||||
local t = require('test.testutil')
|
local t = require('test.testutil')
|
||||||
|
local t_lsp = require('test.functional.plugin.lsp.testutil')
|
||||||
local n = require('test.functional.testnvim')()
|
local n = require('test.functional.testnvim')()
|
||||||
|
|
||||||
|
local clear = n.clear
|
||||||
local eq = t.eq
|
local eq = t.eq
|
||||||
|
local neq = t.neq
|
||||||
local exec_lua = n.exec_lua
|
local exec_lua = n.exec_lua
|
||||||
|
local feed = n.feed
|
||||||
|
local retry = t.retry
|
||||||
|
|
||||||
|
local create_server_definition = t_lsp.create_server_definition
|
||||||
|
|
||||||
--- Convert completion results.
|
--- Convert completion results.
|
||||||
---
|
---
|
||||||
@ -21,10 +28,11 @@ local function complete(line, candidates, lnum)
|
|||||||
local line, cursor_col, lnum, result = ...
|
local line, cursor_col, lnum, result = ...
|
||||||
local line_to_cursor = line:sub(1, cursor_col)
|
local line_to_cursor = line:sub(1, cursor_col)
|
||||||
local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$')
|
local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$')
|
||||||
local items, server_start_boundary = require("vim.lsp._completion")._convert_results(
|
local items, server_start_boundary = require("vim.lsp.completion")._convert_results(
|
||||||
line,
|
line,
|
||||||
lnum,
|
lnum,
|
||||||
cursor_col,
|
cursor_col,
|
||||||
|
1,
|
||||||
client_start_boundary,
|
client_start_boundary,
|
||||||
nil,
|
nil,
|
||||||
result,
|
result,
|
||||||
@ -42,7 +50,7 @@ local function complete(line, candidates, lnum)
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe('vim.lsp._completion', function()
|
describe('vim.lsp.completion: item conversion', function()
|
||||||
before_each(n.clear)
|
before_each(n.clear)
|
||||||
|
|
||||||
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
||||||
@ -159,6 +167,7 @@ describe('vim.lsp._completion', function()
|
|||||||
end, result.items)
|
end, result.items)
|
||||||
eq(expected, result)
|
eq(expected, result)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('uses correct start boundary', function()
|
it('uses correct start boundary', function()
|
||||||
local completion_list = {
|
local completion_list = {
|
||||||
isIncomplete = false,
|
isIncomplete = false,
|
||||||
@ -186,6 +195,7 @@ describe('vim.lsp._completion', function()
|
|||||||
dup = 1,
|
dup = 1,
|
||||||
empty = 1,
|
empty = 1,
|
||||||
icase = 1,
|
icase = 1,
|
||||||
|
info = '',
|
||||||
kind = 'Module',
|
kind = 'Module',
|
||||||
menu = '',
|
menu = '',
|
||||||
word = 'this_thread',
|
word = 'this_thread',
|
||||||
@ -240,6 +250,7 @@ describe('vim.lsp._completion', function()
|
|||||||
dup = 1,
|
dup = 1,
|
||||||
empty = 1,
|
empty = 1,
|
||||||
icase = 1,
|
icase = 1,
|
||||||
|
info = '',
|
||||||
kind = 'Module',
|
kind = 'Module',
|
||||||
menu = '',
|
menu = '',
|
||||||
word = 'this_thread',
|
word = 'this_thread',
|
||||||
@ -278,4 +289,224 @@ describe('vim.lsp._completion', function()
|
|||||||
eq('item-property-has-priority', item.data)
|
eq('item-property-has-priority', item.data)
|
||||||
eq({ line = 1, character = 1 }, item.textEdit.range.start)
|
eq({ line = 1, character = 1 }, item.textEdit.range.start)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it(
|
||||||
|
'uses insertText as textEdit.newText if there are editRange defaults but no textEditText',
|
||||||
|
function()
|
||||||
|
--- @type lsp.CompletionList
|
||||||
|
local completion_list = {
|
||||||
|
isIncomplete = false,
|
||||||
|
itemDefaults = {
|
||||||
|
editRange = {
|
||||||
|
start = { line = 1, character = 1 },
|
||||||
|
['end'] = { line = 1, character = 4 },
|
||||||
|
},
|
||||||
|
insertTextFormat = 2,
|
||||||
|
data = 'foobar',
|
||||||
|
},
|
||||||
|
items = {
|
||||||
|
{
|
||||||
|
insertText = 'the-insertText',
|
||||||
|
label = 'hello',
|
||||||
|
data = 'item-property-has-priority',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local result = complete('|', completion_list)
|
||||||
|
eq(1, #result.items)
|
||||||
|
local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText
|
||||||
|
eq('the-insertText', text)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('vim.lsp.completion: protocol', function()
|
||||||
|
before_each(function()
|
||||||
|
clear()
|
||||||
|
exec_lua(create_server_definition)
|
||||||
|
exec_lua([[
|
||||||
|
_G.capture = {}
|
||||||
|
vim.fn.complete = function(col, matches)
|
||||||
|
_G.capture.col = col
|
||||||
|
_G.capture.matches = matches
|
||||||
|
end
|
||||||
|
]])
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(clear)
|
||||||
|
|
||||||
|
--- @param completion_result lsp.CompletionList
|
||||||
|
--- @return integer
|
||||||
|
local function create_server(completion_result)
|
||||||
|
return exec_lua(
|
||||||
|
[[
|
||||||
|
local result = ...
|
||||||
|
local server = _create_server({
|
||||||
|
capabilities = {
|
||||||
|
completionProvider = {
|
||||||
|
triggerCharacters = { '.' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handlers = {
|
||||||
|
['textDocument/completion'] = function(_, _, callback)
|
||||||
|
callback(nil, result)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
vim.api.nvim_win_set_buf(0, bufnr)
|
||||||
|
return vim.lsp.start({ name = 'dummy', cmd = server.cmd, on_attach = function(client, bufnr)
|
||||||
|
vim.lsp.completion.enable(true, client.id, bufnr)
|
||||||
|
end})
|
||||||
|
]],
|
||||||
|
completion_result
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assert_matches(fn)
|
||||||
|
retry(nil, nil, function()
|
||||||
|
fn(exec_lua('return _G.capture.matches'))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param pos { [1]: integer, [2]: integer }
|
||||||
|
local function trigger_at_pos(pos)
|
||||||
|
exec_lua(
|
||||||
|
[[
|
||||||
|
local win = vim.api.nvim_get_current_win()
|
||||||
|
vim.api.nvim_win_set_cursor(win, ...)
|
||||||
|
vim.lsp.completion.trigger()
|
||||||
|
]],
|
||||||
|
pos
|
||||||
|
)
|
||||||
|
|
||||||
|
retry(nil, nil, function()
|
||||||
|
neq(nil, exec_lua('return _G.capture.col'))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
it('fetches completions and shows them using complete on trigger', function()
|
||||||
|
create_server({
|
||||||
|
isIncomplete = false,
|
||||||
|
items = {
|
||||||
|
{
|
||||||
|
label = 'hello',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
feed('ih')
|
||||||
|
trigger_at_pos({ 1, 1 })
|
||||||
|
|
||||||
|
assert_matches(function(matches)
|
||||||
|
eq({
|
||||||
|
{
|
||||||
|
abbr = 'hello',
|
||||||
|
dup = 1,
|
||||||
|
empty = 1,
|
||||||
|
icase = 1,
|
||||||
|
info = '',
|
||||||
|
kind = 'Unknown',
|
||||||
|
menu = '',
|
||||||
|
user_data = {
|
||||||
|
nvim = {
|
||||||
|
lsp = {
|
||||||
|
client_id = 1,
|
||||||
|
completion_item = {
|
||||||
|
label = 'hello',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
word = 'hello',
|
||||||
|
},
|
||||||
|
}, matches)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('merges results from multiple clients', function()
|
||||||
|
create_server({
|
||||||
|
isIncomplete = false,
|
||||||
|
items = {
|
||||||
|
{
|
||||||
|
label = 'hello',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
create_server({
|
||||||
|
isIncomplete = false,
|
||||||
|
items = {
|
||||||
|
{
|
||||||
|
label = 'hallo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
feed('ih')
|
||||||
|
trigger_at_pos({ 1, 1 })
|
||||||
|
|
||||||
|
assert_matches(function(matches)
|
||||||
|
eq(2, #matches)
|
||||||
|
eq('hello', matches[1].word)
|
||||||
|
eq('hallo', matches[2].word)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('executes commands', function()
|
||||||
|
local completion_list = {
|
||||||
|
isIncomplete = false,
|
||||||
|
items = {
|
||||||
|
{
|
||||||
|
label = 'hello',
|
||||||
|
command = {
|
||||||
|
arguments = { '1', '0' },
|
||||||
|
command = 'dummy',
|
||||||
|
title = '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local client_id = create_server(completion_list)
|
||||||
|
|
||||||
|
exec_lua(
|
||||||
|
[[
|
||||||
|
_G.called = false
|
||||||
|
local client = vim.lsp.get_client_by_id(...)
|
||||||
|
client.commands.dummy = function ()
|
||||||
|
_G.called = true
|
||||||
|
end
|
||||||
|
]],
|
||||||
|
client_id
|
||||||
|
)
|
||||||
|
|
||||||
|
feed('ih')
|
||||||
|
trigger_at_pos({ 1, 1 })
|
||||||
|
|
||||||
|
exec_lua(
|
||||||
|
[[
|
||||||
|
local client_id, item = ...
|
||||||
|
vim.v.completed_item = {
|
||||||
|
user_data = {
|
||||||
|
nvim = {
|
||||||
|
lsp = {
|
||||||
|
client_id = client_id,
|
||||||
|
completion_item = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]],
|
||||||
|
client_id,
|
||||||
|
completion_list.items[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
feed('<C-x><C-o><C-y>')
|
||||||
|
|
||||||
|
assert_matches(function(matches)
|
||||||
|
eq(1, #matches)
|
||||||
|
eq('hello', matches[1].word)
|
||||||
|
eq(true, exec_lua('return _G.called'))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
Loading…
Reference in New Issue
Block a user