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

Skip to content

Latest commit

 

History

History
1379 lines (1178 loc) · 51.9 KB

completion.lua

File metadata and controls

1379 lines (1178 loc) · 51.9 KB
-- MIT License Copyright (c) 2021 Evgeni Chasnovski
-- Documentation ==============================================================
--- Autocompletion and signature help plugin. 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.
---
--- 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 which are not snippets and
--- directly start with completed word are kept and sorted according to
--- LSP specification. Supports `additionalTextEdits`, like auto-import
--- and others (see 'Notes').
--- - 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). After
--- opening, window for signature help is fixed and is closed when there is
--- nothing to show, text is different or
--- when leaving Insert mode.
--- - 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)
--- - User can force two-stage completion via
--- |MiniCompletion.complete_twostage()| (by default is mapped to
--- `<C-Space>`) or fallback completion via
--- |MiniCompletion.complete_fallback()| (maped to `<M-Space>`).
---
--- What it doesn't do:
--- - Snippet expansion.
--- - Many configurable sources.
--- - Automatic mapping of `<CR>`, `<Tab>`, etc., as those tend to have highly
--- variable user expectations. See 'Helpful key mappings' for suggestions.
---
--- # 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.
---
--- # Notes~
---
--- - 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 initial config:
--- `lsp_completion = {source_func = 'omnifunc', auto_setup = false}`.
--- - In `on_attach()` of every LSP client set 'omnifunc' option to exactly
--- `v:lua.MiniCompletion.completefunc_lsp`.
--- - If you have trouble using custom (overriden) |vim.ui.input| (like from
--- 'stevearc/dressing.nvim'), make automated disable of 'mini.completion'
--- for input buffer. For example, currently for 'dressing.nvim' it can be
--- with `au FileType DressingInput lua vim.b.minicompletion_disable = true`.
--- - Support of `additionalTextEdits` tries to handle both types of servers:
--- - When `additionalTextEdits` are supplied in response to
--- 'textDocument/completion' request (like currently in 'pyright').
--- - When `additionalTextEdits` are supplied in response to
--- 'completionItem/resolve' request (like currently in
--- 'typescript-language-server'). In this case to apply edits user needs
--- to trigger such request, i.e. select completion item and wait for
--- `MiniCompletion.config.delay.info` time plus server response time.
---
--- # Comparisons~
---
--- - 'nvim-cmp':
--- - More complex design which allows multiple sources each in form of
--- separate plugin. `MiniCompletion` has two built in: LSP and fallback.
--- - Supports snippet expansion.
--- - Doesn't have customizable delays for basic actions.
--- - Doesn't allow fallback action.
--- - Doesn't provide signature help.
---
--- # Helpful key mappings~
---
--- To use `<Tab>` and `<S-Tab>` for navigation through completion list, make
--- these key mappings:
--- `vim.api.nvim_set_keymap('i', '<Tab>', [[pumvisible() ? "\<C-n>" : "\<Tab>"]], { noremap = true, expr = true })`
--- `vim.api.nvim_set_keymap('i', '<S-Tab>', [[pumvisible() ? "\<C-p>" : "\<S-Tab>"]], { noremap = true, expr = true })`
---
--- To get more consistent behavior of `<CR>`, you can use this template in
--- your 'init.lua' to make customized mapping: >
--- local keys = {
--- ['cr'] = vim.api.nvim_replace_termcodes('<CR>', true, true, true),
--- ['ctrl-y'] = vim.api.nvim_replace_termcodes('<C-y>', true, true, true),
--- ['ctrl-y_cr'] = vim.api.nvim_replace_termcodes('<C-y><CR>', true, true, true),
--- }
---
--- _G.cr_action = function()
--- if vim.fn.pumvisible() ~= 0 then
--- -- If popup is visible, confirm selected item or add new line otherwise
--- local item_selected = vim.fn.complete_info()['selected'] ~= -1
--- return item_selected and keys['ctrl-y'] or keys['ctrl-y_cr']
--- else
--- -- If popup is not visible, use plain `<CR>`. You might want to customize
--- -- according to other plugins. For example, to use 'mini.pairs', replace
--- -- next line with `return require('mini.pairs').cr()`
--- return keys['cr']
--- end
--- end
---
--- vim.api.nvim_set_keymap('i', '<CR>', 'v:lua._G.cr_action()', { noremap = true, expr = true })
--- <
--- # Highlight groups~
---
--- * `MiniCompletionActiveParameter` - highlighting of signature active parameter.
--- By default displayed as plain underline.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling~
---
--- To disable, set `g:minicompletion_disable` (globally) or
--- `b:minicompletion_disable` (for a buffer) to `v: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.
---@tag mini.completion
---@tag MiniCompletion
-- 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 8000 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.
-- - Check if character left to cursor is appropriate (')' or LSP's
-- signature help trigger characters). If not, do nothing.
-- - 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 Module config table. See |MiniCompletion.config|.
---
---@usage `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)
-- Setup module behavior
vim.api.nvim_exec(
[[augroup MiniCompletion
au!
au InsertCharPre * lua MiniCompletion.auto_completion()
au CompleteChanged * lua MiniCompletion.auto_info()
au CursorMovedI * lua MiniCompletion.auto_signature()
au InsertLeavePre * lua MiniCompletion.stop()
au CompleteDonePre * lua MiniCompletion.on_completedonepre()
au TextChangedI * lua MiniCompletion.on_text_changed_i()
au TextChangedP * lua MiniCompletion.on_text_changed_p()
au FileType TelescopePrompt let b:minicompletion_disable=v:true
augroup END]],
false
)
if config.lsp_completion.auto_setup then
local command = string.format(
[[augroup MiniCompletion
au BufEnter * setlocal %s=v:lua.MiniCompletion.completefunc_lsp
augroup END]],
config.lsp_completion.source_func
)
vim.api.nvim_exec(command, false)
end
-- Create highlighting
vim.api.nvim_exec('hi default MiniCompletionActiveParameter term=underline cterm=underline gui=underline', false)
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()`).
window = {
info = { height = 25, width = 80, border = 'none' },
signature = { height = 25, width = 80, border = 'none' },
},
-- 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,
-- `process_items` should be a function which takes LSP
-- 'textDocument/completion' response items and word to complete. Its
-- output should be a table of the same nature as input items. The most
-- common use-cases are custom filtering and sorting. You can use
-- default `process_items` as `MiniCompletion.default_process_items()`.
--minidoc_replace_start process_items = --<function: filters out snippets; sorts by LSP specs>,
process_items = function(items, base)
local res = vim.tbl_filter(function(item)
-- Keep items which match the base and are not snippets
return vim.startsw 8BD0 ith(H.get_completion_word(item), base) and item.kind ~= 15
end, items)
table.sort(res, function(a, b) return (a.sortText or a.label) < (b.sortText or b.label) end)
return res
end,
--minidoc_replace_end
},
-- Fallback action. It will always be run in Insert mode. To use Neovim's
-- built-in completion (see `:h ins-completion`), supply its mapping as
-- string. Example: to use 'whole lines' completion, supply '<C-x><C-l>'.
--minidoc_replace_start fallback_action = --<function: like `<C-n>` completion>,
fallback_action = function() vim.api.nvim_feedkeys(H.keys.ctrl_n, 'n', false) end,
--minidoc_replace_end
-- Module mappings. Use `''` (empty string) to disable one. Some of them
-- might conflict with system mappings.
mappings = {
force_twostep = '<C-Space>', -- Force two-step completion
force_fallback = '<A-Space>', -- Force fallback completion
},
-- Whether to set Vim's settings for better experience (modifies
-- `shortmess` and `completeopt`)
set_vim_settings = true,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Auto completion
---
--- Designed to be used with |autocmd|. No need to use it directly, everything
--- is setup in |MiniCompletion.setup|.
MiniCompletion.auto_completion = function()
if H.is_disabled() then return end
H.completion.timer:stop()
-- Don't do anything if popup is visible
if H.pumvisible() then
-- Keep completion source as it is needed all time when popup is visible
H.stop_completion(true)
return
end
-- Stop everything if inserted character is not appropriate
local char_is_trigger = H.is_lsp_trigger(vim.v.char, 'completion')
if not (H.is_char_keyword(vim.v.char) or char_is_trigger) then
H.stop_completion(false)
return
end
-- If character is purely lsp trigger, make new LSP request without fallback
-- and force new completion
if char_is_trigger then H.cancel_lsp() end
H.completion.fallback, H.completion.force = not char_is_trigger, char_is_trigger
-- 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
H.trigger_fallback()
return
end
-- Using delay (of debounce type) actually improves user experience
-- as it allows fast typing without many popups.
H.completion.timer:start(H.get_config().delay.completion, 0, vim.schedule_wrap(H.trigger_twostep))
end
--- Run two-stage completion
---
---@param fallback boolean Whether to use fallback completion.
---@param force boolean Whether to force update of completion popup.
MiniCompletion.complete_twostage = function(fallback, force)
if H.is_disabled() then return end
H.stop_completion()
H.completion.fallback, H.completion.force = fallback or true, force or true
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
--- Auto completion entry information
---
--- Designed to be used with |autocmd|. No need to use it directly, everything
--- is setup in |MiniCompletion.setup|.
MiniCompletion.auto_info = function()
if H.is_disabled() then return end
H.info.timer:stop()
-- Defer execution because of textlock during `CompleteChanged` event
-- Don't stop timer when closing info window because it is needed
vim.defer_fn(function() H.close_action_window(H.info, true) end, 0)
-- Stop current LSP request that tries to get not current data
H.cancel_lsp({ H.info })
-- Update metadata before leaving to register a `CompleteChanged` event
H.info.event = vim.v.event
H.info.id = H.info.id + 1
-- Don't even try to show info if nothing is selected in popup
if vim.tbl_isempty(H.info.event.completed_item) then return end
H.info.timer:start(H.get_config().delay.info, 0, vim.schedule_wrap(H.show_info_window))
end
--- Auto function signature
---
--- Designed to be used with |autocmd|. No need to use it directly, everything
--- is setup in |MiniCompletion.setup|.
MiniCompletion.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 left_char = H.get_left_char()
local char_is_trigger = left_char == ')' or H.is_lsp_trigger(left_char, 'signature')
if not char_is_trigger then return end
H.signature.timer:start(H.get_config().delay.signature, 0, vim.schedule_wrap(H.show_signature_window))
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 Array containing any of 'completion', 'info', or
--- 'signature' string.
MiniCompletion.stop = function(actions)
actions = actions or { 'completion', 'info', 'signature' }
for _, n in pairs(actions) do
H.stop_actions[n]()
end
end
MiniCompletion.on_completedonepre = function()
-- Try to apply additional text edits
H.apply_additional_text_edits()
-- Stop processes
MiniCompletion.stop({ 'completion', 'info' })
end
--- Act on every |TextChangedI|
MiniCompletion.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
--- Act on every |TextChangedP|
MiniCompletion.on_text_changed_p = function()
-- Track Insert mode changes
H.text_changed_id = H.text_changed_id + 1
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
if findstart == 1 then
return -3
else
return {}
end
end
-- NOTE: having code for request inside this function enables its use
-- directly with `<C-x><...>`.
if H.completion.lsp.status ~= 'received' then
local current_id = H.completion.lsp.id + 1
H.completion.lsp.id = current_id
H.completion.lsp.status = 'sent'
local bufnr = vim.api.nvim_get_current_buf()
local params = vim.lsp.util.make_position_params()
-- 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 executed only on second call because it returns `-3` on
-- first call (which means cancel and leave completion mode).
-- NOTE: using `buf_request_all()` (instead of `buf_request()`) to easily
-- handle possible fallback and to have all completion suggestions be
-- filtered with one `base` in the other route of this function. Anyway,
-- the most common situation is with one attached LSP client.
local cancel_fun = vim.lsp.buf_request_all(bufnr, 'textDocument/completion', params, function(result)
if not H.is_lsp_current(H.completion, current_id) then return end
H.completion.lsp.status = 'received'
H.completion.lsp.result = result
-- Trigger LSP completion to take 'received' route
H.trigger_lsp()
end)
-- Cache cancel function to disable requests when they are not needed
H.completion.lsp.cancel_fun = cancel_fun
-- End completion and wait for LSP callback
if findstart == 1 then
return -3
else
return {}
end
else
if findstart == 1 then return H.get_completion_start() end
local config = H.get_config()
local words = H.process_lsp_response(H.completion.lsp.result, function(response, client_id)
-- Response can be `CompletionList` with 'items' field or `CompletionItem[]`
local items = H.table_get(response, { 'items' }) or response
if type(items) ~= 'table' then return {} end
items = config.lsp_completion.process_items(items, base)
return H.lsp_completion_response_items_to_complete_items(items, client_id)
end)
H.completion.lsp.status = 'done'
-- Maybe trigger fallback action
if vim.tbl_isempty(words) and H.completion.fallback then
H.trigger_fallback()
return
end
-- Track from which source is current popup
H.completion.source = 'lsp'
return words
end
end
--- Default `MiniCompletion.config.lsp_completion.process_items`
MiniCompletion.default_process_items =
function(items, base) return H.default_config.lsp_completion.process_items(items, base) end
-- Helper data ================================================================
-- Module default config
H.default_config = 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: status. One of 'sent', 'received', 'done', 'canceled'.
-- - 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, result = nil, cancel_fun = nil },
}
-- Cache for completion item info
H.info = {
bufnr = nil,
event = nil,
id = 0,
timer = vim.loop.new_timer(),
winnr = 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(),
winnr = nil,
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
-- General idea: if some table elements are not present in user-supplied
-- `config`, take them from default config
vim.validate({ config = { config, 'table', true } })
config = vim.tbl_deep_extend('force', H.default_config, config or {})
-- Validate per nesting level to produce correct error message
vim.validate({
delay = { config.delay, 'table' },
window = { config.window, 'table' },
lsp_completion = { config.lsp_completion, 'table' },
fallback_action = {
config.fallback_action,
function(x) return type(x) == 'function' or type(x) == 'string' end,
'function or string',
},
mappings = { config.mappings, 'table' },
set_vim_settings = { config.set_vim_settings, 'boolean' },
})
vim.validate({
['delay.completion'] = { config.delay.completion, 'number' },
['delay.info'] = { config.delay.info, 'number' },
['delay.signature'] = { config.delay.signature, 'number' },
['window.info'] = { config.window.info, 'table' },
['window.signature'] = { config.window.signature, 'table' },
['lsp_completion.source_func'] = {
config.lsp_completion.source_func,
function(x) return x == 'completefunc' or x == 'omnifunc' end,
'one of strings: "completefunc" or "omnifunc"',
},
['lsp_completion.auto_setup'] = { config.lsp_completion.auto_setup, 'boolean' },
['lsp_completion.process_items'] = { config.lsp_completion.process_items, 'function' },
['mappings.force_twostep'] = { config.mappings.force_twostep, 'string' },
['mappings.force_fallback'] = { config.mappings.force_fallback, 'string' },
})
local is_string_or_array = function(x) return type(x) == 'string' or vim.tbl_islist(x) end
vim.validate({
['window.info.height'] = { config.window.info.height, 'number' },
['window.info.width'] = { config.window.info.width, 'number' },
['window.info.border'] = {
config.window.info.border,
is_string_or_array,
'(mini.completion) `config.window.info.border` can be either string or array.',
},
['window.signature.height'] = { config.window.signature.height, 'number' },
['window.signature.width'] = { config.window.signature.width, 'number' },
['window.signature.border'] = {
config.window.signature.border,
is_string_or_array,
'(mini.completion) `config.window.signature.border` can be either string or array.',
},
})
-- TODO: Remove after 0.7.0 release.
-- Compatibility for renaming `config.window_dimensions` to `config.window`
if config.window_dimensions ~= nil then
vim.notify(
'(mini.completion) Field `config.window_dimensions` is renamed to `config.window`. '
.. 'It will work until next release.'
)
config.window = vim.tbl_deep_extend('force', config.window, config.window_dimensions)
end
return config
end
H.apply_config = function(config)
MiniCompletion.config = config
--stylua: ignore start
H.map('i', config.mappings.force_twostep, '<cmd>lua MiniCompletion.complete_twostage()<cr>', { desc = 'Complete with two-stage' })
H.map('i', config.mappings.force_fallback, '<cmd>lua MiniCompletion.complete_fallback()<cr>', { desc = 'Complete with fallback' })
--stylua: ignore end
if config.set_vim_settings then
-- Don't give ins-completion-menu messages
vim.cmd('set shortmess+=c')
-- More common completion behavior
vim.cmd('set completeopt=menuone,noinsert,noselect')
end
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
-- 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
-- popup should prevent here the call to complete-function.
-- When `force` is `true` then presence of popup shouldn't matter.
local no_popup = H.completion.force or (not H.pumvisible())
if no_popup and vim.fn.mode() == 'i' then
local key = H.keys[H.get_config().lsp_completion.source_func]
vim.api.nvim_feedkeys(key, 'n', false)
end
end
H.trigger_fallback = function()
local no_popup = H.completion.force or (not H.pumvisible())
if no_popup and vim.fn.mode() == 'i' then
-- Track from which source is current popup
H.completion.source = 'fallback'
local config = H.get_config()
if type(config.fallback_action) == 'string' then
-- Having `<C-g><C-g>` also (for some mysterious reason) helps to avoid
-- some weird behavior. For example, if `keys = '<C-x><C-l>'` then Neovim
-- starts new line when there is no suggestions.
local keys = string.format('<C-g><C-g>%s', config.fallback_action)
local trigger_keys = vim.api.nvim_replace_termcodes(keys, true, false, true)
vim.api.nvim_feedkeys(trigger_keys, 'n', false)
else
config.fallback_action()
end
end
end
-- Stop actions ---------------------------------------------------------------
H.stop_completion = function(keep_source)
H.completion.timer:stop()
H.cancel_lsp({ H.completion })
H.completion.fallback, H.completion.force = true, false
if not keep_source then H.completion.source = nil end
end
H.stop_info = function()
-- Id update is needed to notify that all previous work is not current
H.info.id = H.info.id + 1
H.info.timer:stop()
H.cancel_lsp({ H.info })
H.close_action_window(H.info)
end
H.stop_signature = function()
H.signature.text = nil
H.signature.timer:stop()
H.cancel_lsp({ H.signature })
H.close_action_window(H.signature)
end
H.stop_actions = {
completion = H.stop_completion,
info = H.stop_info,
signature = H.stop_signature,
}
-- LSP ------------------------------------------------------------------------
---@param capability string|table|`nil` Server capability (possibly nested
--- supplied via table) to check.
---
---@return boolean Whether at least one LSP client supports `capability`.
---@private
H.has_lsp_clients = function(capability)
local clients = vim.lsp.buf_get_clients()
if vim.tbl_isempty(clients) then return false end
if not capability then return true end
for _, c in pairs(clients) do
local has_capability = H.table_get(c.server_capabilities, capability)
if has_capability then return true end
end
return false
end
H.has_lsp_completion = function()
local func = vim.api.nvim_buf_get_option(0, H.get_config().lsp_completion.source_func)
return func == 'v:lua.MiniCompletion.completefunc_lsp'
end
H.is_lsp_trigger = function(char, type)
local triggers
local providers = {
completion = 'completionProvider',
signature = 'signatureHelpProvider',
}
for _, client in pairs(vim.lsp.buf_get_clients()) do
triggers = H.table_get(client, { 'server_capabilities', providers[type], 'triggerCharacters' })
if vim.tbl_contains(triggers or {}, char) then return true end
end
return false
end
H.cancel_lsp = function(caches)
caches = caches or { H.completion, H.info, H.signature }
for _, c in pairs(caches) do
if vim.tbl_contains({ 'sent', 'received' }, c.lsp.status) then
if c.lsp.cancel_fun then c.lsp.cancel_fun() end
c.lsp.status = 'canceled'
end
c.lsp.result = nil
c.lsp.cancel_fun = nil
end
end
H.process_lsp_response = function(request_result, processor)
if not request_result then return {} end
local res = {}
for client_id, item in pairs(request_result) do
if not item.err and item.result then vim.list_extend(res, processor(item.result, client_id) or {}) end
end
return res
end
H.is_lsp_current = function(cache, id) return cache.lsp.id == id and cache.lsp.status == 'sent' end
-- Completion -----------------------------------------------------------------
-- This is a truncated version of
-- `vim.lsp.util.text_document_completion_list_to_complete_items` which does
-- not filter and sort items.
-- For extra information see 'Response' section:
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion
H.lsp_completion_response_items_to_complete_items = function(items, client_id)
if vim.tbl_count(items) == 0 then return {} end
local res = {}
local docs, info
for _, item in pairs(items) do
-- Documentation info
docs = item.documentation
info = H.table_get(docs, { 'value' })
if not info and type(docs) == 'string' then info = docs end
info = info or ''
table.insert(res, {
word = H.get_completion_word(item),
abbr = item.label,
kind = vim.lsp.protocol.CompletionItemKind[item.kind] or 'Unknown',
menu = item.detail or '',
info = info,
icase = 1,
dup = 1,
empty = 1,
user_data = { nvim = { lsp = { completion_item = item, client_id = client_id } } },
})
end
return res
end
H.get_completion_word = function(item)
-- Completion word (textEdit.newText > insertText > label). This doesn't
-- support snippet expansion.
return H.table_get(item, { 'textEdit', 'newText' }) or item.insertText or item.label or ''
end
H.apply_additional_text_edits = function()
-- Code originally.inspired by https://github.com/neovim/neovim/issues/12310
-- Try to get `additionalTextEdits`. First from 'completionItem/resolve';
-- then - from selected item. The reason for this is inconsistency in how
-- servers provide `additionTextEdits`: on 'textDocument/completion' or
-- 'completionItem/resolve'.
local resolve_data = H.process_lsp_response(H.info.lsp.result, function(response, client_id)
-- Return nested table because this will be a second argument of
-- `vim.list_extend()` and the whole inner table is a target value here.
return { { edits = response.additionalTextEdits, client_id = client_id } }
end)
local edits, client_id
if #resolve_data >= 1 then
edits, client_id = resolve_data[1].edits, resolve_data[1].client_id
else
local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' }) or {}
edits = H.table_get(lsp_data, { 'completion_item', 'additionalTextEdits' })
client_id = lsp_data.client_id
end
if edits == nil then return end
client_id = client_id or 0
-- Use extmark to track relevant cursor postion after text edits
local cur_pos = vim.api.nvim_win_get_cursor(0)
local extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], {})
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding)
local extmark_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, extmark_id, {})
pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, extmark_id)
pcall(vim.api.nvim_win_set_cursor, 0, { extmark_data[1] + 1, extmark_data[2] })
end
-- Completion item info -------------------------------------------------------
H.show_info_window = function()
local event = H.info.event
if not event then return end
-- Try first to take lines from LSP request result.
local lines
if H.info.lsp.status == 'received' then
lines = H.process_lsp_response(H.info.lsp.result, function(response)
if not response.documentation then return {} end
local res = vim.lsp.util.convert_input_to_markdown_lines(response.documentation)
return vim.lsp.util.trim_empty_lines(res)
end)
H.info.lsp.status = 'done'
else
lines = H.info_window_lines(H.info.id)
end
-- Don't show anything if there is nothing to show
if not lines or H.is_whitespace(lines) then return end
-- If not already, create a permanent buffer where info will be
-- displayed. For some reason, it is important to have it created not in
-- `setup()` because in that case there is a small flash (which is really a
-- brief open of window at screen top, focus on it, and its close) on the
-- first show of info window.
H.ensure_buffer(H.info, 'MiniCompletion:completion-item-info')
-- Add `lines` to info buffer. Use `wrap_at` to have proper width of
-- 'non-UTF8' section separators.
vim.lsp.util.stylize_markdown(H.info.bufnr, lines, { wrap_at = H.get_config().window.info.width })
-- Compute floating window options
local opts = H.info_window_options()
-- Defer execution because of textlock during `CompleteChanged` event
vim.defer_fn(function()
-- Ensure that window doesn't open when it shouldn't be
if not (H.pumvisible() and vim.fn.mode() == 'i') then return end
H.open_action_window(H.info, opts)
end, 0)
end
H.info_window_lines = function(info_id)
-- Try to use 'info' field of Neovim's completion item
local completed_item = H.table_get(H.info, { 'event', 'completed_item' }) or {}
local text = completed_item.info or ''
if not H.is_whitespace(text) then
-- Use `<text></text>` to be properly processed by `stylize_markdown()`
local lines = { '<text>' }
vim.list_extend(lines, vim.split(text, '\n', false))
table.insert(lines, '</text>')
return lines
end
-- If popup is not from LSP then there is nothing more to do
if H.completion.source ~= 'lsp' then return nil end
-- Try to get documentation from LSP's initial completion result
local lsp_completion_item = H.table_get(completed_item, { 'user_data', 'nvim', 'lsp', 'completion_item' })
-- If there is no LSP's completion item, then there is no point to proceed as
-- it should serve as parameters to LSP request
if not lsp_completion_item then return end
local doc = lsp_completion_item.documentation
if doc then
local lines = vim.lsp.util.convert_input_to_markdown_lines(doc)
return vim.lsp.util.trim_empty_lines(lines)
end
-- Finally, try request to resolve current completion to add documentation
local bufnr = vim.api.nvim_get_current_buf()
local params = lsp_completion_item
local current_id = H.info.lsp.id + 1
H.info.lsp.id = current_id
H.info.lsp.status = 'sent'
local cancel_fun = vim.lsp.buf_request_all(bufnr, 'completionItem/resolve', params, function(result)
-- Don't do anything if there is other LSP request in action
if not H.is_lsp_current(H.info, current_id) then return end
H.info.lsp.status = 'received'
-- Don't do anything if completion item was changed
0