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

Skip to content

Latest commit

 

History

History
1980 lines (1715 loc) · 84.1 KB

completion.lua

File metadata and controls

1980 lines (1715 loc) · 84.1 KB
--- *mini.completion* Completion and signature help
--- *MiniCompletion*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Key design ideas:
--- - Have an async (with customizable "debounce" delay) "two-stage chain
--- completion": first try to get completion items from LSP client (if set
--- up) and if no result, fallback to custom action.
---
--- - Managing completion is done as much with Neovim's built-in tools as
--- possible. |popupmenu-completion| is used to show completion suggestions.
---
--- Features:
--- - Two-stage chain completion:
--- - First stage is an LSP completion implemented via
--- |MiniCompletion.completefunc_lsp()|. It should be set up as either
--- |completefunc| or |omnifunc|. It tries to get completion items from
--- LSP client (via 'textDocument/completion' request). Custom
--- preprocessing of response items is possible (with
--- `MiniCompletion.config.lsp_completion.process_items`), for example
--- with fuzzy matching. By default items directly starting with completed
--- word are kept and are sorted according to LSP specification.
--- Supports `additionalTextEdits`, like auto-import and others (see 'Notes'),
--- and snippet items (best results require |mini.snippets| dependency).
--- - If first stage is not set up or resulted into no candidates, fallback
--- action is executed. The most tested actions are Neovim's built-in
--- insert completion (see |ins-completion|).
---
--- - Automatic display in floating window of completion item info (via
--- 'completionItem/resolve' request) and signature help (with highlighting
--- of active parameter if LSP server provides such information).
--- Signature help is shown if character to cursor's left is a dedicated trigger
--- character (configured in `signatureHelpProvider.triggerCharacters` of LSP
--- server capabilities) and updated without delay if is currently opened.
--- Already shown window for signature help is fixed and is closed when there
--- is nothing to show, its text is different, or when leaving Insert mode.
--- Scroll in either info/signature window with `<C-f>` / `<C-b>` (by default).
---
--- - Automatic actions are done after some configurable amount of delay. This
--- reduces computational load and allows fast typing (completion and
--- signature help) and item selection (item info)
---
--- - Force two-stage/fallback completion (`<C-Space>` / `<A-Space>` by default).
---
--- - LSP kind highlighting ("Function", "Keyword", etc.). Requires Neovim>=0.11.
--- By default uses "lsp" category of |MiniIcons| (if enabled). Can be customized
--- via `config.lsp_completion.process_items` by adding field <kind_hlgroup>
--- (same meaning as in |complete-items|) to items.
---
--- What it doesn't do:
--- - Many configurable sources.
--- - Automatic mapping of `<CR>`, `<Tab>`, etc. Those tend to have highly
--- variable user expectations. See 'Helpful mappings' for suggestions or
--- use |MiniKeymap.map_multistep()| with `"pmenu_*"` built-in steps.
---
--- # Dependencies ~
---
--- Suggested dependencies (provide extra functionality, will work without them):
---
--- - Enabled |MiniIcons| module to highlight LSP kind (requires Neovim>=0.11).
--- If absent, |MiniCompletion.default_process_items()| does not add highlighting.
--- Also take a look at |MiniIcons.tweak_lsp_kind()|.
--- - Enabled |MiniSnippets| module for better snippet handling (much recommended).
--- If absent and custom snippet insert is not configured, |vim.snippet.expand()|
--- is used on Neovim>=0.10 (nothing extra is done on earlier versions).
--- See |MiniCompletion.default_snippet_insert()|.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.completion').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniCompletion` which you can use for scripting or manually (with
--- `:lua MiniCompletion.*`).
---
--- See |MiniCompletion.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minicompletion_config` which should have same structure as
--- `MiniCompletion.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Suggested option values ~
---
--- Some options are set automatically (if not set before |MiniCompletion.setup()|):
--- - 'completeopt' is set to "menuone,noselect" for less intrusive popup.
--- To enable fuzzy matching, manually set to "menuone,noselect,fuzzy". Consider
--- also adding "nosort" flag to preserve initial order when filtering.
--- - 'shortmess' is appended with "c" flag for silent <C-n> fallback.
---
--- # Snippets ~
---
--- As per LSP specification, some completion items can be supplied in the form of
--- snippet - a template with both pre-defined text and places (called "tabstops")
--- for user to interactively change/add text during snippet session.
---
--- In 'mini.completion' items that will insert snippet have "S" symbol shown in
--- the popup (as part of `menu` in |complete-items|). To actually insert a snippet:
--- - Select an item via <C-n> / <C-p>. This will insert item's label (usually not
--- full snippet) first to reduce visual flicker. The full snippet text will be
--- shown in info window if LSP server doesn't provide its own info for an item.
--- - Press <C-y> (|complete_CTRL-Y|) or attempt inserting a non-keyword character
--- (like <CR>; new character will be removed). It will clear text from previous
--- step, set cursor, and call `lsp_completion.snippet_insert` with snippet text.
--- - Press <C-e> (|complete_CTRL-E|) to cancel snippet insert and properly end
--- completion.
---
--- See |MiniCompletion.default_snippet_insert()| for overview of how to work with
--- inserted snippets.
---
--- Notes:
--- - To stop LSP server from suggesting snippets, disable (set to `false`) the
--- following capability during LSP server start:
--- `textDocument.completion.completionItem.snippetSupport`.
--- - If snippet body doesn't contain tabstops, `lsp_completion.snippet_insert`
--- is not called and text is inserted as is.
---
--- # Notes ~
---
--- - A more appropriate (albeit slightly advanced) LSP completion setup is to set
--- it not on every |BufEnter| event (default), but on every attach of LSP client.
--- To do that:
--- - Use in |MiniCompletion.setup()| config: >lua
---
--- lsp_completion = { source_func = 'omnifunc', auto_setup = false }
--- <
--- - Set 'omnifunc' option to exactly `v:lua.MiniCompletion.completefunc_lsp`
--- for every client attach in an |LspAttach| event. Like this: >lua
---
--- local on_attach = function(args)
--- vim.bo[args.buf].omnifunc = 'v:lua.MiniCompletion.completefunc_lsp'
--- end
--- vim.api.nvim_create_autocmd('LspAttach', { callback = on_attach })
--- <
--- This setup is not default to allow simultaneous usage of filetype-specific
--- 'omnifunc' (with manual |i_CTRL-X_CTRL-O|) and automated LSP completion.
---
--- - Use |MiniCompletion.get_lsp_capabilities()| to get/set information about part
--- of LSP specification supported by module. See its help for usability notes.
---
--- - Uses `vim.lsp.protocol.CompletionItemKind` map in LSP step to show a readable
--- version of item's kind. Modify it directly to change what is displayed.
--- If you have |mini.icons| enabled, take a look at |MiniIcons.tweak_lsp_kind()|.
---
--- - If you have trouble using custom (overridden) |vim.ui.input|, disable
--- 'mini.completion' for input buffer (usually based on its 'filetype').
---
--- # Comparisons ~
---
--- - 'hrsh7th/nvim-cmp':
--- - Implements own popup menu to show completion candidates, while this
--- module reuses |ins-completion-menu|.
--- - Has more complex design which allows multiple sources, each in a form of
--- a separate plugin. This module has two built-in: LSP and fallback.
--- - Requires separate plugin for automated signature help.
--- - Implements own "ghost text" feature, while this module does not.
---
--- - 'Saghen/blink.cmp':
--- - Mostly similar to 'nvim-cmp' comparison: provides more features at the
--- cost of more code and config complexity, while this module is designed
--- to provide only a handful of "enough" features while relying on Neovim's
--- built-in capabilities as much as possible.
--- - Both provide automated signature help out of the box.
---
--- # Helpful mappings ~
---
--- If there is |mini.keymap| available, prefer using |MiniKeymap.map_multistep()|
--- with `"pmenu_*"` built-in steps. See |MiniKeymap-examples| for examples.
---
--- To use `<Tab>` and `<S-Tab>` for navigation through completion list, make
--- these mappings: >lua
---
--- local imap_expr = function(lhs, rhs)
--- vim.keymap.set('i', lhs, rhs, { expr = true })
--- end
--- imap_expr('<Tab>', [[pumvisible() ? "\<C-n>" : "\<Tab>"]])
--- imap_expr('<S-Tab>', [[pumvisible() ? "\<C-p>" : "\<S-Tab>"]])
--- <
--- To get more consistent behavior of `<CR>`, you can use this template in
--- your 'init.lua' to make customized mapping: >lua
---
--- _G.cr_action = function()
--- -- If there is selected item in popup, accept it with <C-y>
--- if vim.fn.complete_info()['selected'] ~= -1 then return '\25' end
--- -- Fall back to plain `<CR>`. You might want to customize according
--- -- to other plugins. For example if 'mini.pairs' is set up, replace
--- -- next line with `return MiniPairs.cr()`
--- return '\r'
--- end
---
--- vim.keymap.set('i', '<CR>', 'v:lua.cr_action()', { expr = true })
--- <
--- # Highlight groups ~
---
--- * `MiniCompletionActiveParameter` - signature active parameter.
--- * `MiniCompletionInfoBorderOutdated` - info window border when text is outdated
--- due to explicit delay during fast movement through candidates.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minicompletion_disable` (globally) or
--- `vim.b.minicompletion_disable` (for a buffer) to `true`. Considering high
--- number of different scenarios and customization intentions, writing exact
--- rules for disabling module's functionality is left to user. See
--- |mini.nvim-disabling-recipes| for common recipes.
--- Events ~
---
--- To allow user customization, certain |User| autocommand events are
--- triggered under common circumstances:
---
--- - Info and signature help window:
--- - `MiniCompletionWindowOpen` - after opening new window.
--- - `MiniCompletionWindowUpdate` - after updating existing window.
---
--- Each event's |event-data| table contains `kind` (one of "info" or "signature")
--- and `win_id` (affected window identifier) fields.
---@tag MiniCompletion-events
-- Overall implementation design:
-- - Completion:
-- - On `InsertCharPre` event try to start auto completion. If needed,
-- start timer which after delay will start completion process. Stop this
-- timer if it is not needed.
-- - When timer is activated, first execute LSP source (if set up and there
-- is an active LSP client) by calling built-in complete function
-- (`completefunc` or `omnifunc`) which tries LSP completion by
-- asynchronously sending LSP 'textDocument/completion' request to all
-- LSP clients. When all are done, execute callback which processes
-- results, stores them in LSP cache and reruns built-in complete
-- function which produces completion popup.
-- - If previous step didn't result into any completion, execute (in Insert
-- mode and if no popup) fallback action.
-- - Documentation:
-- - On `CompleteChanged` start auto info with similar to completion timer
-- pattern.
-- - 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
--- Module config
---
--- Default values:
---@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` (on Neovim>=0.10).
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' string. Default: array containing all of them.
MiniCompletion.stop = function(actions)
actions = actions or { 'completion', 'info', 'signature' }
for _, n in pairs(actions) do
H.stop_actions[n]()
end
end
--- Module's |complete-function|
---
--- 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
H.completion.start_pos = H.get_completion_start(H.completion.lsp.result)
return H.completion.start_pos[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.
--- - If |MiniIcons| 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
-- Possibly add "kind" highlighting
if _G.MiniIcons == nil then return res end
local add_kind_hlgroup = H.make_add_kind_hlgroup()
for _, item in ipairs(res) do
add_kind_hlgroup(item)
end
return res
end
--- Default snippet insert
---
--- Order of preference:
--- - Use |MiniSnippets| if set up (i.e. there is `require('mini.snippets').setup()`).
--- - Use |vim.snippet.expand()| on Neovim>=0.10
--- - Add snippet text at cursor as is.
---
--- After snippet is inserted, user is expected to navigate/jump between dedicated
--- places (tabstops) to adjust inserted text as needed:
--- - |MiniSnippets| 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
if vim.fn.has('nvim-0.10') == 1 then return vim.snippet.expand(snippet) end
local pos, lines = vim.api.nvim_win_get_cursor(0), vim.split(snippet, '\n')
vim.api.nvim_buf_set_text(0, pos[1] - 1, pos[2], pos[1] - 1, pos[2], lines)
local n = #lines
local new_pos = n == 1 and { pos[1], pos[2] + lines[n]:len() } or { pos[1] + n - 1, lines[n]:len() }
vim.api.nvim_win_set_cursor(0, new_pos)
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'`
--- requrest. 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
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 = false,
preselectSupport = false,
tagSupport = { 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),
}
-- 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 },
start_pos = {},
}
-- 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 H.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 ("C" is default on Neovim>=0.10)
local shortmess_flags = 'c' .. (vim.fn.has('nvim-0.10') == 0 and 'C' or '')
was_set = vim.api.nvim_get_option_info2('shortmess', { scope = 'global' }).was_set
if not was_set then vim.opt.shortmess:append(shortmess_flags) 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, '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):
-- 0ms: Key is pressed.
-- 100ms: LSP is triggered from first key press.
-- 110ms: Another key is pressed.
-- 200ms: LSP callback is processed, triggers complete-function which
-- processes "received" LSP request.
-- 201ms: LSP request is processed, completion is (should be almost
-- immediately) provided, request is marked as "done".
-- 210ms: LSP is triggered from second key press. As previous request is
-- "done", it will once make whole LSP request. Having check for visible
0