8000 mini.nvim/lua/mini/completion.lua at main · vandalt/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
2069 lines (1793 loc) · 88.7 KB

completion.lua

File metadata and controls

2069 lines (1793 loc) · 88.7 KB
-- - If timer is activated, try these sources of item info:
-- - 'info' field of completion item (see `:h complete-items`).
-- - 'documentation' field of LSP's previously returned result.
-- - 'documentation' field in result of asynchronous
-- 'completeItem/resolve' LSP request.
-- - If info doesn't consist only from whitespace, show floating window
-- with its content. Its dimensions and position are computed based on
-- current state of Neovim's data and content itself (which will be
-- displayed wrapped with `linebreak` option).
-- - Signature help (similar to item info):
-- - On `CursorMovedI` start auto signature (if there is any active LSP
-- client) with similar to completion timer pattern. Better event might
-- be `InsertCharPre` but there are issues with 'autopair-type' plugins.
-- Update immediately if already shown or after delay if character to the
-- left is signature help trigger (after delay has passed).
-- - If timer is activated, send 'textDocument/signatureHelp' request to
-- all LSP clients. On callback, process their results. Window is opened
-- if not already with the same text (its characteristics are computed
-- similar to item info). For every LSP client it shows only active
-- signature (in case there are many). If LSP response has data about
-- active parameter, it is highlighted with
-- `MiniCompletionActiveParameter` highlight group.
-- Module definition ==========================================================
local MiniCompletion = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniCompletion.config|.
---
---@usage >lua
--- require('mini.completion').setup() -- use default config
--- -- OR
--- require('mini.completion').setup({}) -- replace {} with your config table
--- <
MiniCompletion.setup = function(config)
-- Export module
_G.MiniCompletion = MiniCompletion
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
-- Create default highlighting
H.create_default_hl()
end
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniCompletion.config = {
-- Delay (debounce type, in ms) between certain Neovim event and action.
-- This can be used to (virtually) disable certain automatic actions by
-- setting very high delay time (like 10^7).
delay = { completion = 100, info = 100, signature = 50 },
-- Configuration for action windows:
-- - `height` and `width` are maximum dimensions.
-- - `border` defines border (as in `nvim_open_win()`; default "single").
window = {
info = { height = 25, width = 80, border = nil },
signature = { height = 25, width = 80, border = nil },
},
-- Way of how module does LSP completion
lsp_completion = {
-- `source_func` should be one of 'completefunc' or 'omnifunc'.
source_func = 'completefunc',
-- `auto_setup` should be boolean indicating if LSP completion is set up
-- on every `BufEnter` event.
auto_setup = true,
-- A function which takes LSP 'textDocument/completion' response items
-- (each with `client_id` field for item's server) and word to complete.
-- Output should be a table of the same nature as input. Common use case
-- is custom filter/sort. Default: `default_process_items`
process_items = nil,
-- A function which takes a snippet as string and inserts it at cursor.
-- Default: `default_snippet_insert` which tries to use 'mini.snippets'
-- and falls back to `vim.snippet.expand`.
snippet_insert = nil,
},
-- Fallback action as function/string. Executed in Insert mode.
-- To use built-in completion (`:h ins-completion`), set its mapping as
-- string. Example: set '<C-x><C-l>' for 'whole lines' completion.
fallback_action = '<C-n>',
-- Module mappings. Use `''` (empty string) to disable one. Some of them
-- might conflict with system mappings.
mappings = {
-- Force two-step/fallback completions
force_twostep = '<C-Space>',
force_fallback = '<A-Space>',
-- Scroll info/signature window down/up. When overriding, check for
-- conflicts with built-in keys for popup menu (like `<C-u>`/`<C-o>`
-- for 'completefunc'/'omnifunc' source function; or `<C-n>`/`<C-p>`).
scroll_down = '<C-f>',
scroll_up = '<C-b>',
},
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Run two-stage completion
---
---@param fallback boolean|nil Whether to use fallback completion. Default: `true`.
---@param force boolean|nil Whether to force update of completion popup.
--- Default: `true`.
MiniCompletion.complete_twostage = function(fallback, force)
if H.is_disabled() then return end
if fallback == nil then fallback = true end
if force == nil then force = true end
H.stop_completion()
H.completion.fallback, H.completion.force = fallback, force
H.trigger_twostep()
end
--- Run fallback completion
MiniCompletion.complete_fallback = function()
if H.is_disabled() then return end
H.stop_completion()
H.completion.fallback, H.completion.force = true, true
H.trigger_fallback()
end
--- Scroll in info/signature window
---
--- Designed to be used in |:map-<expr>|.
--- Scrolling is done as if |CTRL-F| and |CTRL-B| is pressed inside target window.
--- Used in default `config.mappings.scroll_xxx` mappings.
---
---@param direction string One of `"down"` or `"up"`.
---
---@return boolean Whether scroll is scheduled to be done.
MiniCompletion.scroll = function(direction)
if not (direction == 'down' or direction == 'up') then H.error('`direction` should be one of "up" or "down"') end
local win_id = H.is_valid_win(H.info.win_id) and H.info.win_id
or (H.is_valid_win(H.signature.win_id) and H.signature.win_id or nil)
if win_id == nil then return false end
-- Schedule execution as scrolling is not allowed in expression mappings
local key = direction == 'down' and '\6' or '\2'
vim.schedule(function()
if not H.is_valid_win(win_id) then return end
vim.api.nvim_win_call(win_id, function() vim.cmd('noautocmd normal! ' .. key) end)
end)
return true
end
--- Stop actions
---
--- This stops currently active (because of module delay or LSP answer delay)
--- actions.
---
--- Designed to be used with |autocmd|. No need to use it directly, everything
--- is setup in |MiniCompletion.setup()|.
---
---@param actions table|nil Array containing any of `'completion'`, `'info'`, or
--- `'signature'` strings. Default: array containing all of them.
MiniCompletion.stop = function(actions)
actions = actions or { 'completion', 'info', 'signature' }
for _, n in ipairs(actions) do
H.stop_actions[n]()
end
end
--- Module's |complete-functions|
---
--- This is the main function which enables two-stage completion. It should be
--- set as one of |'completefunc'| or |'omnifunc'|.
---
--- No need to use it directly, everything is setup in |MiniCompletion.setup()|.
MiniCompletion.completefunc_lsp = function(findstart, base)
-- Early return
if not H.has_lsp_clients('completionProvider') or H.completion.lsp.status == 'sent' then
return findstart == 1 and -3 or {}
end
-- NOTE: having code for request inside this function enables its use
-- directly with `<C-x><...>` and as a reaction to `<BS>`.
if H.completion.lsp.status ~= 'received' then
-- NOTE: it is CRUCIAL to make LSP request on the first call to
-- 'complete-function' (as in Vim's help). This is due to the fact that
-- cursor line and position are different on the first and second calls to
-- 'complete-function'. For example, when calling this function at the end
-- of the line ' he', cursor position on the first call will be
-- (<linenum>, 4) and line will be ' he' but on the second call -
-- (<linenum>, 2) and ' ' (because 2 is a column of completion start).
--
-- This request is not executed on second call because it returns `-3` on
-- first call (which means cancel and leave completion mode).
H.make_completion_request()
-- End completion and wait for LSP callback to re-trigger this
return findstart == 1 and -3 or {}
else
if findstart == 1 then
local from, to = H.get_completion_range(H.completion.lsp.result)
-- Cache initial completion state to revert to it when inserting snippet
-- NOTE: Track only length of base for performance, since this is enough
H.completion.init_base = { lnum = from[1], col = from[2], length = math.max(to[2] - from[2], 0) }
return from[2]
end
local is_incomplete = false
local all_items = H.process_lsp_response(H.completion.lsp.result, function(response, client_id)
is_incomplete = is_incomplete or (response.isIncomplete == true)
-- Response can be `CompletionList` with 'items' field plus their
-- defaults or `CompletionItem[]`
local items = H.table_get(response, { 'items' }) or response
if type(items) ~= 'table' then return {} end
items = H.apply_item_defaults(items, response.itemDefaults)
for _, item in ipairs(items) do
item.client_id = client_id
end
return items
end)
-- Process items
local process_items = H.get_config().lsp_completion.process_items or MiniCompletion.default_process_items
all_items = process_items(all_items, base)
local candidates = H.lsp_completion_response_items_to_complete_items(all_items)
H.completion.lsp.status = 'done'
H.completion.lsp.is_incomplete = is_incomplete
-- Maybe trigger fallback action
if vim.tbl_isempty(candidates) and H.completion.fallback then return H.trigger_fallback() end
-- Track from which source is current popup
H.completion.source = 'lsp'
return candidates
end
end
--- Default processing of LSP items
---
--- Steps:
--- - Filter and sort items according to supplied method.
--- - Arrange items further by completion item kind according to their priority.
--- - Add `MiniCompletionDeprecated` <abbr_hlgroup> if item is marked as deprecated.
--- - If |mini.icons| is enabled, add <kind_hlgroup> based on the "lsp" category.
---
--- Example of forcing fuzzy matching, filtering out `Text` items, and putting
--- `Snippet` items last: >lua
---
--- local kind_priority = { Text = -1, Snippet = 99 }
--- local opts = { filtersort = 'fuzzy', kind_priority = kind_priority }
--- local process_items = function(items, base)
--- return MiniCompletion.default_process_items(items, base, opts)
--- end
--- require('mini.completion').setup({
--- lsp_completion = { process_items = process_items },
--- })
--- <
---@param items table Array of items from LSP response.
---@param base string Base for which completion is done. See |complete-functions|.
---@param opts table|nil Options. Possible fields:
--- - <filtersort> `(string|function)` - method of filtering and sorting items.
--- If string, should be one of the following:
--- - `'prefix'` - filter out items not starting with `base`, sort according
--- to LSP specification. Use `filterText` and `sortText` respectively with
--- fallback to `label`.
--- - `'fuzzy'` - filter and sort with |matchfuzzy()| using `filterText`.
--- - `'none'` - no filter and no sort.
--- If callable, should take `items` and `base` arguments and return items array.
--- Default: `'fuzzy'` if |'completeopt'| contains "fuzzy", `'prefix'` otherwise.
--- - <kind_priority> `(table)` - map of completion item kinds (like `Variable`,
--- `Snippet`; see string keys of `vim.lsp.protocol.CompletionItemKind`) to
--- their numerical priority. It will be used after applying <filtersort> to
--- arrange by completion item kind: items with negative priority kinds will
--- be filtered out, the rest are sorted by decreasing priority (preserving
--- order in case of same priority).
--- Priorities can be any number, only matters how they compare to each other.
--- Value 100 is used for missing kinds (i.e. not all can be supplied).
--- Default: `{}` (all equal priority).
---
---@return table Array of processed items from LSP response.
MiniCompletion.default_process_items = function(items, base, opts)
opts = opts or {}
-- Filter+sort (important with frequent `isIncomplete`)
local fs = opts.filtersort or (vim.o.completeopt:find('fuzzy') ~= nil and 'fuzzy' or 'prefix')
if type(fs) == 'string' then fs = H.filtersort_methods[fs] end
if not vim.is_callable(fs) then H.error('`filtersort` should be callable or one of "prefix", "fuzzy", "none"') end
local res = fs(items, base)
-- Arrange by kind
if opts.kind_priority ~= nil then res = H.lsp_arrange_by_kind(res, opts.kind_priority) end
-- Add custom highlighting
local add_abbr_hlgroup = H.make_add_abbr_hlgroup()
local add_kind_hlgroup = H.make_add_kind_hlgroup()
for _, item in ipairs(res) do
add_abbr_hlgroup(item)
add_kind_hlgroup(item)
end
return res
end
--- Default snippet insert
---
--- Order of preference:
--- - Use |mini.snippets| if set up (i.e. after `require('mini.snippets').setup()`).
--- - Fall back to |vim.snippet.expand()|.
---
--- After snippet is inserted, user is expected to navigate/jump between dedicated
--- places (tabstops) to adjust inserted text as needed:
--- - |mini.snippets| by default uses <C-l> / <C-h> to jump to next/previous tabstop.
--- Can be adjusted in `mappings` of |MiniSnippets.config|.
--- - |vim.snippet| on Neovim=0.10 requires manually created mappings for jumping
--- between tabstops (see |vim.snippet.jump()|). Neovim>=0.11 sets them up
--- automatically to <Tab> / <S-Tab> (if not overridden by user).
---
--- End session by navigating all the way to the last tabstop. In |mini.snippets|:
--- - Also make any text edit or exit Insert mode to end the session. This allows
--- smoother navigation to previous tabstops in case of a lately spotted typo.
--- - Press `<C-c>` to force session stop.
---
---@param snippet string Snippet body to insert at cursor.
---
---@seealso - |MiniSnippets-session| if |mini.snippets| is set up.
--- - |vim.snippet| for Neovim's built-in snippet engine.
MiniCompletion.default_snippet_insert = function(snippet)
if _G.MiniSnippets then
local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
return insert({ body = snippet })
end
vim.snippet.expand(snippet)
end
--- Get client LSP capabilities
---
--- Possible usages:
--- - On Neovim>=0.11 via |vim.lsp.config()|: >lua
---
--- vim.lsp.config('*', {capabilities = MiniCompletion.get_lsp_capabilities()})
--- <
--- - Together with |vim.lsp.protocol.make_client_capabilities()| to get the full
--- client capabilities (use |vim.tbl_deep_extend()| to merge tables).
---
--- - Manually execute `:=MiniCompletion.get_lsp_capabilities()` to see the info.
---
--- Notes:
--- - It declares completion resolve support for `'additionalTextEdits'` (usually
--- used for something like auto-import feature), as it is usually a more robust
--- choice for various LSP servers. As a consequence, this requires selecting
--- completion item and waiting for `config.delay.info` milliseconds plus server
--- response time (i.e. until information window shows relevant text).
--- To not have to wait after an item selection and if the server handles absent
--- `'additionalTextEdits'` well, set `opts.resolve_additional_text_edits = false`.
---
---@param opts table|nil Options. Possible fields:
--- - <resolve_additional_text_edits> `(boolean)` - whether to declare
--- `'additionalTextEdits'` as possible to resolve in `'completionitem/resolve'`
--- request. See above "Notes" section.
--- Default: `true`.
---
---@return table Data about LSP capabilities supported by |mini.completion|. Has same
--- structure as relevant parts of |vim.lsp.protocol.make_client_capabilities()|.
---
---@seealso Structures of `completionClientCapabilities` and `signatureHelpClientCapabilities`
--- at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification
MiniCompletion.get_lsp_capabilities = function(opts)
opts = vim.tbl_extend('force', { resolve_additional_text_edits = true }, opts or {})
local resolve_support = { 'detail', 'documentation' }
if opts.resolve_additional_text_edits then table.insert(resolve_support, 1, 'additionalTextEdits') end
local tag_valueset = vim.fn.has('nvim-0.11') == 1 and { vim.lsp.protocol.CompletionTag.Deprecated } or {}
return {
textDocument = {
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionClientCapabilities
completion = {
dynamicRegistration = false,
completionItem = {
snippetSupport = true,
commitCharactersSupport = false,
documentationFormat = { 'markdown', 'plaintext' },
deprecatedSupport = true,
preselectSupport = false,
tagSupport = { valueSet = tag_valueset },
insertReplaceSupport = true,
resolveSupport = { properties = resolve_support },
insertTextModeSupport = { valueSet = { 1 } },
labelDetailsSupport = true,
},
completionItemKind = {
valueSet = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 },
},
contextSupport = true,
insertTextMode = 1,
completionList = {
itemDefaults = { 'commitCharacters', 'editRange', 'insertTextFormat', 'insertTextMode', 'data' },
},
},
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureHelpClientCapabilities
signatureHelp = {
dynamicRegistration = false,
signatureInformation = {
documentationFormat = { 'markdown', 'plaintext' },
parameterInformation = {
labelOffsetSupport = true,
},
activeParameterSupport = true,
},
contextSupport = false,
},
},
}
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniCompletion.config)
-- Track Insert mode changes
H.text_changed_id = 0
-- Namespace for highlighting
H.ns_id = vim.api.nvim_create_namespace('MiniCompletion')
-- Commonly used key sequences
H.keys = {
completefunc = vim.api.nvim_replace_termcodes('<C-x><C-u>', true, false, true),
omnifunc = vim.api.nvim_replace_termcodes('<C-x><C-o>', true, false, true),
ctrl_n = vim.api.nvim_replace_termcodes('<C-g><C-g><C-n>', true, false, true),
}
-- Flags for whether there is support for dedicated options
H.has_no_winborder = vim.fn.has('nvim-0.11') == 0
H.has_pumborder = vim.fn.exists('+pumborder') == 1 -- Neovim>=0.12
-- Caches for different actions -----------------------------------------------
-- Field `lsp` is a table describing state of all used LSP requests. It has the
-- following structure:
-- - id: identifier (consecutive numbers).
-- - status: one of 'sent', 'received', 'done', 'canceled'
-- - is_incomplete: whether request was incomplete and require recomputing
-- - result: result of request.
-- - cancel_fun: function which cancels current request.
-- Cache for completion
H.completion = {
fallback = true,
force = false,
source = nil,
text_changed_id = 0,
timer = vim.loop.new_timer(),
lsp = { id = 0, status = nil, is_incomplete = false, result = nil, resolved = {}, cancel_fun = nil, context = nil },
init_base = { lnum = nil, col = nil, length = nil },
}
-- Cache for completion item info
H.info = {
bufnr = nil,
event = nil,
id = 0,
timer = vim.loop.new_timer(),
win_id = nil,
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}
-- Cache for signature help
H.signature = {
bufnr = nil,
text = nil,
timer = vim.loop.new_timer(),
win_id = nil,
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
H.check_type('config', config, 'table', true)
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
H.check_type('delay', config.delay, 'table')
H.check_type('window', config.window, 'table')
H.check_type('lsp_completion', config.lsp_completion, 'table')
if not (type(config.fallback_action) == 'function' or type(config.fallback_action) == 'string') then
H.error('`fallback_action` should be function or string, not ' .. type(config.fallback_action))
end
H.check_type('mappings', config.mappings, 'table')
H.check_type('delay.completion', config.delay.completion, 'number')
H.check_type('delay.info', config.delay.info, 'number')
H.check_type('delay.signature', config.delay.signature, 'number')
H.check_type('window.info', config.window.info, 'table')
H.check_type('window.signature', config.window.signature, 'table')
if not (config.lsp_completion.source_func == 'completefunc' or config.lsp_completion.source_func == 'omnifunc') then
H.error('`lsp_completion.source_func` should be one of "completefunc" or "omnifunc"')
end
H.check_type('lsp_completion.auto_setup', config.lsp_completion.auto_setup, 'boolean')
H.check_type('lsp_completion.process_items', config.lsp_completion.process_items, 'callable', true)
H.check_type('lsp_completion.snippet_insert', config.lsp_completion.snippet_insert, 'callable', true)
H.check_type('mappings.force_twostep', config.mappings.force_twostep, 'string')
H.check_type('mappings.force_fallback', config.mappings.force_fallback, 'string')
H.check_type('mappings.scroll_down', config.mappings.scroll_down, 'string')
H.check_type('mappings.scroll_up', config.mappings.scroll_up, 'string')
local is_string_or_array = function(x) return type(x) == 'string' or vim.islist(x) end
H.check_type('window.info.height', config.window.info.height, 'number')
H.check_type('window.info.width', config.window.info.width, 'number')
if not is_string_or_array(config.window.info.border or 'single') then
H.error('`config.window.info.border` should be either string or array, not ' .. type(config.window.info.border))
end
H.check_type('window.signature.height', config.window.signature.height, 'number')
H.check_type('window.signature.width', config.window.signature.width, 'number')
if not is_string_or_array(config.window.signature.border or 'single') then
H.error(
'`config.window.signature.border` should be either string or array, not ' .. type(config.window.signature.border)
)
end
return config
end
H.apply_config = function(config)
MiniCompletion.config = config
H.map('i', config.mappings.force_twostep, MiniCompletion.complete_twostage, { desc = 'Complete with two-stage' })
H.map('i', config.mappings.force_fallback, MiniCompletion.complete_fallback, { desc = 'Complete with fallback' })
local map_scroll = function(lhs, direction)
local rhs = function() return MiniCompletion.scroll(direction) and '' or lhs end
H.map('i', lhs, rhs, { expr = true, desc = 'Scroll info/signature ' .. direction })
end
map_scroll(config.mappings.scroll_down, 'down')
map_scroll(config.mappings.scroll_up, 'up')
-- Try setting suggested option values
-- - More common completion behavior
local was_set = vim.api.nvim_get_option_info2('completeopt', { scope = 'global' }).was_set
if not was_set then vim.o.completeopt = 'menuone,noselect' end
-- - Don't show ins-completion-menu messages
was_set = vim.api.nvim_get_option_info2('shortmess', { scope = 'global' }).was_set
if not was_set then vim.opt.shortmess:append('c') end
-- - Remove "t" flag to reduce visible lags
was_set = vim.api.nvim_get_option_info2('complete', { scope = 'global' }).was_set
if not was_set and config.fallback_action == '<C-n>' then vim.opt.complete:remove('t') end
end
H.create_autocommands = function(config)
local gr = vim.api.nvim_create_augroup('MiniCompletion', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
end
au('InsertCharPre', '*', H.auto_completion, 'Auto show completion')
au('CompleteChanged', '*', H.auto_info, 'Auto show info')
au('CursorMovedI', '*', H.auto_signature, 'Auto show signature')
au('ModeChanged', 'i*:[^i]*', function() MiniCompletion.stop() end, 'Stop completion')
au('CompleteDonePre', '*', H.on_completedonepre, 'On CompleteDonePre')
au('TextChangedI', '*', H.on_text_changed_i, 'On TextChangedI')
au('TextChangedP', '*', H.on_text_changed_p, 'On TextChangedP')
if config.lsp_completion.auto_setup then
local source_func = config.lsp_completion.source_func
local callback = function() vim.bo[source_func] = 'v:lua.MiniCompletion.completefunc_lsp' end
au('BufEnter', '*', callback, 'Set completion function')
end
au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
au('FileType', 'TelescopePrompt', function() vim.b.minicompletion_disable = true end, 'Disable locally')
end
H.create_default_hl = function()
vim.api.nvim_set_hl(0, 'MiniCompletionActiveParameter', { default = true, link = 'LspSignatureActiveParameter' })
vim.api.nvim_set_hl(0, 'MiniCompletionDeprecated', { default = true, link = 'DiagnosticDeprecated' })
vim.api.nvim_set_hl(0, 'MiniCompletionInfoBorderOutdated', { default = true, link = 'DiagnosticFloatingWarn' })
end
H.is_disabled = function() return vim.g.minicompletion_disable == true or vim.b.minicompletion_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniCompletion.config, vim.b.minicompletion_config or {}, config or {})
end
-- Autocommands ---------------------------------------------------------------
H.auto_completion = function()
if H.is_disabled() then return end
H.completion.timer:stop()
local is_incomplete = H.completion.lsp.is_incomplete
local is_trigger = H.is_lsp_trigger(vim.v.char, 'completion')
local force = is_trigger or is_incomplete
if force then
-- Force fresh LSP completion if needed. Check before checking pumvisible
-- because it should be forced even if there are visible candidates.
-- Keep positive `is_incomplete` to allow fast typing and not "forget" that
-- list was incomplete after the second fast key press. This will force LSP
-- completion until `isIncomplete=false` response or general `stop()`.
H.stop_completion(false, is_incomplete)
elseif H.pumvisible() then
-- Do nothing if popup is visible. `H.pumvisible()` might be `true` even if
-- there is no popup. It is common when manually typing candidate followed
-- by an LSP trigger (like ".").
-- Keep completion source as it is needed all time when popup is visible.
-- Keep resolved candidates because they should be relevant in this route.
return H.stop_completion(true, false, true)
elseif not H.is_char_keyword(vim.v.char) then
-- Stop everything if inserted character is not appropriate. Check this
-- after popup check to allow completion candidates to have bad characters.
return H.stop_completion(false)
end
-- Start non-forced completion with fallback or forced LSP source for trigger
H.completion.fallback, H.completion.force = not force, force
-- Cache id of Insert mode "text changed" event for a later tracking (reduces
-- false positive delayed triggers). The intention is to trigger completion
-- after the delay only if text wasn't changed during waiting. Using only
-- `InsertCharPre` is not enough though, as not every Insert mode change
-- triggers `InsertCharPre` event (notable example - hitting `<CR>`).
-- Also, using `+ 1` here because it is a `Pre` event and needs to cache
-- after inserting character.
H.completion.text_changed_id = H.text_changed_id + 1
-- If completion was requested after 'lsp' source exhausted itself (there
-- were matches on typing start, but they disappeared during filtering), call
-- fallback immediately.
if H.completion.source == 'lsp' then return H.trigger_fallback() end
-- Set completion context with information about how it was triggered
-- Prefer manual `TriggerCharacter` over automated `...ForIncomplete...`.
local trigger_kind_name = is_trigger and 'TriggerCharacter'
or (is_incomplete and 'TriggerForIncompleteCompletions' or 'Invoked')
local trigger_kind = vim.lsp.protocol.CompletionTriggerKind[trigger_kind_name]
local trigger_char = trigger_kind_name == 'TriggerCharacter' and vim.v.char or nil
H.completion.lsp.context = { triggerKind = trigger_kind, triggerCharacter = trigger_char }
-- Debounce delay improves experience (can type fast without many popups)
-- Request right away if improving incomplete suggestions (less flickering),
-- but still with `vim.schedule` because line is still not up to date during
-- `InsertCharPre` event.
local delay = is_incomplete and 0 or H.get_config().delay.completion
H.completion.timer:start(delay, 0, vim.schedule_wrap(H.trigger_twostep))
end
H.auto_info = function()
if H.is_disabled() then return end
-- Stop current LSP request that tries to get already not current data
H.cancel_lsp({ H.info })
-- Update metadata before leaving to register a `CompleteChanged` event
H.info.timer:stop()
H.info.event = vim.v.event
H.info.id = H.info.id + 1
-- Stop showing window if no candidate is selected
local completed_item = H.info.event.completed_item or {}
if completed_item.word == nil then
return vim.schedule(function() H.close_action_window(H.info) end)
end
-- Show info content without delay for visited and resolved LSP item.
-- Otherwise delay to not spam LSP requests on up/down navigation.
local item_id = H.table_get(completed_item, { 'user_data', 'lsp', 'item_id' })
local is_resolved = item_id == nil or H.completion.lsp.resolved[item_id] ~= nil
local delay = is_resolved and 0 or H.get_config().delay.info
-- Mark visually that currently shown content will be outdated for a while
local win_id = H.info.win_id
if H.is_valid_win(win_id) and delay > 0 then
vim.wo[win_id].winhighlight = vim.wo[win_id].winhighlight .. ',FloatBorder:MiniCompletionInfoBorderOutdated'
end
local cur_info_id = H.info.id
H.info.timer:start(delay, 0, function() H.show_info_window(cur_info_id) end)
end
H.auto_signature = function()
if H.is_disabled() then return end
H.signature.timer:stop()
if not H.has_lsp_clients('signatureHelpProvider') then return end
local is_shown = H.is_valid_win(H.signature.win_id)
local left_char_is_trigger = H.is_lsp_trigger(H.get_left_char(), 'signature')
if not (is_shown or left_char_is_trigger) then return end
local delay = is_shown and 0 or H.get_config().delay.signature
H.signature.timer:start(delay, 0, vim.schedule_wrap(H.show_signature_window))
end
H.on_completedonepre = function()
-- Do nothing if it is triggered inside `trigger_lsp()` as a result of
-- emulating 'completefunc'/'omnifunc' keys. This can happen if popup is
-- visible and pressing keys first hides it with 'CompleteDonePre' event.
if H.completion.lsp.status == 'received' then return end
-- Do extra actions for LSP completion items
local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'lsp' })
if lsp_data ~= nil then H.make_lsp_extra_actions(lsp_data) end
-- Stop processes
MiniCompletion.stop({ 'completion', 'info' })
end
H.on_text_changed_i = function()
-- Track Insert mode changes
H.text_changed_id = H.text_changed_id + 1
-- Stop 'info' processes in case no completion event is triggered but popup
-- is not visible. See https://github.com/neovim/neovim/issues/15077
H.stop_info()
end
H.on_text_changed_p = function()
-- Track Insert mode changes
H.text_changed_id = H.text_changed_id + 1
end
-- Completion triggers --------------------------------------------------------
H.trigger_twostep = function()
-- Trigger only in Insert mode and if text didn't change after trigger
-- request, unless completion is forced
-- NOTE: check for `text_changed_id` equality is still not 100% solution as
-- there are cases when, for example, `<CR>` is hit just before this check.
-- Because of asynchronous id update and this function call (called after
-- delay), these still match.
local allow_trigger = (vim.fn.mode() == 'i')
and (H.completion.force or (H.completion.text_changed_id == H.text_changed_id))
if not allow_trigger then return end
if H.has_lsp_clients('completionProvider') and H.has_lsp_completion() then
H.trigger_lsp()
elseif H.completion.fallback then
H.trigger_fallback()
end
end
H.trigger_lsp = function()
-- Check for popup visibility is needed to reduce flickering.
-- Possible issue timeline (with 100ms delay with set up LSP):
0