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

Skip to content

Latest commit

 

History

History
1175 lines (1029 loc) · 49.2 KB

cmdline.lua

File metadata and controls

1175 lines (1029 loc) · 49.2 KB
--- *mini.cmdline* Command line tweaks
---
--- MIT License Copyright (c) 2025 Evgeni Chasnovski
--- Features:
---
--- - Autocomplete with customizable delay. Enhances |cmdline-completion| and
--- manual |'wildchar'| pressing experience.
--- Requires Neovim>=0.11, though Neovim>=0.12 is recommended.
---
--- - Autocorrect words as-you-type. Only words that must come from a fixed set of
--- candidates (like commands and options) are autocorrected by default.
---
--- - Autopeek command range as-you-type. Shows a floating window with range lines
--- along with customizable context lines.
---
--- What it doesn't do:
---
--- - Customization of command line UI. Use |ui2| (on Neovim>=0.12).
---
--- - Customization of autocompletion candidates. They are computed
--- via |cmdline-completion|.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.cmdline').setup({})` (replace `{}`
--- with your `config` table). It will create global Lua table `MiniCmdline` which
--- you can use for scripting or manually (with `:lua MiniCmdline.*`).
---
--- See |MiniCmdline.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minicmdline_config` which should have same structure as
--- `MiniCmdline.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Suggested option values ~
---
--- Some options are set automatically (if not set before |MiniCmdline.setup()|):
--- - |'wildmode'| is set to "noselect,full" for less intrusive autocompletion.
--- Requires Neovim>=0.11 and enabled `config.autocomplete`.
--- - |'wildoptions'| is set to "pum,fuzzy" to enable fuzzy matching.
---
--- # Comparisons ~
---
--- - [folke/noice.nvim](https://github.com/folke/noice.nvim):
--- - Mostly focuses on visual aspects of the Command line.
--- This modules is aimed to improve its workflow without changing UI.
---
--- - [nacro90/numb.nvim](https://github.com/nacro90/numb.nvim):
--- - Designed to preview only a single line range defined by numbers.
--- This module handles any form of |:range| and |:range-offset| for both
--- one and two line ranges.
--- - Shows target line directly in the normal window.
--- This module uses a dedicated floating window.
---
--- - Built-in |cmdline-autocompletion| (on Neovim>=0.12):
--- - This module on Neovim>=0.12 uses that as its base for autocompletion.
--- Ont top of that it also provides customizable delay and predicate.
---
--- - Built-in |ui2| (on Neovim>=0.12):
--- - Mostly focuses on visual aspects of the Command line.
--- This modules is aimed to improve its workflow without changing UI.
---
--- # Highlight groups ~
--- *MiniCmdline-hl-groups*
---
--- - `MiniCmdlinePeekBorder` - border of autopeek window.
--- - `MiniCmdlinePeekLineNr` - line numbers in autopeek window.
--- - `MiniCmdlinePeekNormal` - basic foreground/background of autopeek window.
--- - `MiniCmdlinePeekSep` - statuscolumn separator in autopeek window.
--- - `MiniCmdlinePeekSign` - signs in autopeek window.
--- - `MiniCmdlinePeekTitle` - title of autopeek window.
---
--- # Disabling ~
---
--- To disable acting in mappings, set `vim.g.minicmdline_disable` (globally) or
--- `vim.b.minicmdline_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.
---@tag MiniCmdline
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
-- Module definition ==========================================================
local MiniCmdline = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniCmdline.config|.
---
---@usage >lua
--- require('mini.cmdline').setup() -- use default config
--- -- OR
--- require('mini.cmdline').setup({}) -- replace {} with your config table
--- <
MiniCmdline.setup = function(config)
-- Export module
_G.MiniCmdline = MiniCmdline
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
-- Create default highlighting
H.create_default_hl()
end
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # General ~
---
--- - Each feature is configured via separate table.
--- - Use `enable = false` to disable a feature.
---
---# Autocomplete ~
---
--- `config.autocomplete` is used to configure autocompletion: automatic show
--- of |'wildmenu'|.
---
--- `autocomplete.delay` defines a (debounce style) delay after which |'wildchar'|
--- is triggered to show wildmenu.
--- Default: 0. Note: Neovim>=0.12 is recommended for positive values if you
--- want to reduce flicker (thanks to |wildtrigger()|).
---
--- `autocomplete.predicate` defines a condition of whether to trigger completion
--- at the current command line state. Takes a table with input data and should
--- return `true` to show completion and `false` otherwise. Will be called before
--- the possible delay at current command line state.
--- Default: |MiniCmdline.default_autocomplete_predicate()|.
---
--- Input data fields:
--- - <line> `(string)` - current command line text. See |getcmdline()|.
--- - <pos> `(number)` - current command line column. See |getcmdpos()|.
--- - <line_prev> `(string)` - command line text before the latest change.
--- - <pos_prev> `(number)` - command line column before the latest cursor move.
---
--- Example of blocking completion based on completion type (as some may be slow): >lua
---
--- local block_compltype = { shellcmd = true }
--- require('mini.cmdline').setup({
--- autocomplete = {
--- predicate = function()
--- return not block_compltype[vim.fn.getcmdcompltype()]
--- end,
--- },
--- })
--- <
--- Similar approach can be used to enable completion only for normal Ex commands.
--- Use `return vim.fn.getcmdtype() == ':'` as a predicate output.
---
---# Autocorrect ~
---
--- `config.autocorrect` is used to configure autocorrection: automatic adjustment
--- of bad words as you type them. This works only when appending single character
--- at the end of the command line. Editing already typed words does not trigger
--- autocorrect (allows correcting the autocorrection).
---
--- When to autocorrect is computed automatically based on |getcmdcomplpat()| after
--- every key press: if it doesn't add the character to completion pattern, then
--- the pattern before the key press is attempted to be corrected.
--- There is also an autocorrection attempt for the last word just before
--- executing the command.
---
--- Notes:
--- - This is intended mostly for fixing typos and not as a shortcut for fuzzy
--- matching. Performing the latter automatically is too intrusive. Explicitly
--- use fuzzy completion for that (set up by default).
---
--- - Default autocorrection is done only for words that must come from a fixed
--- set of candidates (like commands and options) by choosing the one with
--- the lowest string distance.
--- See |MiniCmdline.default_autocorrect_func()| for details.
---
--- - Word that matches some Command-line |abbreviation| is not autocorrected.
---
--- - If current command expects only a single argument (like |:colorscheme|), then
--- autocorrection will happen only just before executing the command.
---
--- `autocorrect.func` is a function that can be used to customize autocorrection.
--- Takes a table with input data and should return a string with the correct word
--- or `nil` for no autocorrection. Default: |MiniCmdline.default_autocorrect_func()|.
---
--- Input data fields:
--- - <word> `(string)` - word to be autocorrected. Never empty string.
--- - <type> `(string)` - word type. Output of |getcmdcompltype()|.
---
---# Autopeek ~
---
--- `config.autopeek` is used to configure automatic peeking: show command's target
--- range in a floating window. The window will appear above command line and show
--- current buffer with the focus on left and right (if present and differs from
--- left) range lines.
---
--- `autopeek.n_context` defines how many lines to show above and below the target.
--- The range itself is visualized by default with the statuscolumn signs.
--- Default: 1.
---
--- `autopeek.predicate` defines a condition of whether to show peek window at
--- the current command line state. Takes a table with input data and should
--- return `true` to peek and `false` otherwise.
--- Will be called only if it is possible to parse range from the current command
--- line text and it is for buffer lines (no command or |:command-addr| is `lines`)
--- Default: |MiniCmdline.default_autopeek_predicate()|.
---
--- Input data fields:
--- - <left> `(number)` - left range edge. Not necessarily smallest.
--- - <right> `(number)` - right range edge. Same as `left` for a single line range.
--- - <cmd> `(string)` - full command name. Can be empty string if no valid
--- command is (yet) entered.
---
--- `autopeek.window` defines behavior of a peek window.
--- `autopeek.window.config` is a table defining floating window characteristics
--- or a callable returning such table.
--- It should have the same structure as in |nvim_open_win()|.
---
--- `autopeek.window.statuscolumn` is a special function that can be used to
--- customize |'statuscolumn'| value for the peek window. Takes a table with input
--- data and should return a string to display for line |v:lnum|.
--- Default: |MiniCmdline.default_autopeek_statuscolumn()|.
--- Input data fields are the same as for `autopeek.predicate`.
---
--- Example of showing `<` and `>` signs on range lines: >lua
---
--- function(data)
--- local n, l, r = vim.v.lnum, data.left, data.right
--- local s = n == l and (n == r and '* ' or '< ') or n == r and '> ' or ''
--- -- Needs explicit highlighting via `:h 'statusline'` syntax
--- return '%#MiniCmdlinePeekSign#' .. s
--- end
--- <
--- Notes:
--- - Peek window directly shows current buffer, which means that all its
--- extmarks, virtual text, virtual lines, etc. are also shown.
--- - Non-zero context might work unreliably if there are virtual lines.
--- - Peeking intentionally hides Visual selection if Command-line mode is entered
--- directly from it. Peeking `'<,'>` range already visualizes the selection.
--- To disable autopeek for this case, add the following code BEFORE
--- executing `require('mini.cmdline').setup()`: >lua
---
--- local disable = vim.schedule_wrap(function()
--- local is_from_visual = vim.startswith(vim.fn.getcmdline(), "'<,'>")
--- MiniCmdline.config.autopeek.enable = not is_from_visual
--- end)
--- local reenable = function() MiniCmdline.config.autopeek.enable = true end
---
--- vim.api.nvim_create_autocmd('CmdlineEnter', { callback = disable })
--- vim.api.nvim_create_autocmd('CmdlineLeave', { callback = reenable })
--- <
MiniCmdline.config = {
-- Autocompletion: show `:h 'wildmenu'` as you type
autocomplete = {
enable = true,
-- Delay (in ms) after which to trigger completion
-- Neovim>=0.12 is recommended for positive values
delay = 0,
-- Custom rule of when to trigger completion
predicate = nil,
-- Whether to map arrow keys for more consistent wildmenu behavior
map_arrows = true,
},
-- Autocorrection: adjust non-existing words (commands, options, etc.)
autocorrect = {
enable = true,
-- Custom autocorrection rule
func = nil,
},
-- Autopeek: show command's target range in a floating window
autopeek = {
enable = true,
-- Number of lines to show above and below range lines
n_context = 1,
-- Custom rule of when to show peek window
predicate = nil,
-- Window options
window = {
-- Floating window config
config = {},
-- Function to render statuscolumn
statuscolumn = nil,
},
},
}
--minidoc_afterlines_end
--- Default autocompletion predicate
---
---@param data table Input autocompletion data. As described in |MiniCmdline.config|.
---@param opts table|nil Options. Reserved for future use.
---
---@return boolean If command line does not (yet) contain a letter - `false`,
--- otherwise - `true`. This makes autopeek easier to use for a numerical range.
MiniCmdline.default_autocomplete_predicate = function(data, opts) return data.line:find('%a') ~= nil end
--- Default autocorrection function
---
--- - Return input word if `opts.strict_type=true` and input `type` is not proper.
--- - Get candidates via `opts.get_candidates()`.
--- Default: mostly via |getcompletion()| with empty pattern and input `type`.
--- Exceptions are `help` and `option` types: both list all available candidates
--- in their own ways.
--- - Choose the candidate with the lowest Damerau–Levenshtein distance
--- (smallest number of deletion/insertion/substitution/transposition needed
--- to transform one word into another; slightly prefers transposition).
--- Notes:
--- - Type `'command'` also chooses from all valid candidate abbreviations.
--- - Comparison is done both respecting and ignoring case.
---
---@param data table Input autocorrection data. As described in |MiniCmdline.config|.
---@param opts table|nil Options. Possible fields:
--- - <strict_type> `(boolean)` - whether to restrict output only for types which
--- must have words from a fixed set of candidates (like command or option
--- names). Note: does not include `help` type since |:help| already has
--- "sophisticated algorithm" to handle typos. Default: `true`.
--- - <get_candidates> `(function)` - source of candidates. Will be called
--- with `data` as argument and should return array of string candidates to
--- choose from.
--- Default: for most types - |getcompletion()| with empty pattern and
--- input `type`; for `help` and `option` type - all available help tags and
--- option names (long and short) respectively.
---
---@return string Autocorrected word.
MiniCmdline.default_autocorrect_func = function(data, opts)
H.check_type('data', data, 'table')
H.check_type('data.word', data.word, 'string')
H.check_type('data.type', data.type, 'string')
opts = opts or {}
local strict_type = opts.strict_type == nil or opts.strict_type
if strict_type and not H.autocorrect_strict_types[data.type] then return data.word end
-- Get all valid words
local all = vim.is_callable(opts.get_candidates) and opts.get_candidates(data) or H.get_autocorrect_candidates(data)
if vim.tbl_contains(all, data.word) or data.word == '' then return data.word end
-- Make results stable in case several candidates have the same distance
table.sort(all)
-- Handle commands separately: need dedicated abbreviation length
-- computation and to allow `!`
if data.type == 'command' then return H.get_nearest_command(data.word, all) end
-- Fall back to computing nearest string without allowing abbreviations
local abbr_lens = vim.tbl_map(string.len, all)
return H.get_nearest_abbr(data.word, all, abbr_lens)
end
--- Default autopeek predicate
---
---@param data table Input autopeek data. As described in |MiniCmdline.config|.
---@param opts table|nil Options. Reserved for future use.
---
---@return boolean If command defines |:command-preview| - `false`, otherwise - `true`.
--- This makes autopeek easier to use for commands like |:substitute|,
--- especially if |'inccommand'| is set to `split`.
MiniCmdline.default_autopeek_predicate = function(data, opts)
local cmd_preview_map = H.cache.cmd_preview_map or H.get_cmd_preview_map()
return cmd_preview_map[data.cmd] ~= true
end
--- Default autopeek statuscolumn
---
--- - Show signs next to lines depending on their relation to peeked range.
--- Highlighted with `MiniCmdlinePeekSign` group.
--- - Show line numbers for left and right parts of the range.
--- Highlighted with `MiniCmdlinePeekLineNr` group.
--- - Separate statuscolumn and buffer text with dedicated separator character.
--- Highlighted with `MiniCmdlinePeekSep` group.
---
--- Notes:
--- - Intended to only be used as a part of |'statuscolumn'| function, as it
--- uses |v:lnum| and |v:virtnum| to compute the output.
---
--- Example of adjusting a `mid` sign: >lua
---
--- local peek_stc_opts = { signs = { mid = '+' } }
--- local peek_stc = function(data)
--- return MiniCmdline.default_autopeek_statuscolumn(data, peek_stc_opts)
--- end
--- require('mini.cmdline').setup({
--- autopeek = { window = { statuscolumn = peek_stc } },
--- })
--- <
---@param data table Input peek data. As described in |MiniCmdline.config|.
---@param opts table|nil Options. Possible fields:
--- - <signs> `(table)` - signs to show. Possible fields:
--- - <same> `(string)` - on range if `left=right`. Default: `'🭬'`.
--- - <left> `(string)` - on `left` line. Default: `'┌'`.
--- - <mid> `(string)` - inside range. Default: `'┊'`.
--- - <right> `(string)` - on `right` line. Default: `'└'`.
--- - <out> `(string)` - outside of range. Default: `''` (no sign).
--- - <virt> `(string)` - virtual line. Default: `'•'`.
--- - <wrap> `(string)` - wrapped line. Default: `'↳'`.
--- - <sep> `(string)` - string to put at the end to separate statuscolumn and
--- buffer text. Default: `'│'`
---
--- Note: Any sign and separator should have every `%` escaped as `%%` (due to its
--- special meaning in |'statuscolumn'|).
MiniCmdline.default_autopeek_statuscolumn = function(data, opts)
opts = opts or {}
local signs = opts.signs or {}
local sep_format = opts.sep == nil and '│' or opts.sep:gsub('%%', '%%%%')
if vim.v.virtnum ~= 0 then
local sign = (vim.v.virtnum < 0 and (signs.virt or '•') or (signs.wrap or '↳'))
-- Format as `sign %= sep-hl sep`
return sign .. '%=%#MiniCmdlinePeekSep#' .. sep_format:gsub('%%%%', '%%')
end
local n, l, r = vim.v.lnum, data.left, data.right
-- Format as `sign-hl sign linenr-hl %= linenr sep-hl sep`
local fmt = '%%#MiniCmdlinePeekSign#%s%%#MiniCmdlinePeekLineNr#%%=%s%%#MiniCmdlinePeekSep#' .. sep_format
if n == l and n == r then return string.format(fmt, signs.same or '🭬', n) end
if n == l then return string.format(fmt, signs.left or '┌', n) end
if n == r then return string.format(fmt, signs.right or '└', n) end
if (l < n and n < r) or (r < n and n < l) then return string.format(fmt, signs.mid or '┊', '') end
return string.format(fmt, signs.out or '', '')
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniCmdline.config)
-- Timers
H.timers = {
autocomplete = vim.uv.new_timer(),
}
-- Autocomplete requires `noselect` flag of 'wildmode'. Present in Neovim>=0.11
H.can_autocomplete = vim.fn.has('nvim-0.11') == 1
-- Autocorrect types for which words *must* be from some fixed set
-- Basically a subset of `:h :command-complete` which might lead to an error if
-- word is not from a fixed set. Can be adjusted for more nuances.
-- Reasons for not including a type:
-- - The main reason is because type's usage can be done in context when
-- creating a new object. Like `:edit new-file` for `file` type.
-- - No `help` because there already is an autocorrection with a "sophisticated
-- algorithm to decide which match is better than another one".
-- - No `buffer` because completion candidates only include names for listed
-- buffers. So ids of listed buffers and name+ids of unlisted buffers are
-- missing. Also, partial buffer name is enough for `:buffer`, which would
-- be too cumbersome to account for.
--stylua: ignore
H.autocorrect_strict_types = {
arglist = true, -- file names in argument list
-- augroup = true, -- autocmd groups
-- breakpoint = true, -- |:breakadd| suboptions
-- buffer = true, -- buffer names
color = true, -- color schemes
command = true, -- Ex command (and arguments)
compiler = true, -- compilers
diff_buffer = true, -- diff buffer names
-- dir = true, -- directory names
-- dir_in_path = true, -- directory names in |'cdpath'|
-- environment = true, -- environment variable names
event = true, -- autocommand events
-- expression = true, -- Vim expression
-- file = true, -- file and directory names
-- file_in_path = true, -- file and directory names in |'path'|
filetype = true, -- filetype names |'filetype'|
-- ['function'] = true, -- function name
-- help = true, -- help subjects
-- highlight = true, -- highlight groups
history = true, -- |:history| suboptions
keymap = true, -- keyboard mappings
locale = true, -- locale names (as output of locale -a)
-- lua = true, -- Lua expression |:lua|
mapclear = true, -- buffer argument
-- mapping = true, -- mapping name
-- menu = true, -- menus
messages = true, -- |:messages| suboptions
option = true, -- options
packadd = true, -- optional package |pack-add| names
-- runtime = true, -- file and directory names in |'runtimepath'|
-- scriptnames = true, -- sourced script names
-- shellcmd = true, -- Shell command
-- shellcmdline = true, -- First is a shell command and subsequent ones are filenames
sign = true, -- |:sign| suboptions
syntax = true, -- syntax file names |'syntax'|
syntime = true, -- |:syntime| suboptions
-- tag = true, -- tags
-- tag_listfiles = true, -- tags, file names are shown when CTRL-D is hit
-- user = true, -- user names
-- var = true, -- user variables
}
-- Various cache to use during command line edit
H.cache = {}
-- 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('autocomplete', config.autocomplete, 'table')
H.check_type('autocomplete.enable', config.autocomplete.enable, 'boolean')
H.check_type('autocomplete.delay', config.autocomplete.delay, 'number')
H.check_type('autocomplete.predicate', config.autocomplete.predicate, 'function', true)
H.check_type('autocomplete.map_arrows', config.autocomplete.map_arrows, 'boolean')
H.check_type('autocorrect', config.autocorrect, 'table')
H.check_type('autocorrect.enable', config.autocorrect.enable, 'boolean')
H.check_type('autocorrect.func', config.autocorrect.func, 'function', true)
H.check_type('autopeek', config.autopeek, 'table')
H.check_type('autopeek.enable', config.autopeek.enable, 'boolean')
H.check_type('autopeek.n_context', config.autopeek.n_context, 'number')
H.check_type('autopeek.predicate', config.autopeek.predicate, 'callable', true)
H.check_type('autopeek.window', config.autopeek.window, 'table')
local autopeek_win_config = config.autopeek.window.config
if not (type(autopeek_win_config) == 'table' or vim.is_callable(autopeek_win_config)) then
H.error('`autopeek.window.config` should be table or callable, not ' .. type(autopeek_win_config))
end
H.check_type('autopeek.window.statuscolumn', config.autopeek.window.statuscolumn, 'callable', true)
return config
end
H.apply_config = function(config)
MiniCmdline.config = config
-- Try setting suggested option values
-- NOTE: This makes it more like 'mini.completion' (with 'noselect')
local was_set = vim.api.nvim_get_option_info2('wildmode', { scope = 'global' }).was_set
if not was_set and config.autocomplete.enable and H.can_autocomplete then vim.o.wildmode = 'noselect,full' end
was_set = vim.api.nvim_get_option_info2('wildoptions', { scope = 'global' }).was_set
if not was_set then vim.o.wildoptions = 'pum,fuzzy' end
-- Set useful mappings
if config.autocomplete.map_arrows then
local map_arrow = function(dir, wildmenu_prefix, desc)
local rhs = function() return (vim.fn.wildmenumode() == 1 and wildmenu_prefix or '') .. dir end
vim.keymap.set('c', dir, rhs, { expr = true, desc = desc })
end
map_arrow('<Left>', '<Space><BS>', 'Move cursor left')
map_arrow('<Right>', '<Space><BS>', 'Move cursor right')
-- NOTE: All tests seem to pass without these mappings. But it is mentioned
-- in `:h cmdline-autocompletion`, so probably worth adding
map_arrow('<Up>', '<C-e>', 'Go to earlier history')
map_arrow('<Down>', '<C-e>', 'Go to newer history')
end
end
H.create_autocommands = function()
local gr = vim.api.nvim_create_augroup('MiniCmdline', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
end
-- Act on command line events. Notes:
-- - Prefer 'CursorMovedC' to track command line change as it is triggered
-- both after only position change and after text change. Schedule its
-- callback to work around autcompletion issues with mocking wildchar.
-- - Do not schedule 'CmdlineLeave' to be able to set command text before
-- executing it.
au('CmdlineEnter', '*', H.on_cmdline_enter, 'Act on command line enter')
local update_event = vim.fn.has('nvim-0.11') == 1 and 'CursorMovedC' or 'CmdlineChanged'
au(update_event, '*', vim.schedule_wrap(H.on_cmdline_update), 'Act on command line update')
au('CmdlineLeave', '*', H.on_cmdline_leave, 'Act on command line leave')
au('VimResized', '*', function() H.autopeek(true) end, 'Adjust peek window')
au('CmdwinEnter', '*', function() H.peek_hide() end, 'Hide peek window')
au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
end
H.create_default_hl = function()
local hi = function(name, opts)
opts.default = true
vim.api.nvim_set_hl(0, name, opts)
end
hi('MiniCmdlinePeekBorder', { link = 'FloatBorder' })
hi('MiniCmdlinePeekLineNr', { link = 'DiagnosticSignWarn' })
hi('MiniCmdlinePeekNormal', { link = 'NormalFloat' })
hi('MiniCmdlinePeekSep', { link = 'SignColumn' })
hi('MiniCmdlinePeekSign', { link = 'DiagnosticSignHint' })
hi('MiniCmdlinePeekTitle', { link = 'FloatTitle' })
end
H.is_disabled = function() return vim.g.minicmdline_disable == true or vim.b.minicmdline_disable == true end
H.get_config = function() return vim.tbl_deep_extend('force', MiniCmdline.config, vim.b.minicmdline_config or {}) end
-- Autocommands ---------------------------------------------------------------
H.on_cmdline_enter = function()
-- Check for Command-line mode to not act on `:...` mappings
if H.is_disabled() then return end
-- Act only on "not nested" command line (for simplicity). It can nest after
-- `c_CTRL-R_=`, since there are CmdlineEnter-CmdlineChanged-CmdlineLeave for
-- it without explicit leave-enter for the initial normal Ex command mode.
H.cache.cmdlevel = vim.v.event.cmdlevel
if H.cache.cmdlevel > 1 then return end
-- Schedule for 'CmdlineEnter' to not act on mappings like `:...` (like
-- `:<C-u>...` popular for Visual mode).
H.on_cmdline_leave_scheduled()
end
H.on_cmdline_leave_scheduled = vim.schedule_wrap(function()
-- Check for Command-line mode to not act on `:...` mappings
if H.is_disabled() or vim.fn.mode() ~= 'c' then return end
H.cache = {
buf_id = vim.api.nvim_get_current_buf(),
cmd_preview_map = H.get_cmd_preview_map(),
cmd_type = vim.fn.getcmdtype(),
cmdlevel = H.cache.cmdlevel,
config = H.get_config(),
peek = {},
state = H.get_cmd_state(),
state_prev = H.get_cmd_state(true),
}
H.cache.autocomplete_predicate = H.cache.config.autocomplete.predicate or MiniCmdline.default_autocomplete_predicate
H.cache.buf_is_cmdwin = vim.fn.getbufinfo(H.cache.buf_id)[1].command == 1
H.cache.autopeek_predicate = H.cache.config.autopeek.predicate or MiniCmdline.default_autopeek_predicate
MiniCmdline._peek_statuscolumn = H.make_peek_statuscolumn()
if H.cache.config.autopeek.enable then H.autopeek() end
end)
H.on_cmdline_update = function()
if H.cache.state == nil or H.cache.cmdlevel > 1 then return end
-- Track state only if there was an actual change (line or position)
local state = H.get_cmd_state()
local is_same_line = state.line == H.cache.state.line
if is_same_line and state.pos == H.cache.state.pos then return end
-- Update state accounting for some edge cases
if H.cache.state_prev.compltype == 'option' then H.adjust_option_cmd_state(state) end
H.cache.state_prev, H.cache.state = H.cache.state, state
-- Act only on text change
if is_same_line then return end
local config = H.cache.config
if config.autocomplete.enable and H.can_autocomplete then H.autocomplete() end
if config.autocorrect.enable then H.autocorrect(false) end
if config.autopeek.enable then H.autopeek() end
end
H.on_cmdline_leave = function()
H.cache.cmdlevel = vim.v.event.cmdlevel - 1
if H.cache.state == nil or H.cache.cmdlevel >= 1 then return end
if H.cache.config.autocorrect.enable and not vim.v.event.abort then H.autocorrect(true) end
H.peek_hide()
H.cache = { cmdlevel = vim.v.event.cmdlevel }
MiniCmdline._peek_statuscolumn = nil
end
H.get_cmd_state = function(is_init)
local compltype = vim.fn.getcmdcompltype()
if is_init then return { complpat = '', compltype = compltype, line = '', pos = 1, cmd = {} } end
return { complpat = H.getcmdcomplpat(), compltype = compltype, line = vim.fn.getcmdline(), pos = vim.fn.getcmdpos() }
end
H.adjust_option_cmd_state = function(state)
-- Cases like `set nowrap invmagic` are completed specially. After `no`/`inv`
-- there is a specialized completion only for boolean options. In practice it
-- results into `compltype=''` and `complpat=<text after no/inv>`.
-- This interferes with how autocorrection is detected, as it relies on whole
-- `nowrap` / `invmagic` to be a single complpat *with compltype=option*.
--
-- The solution is to detect cases "it was compltype=option but now it isn't"
-- and try to expand complpat to match the whole word on cursor's left.
if state.compltype == 'option' then return end
state.complpat = state.line:sub(1, state.pos - 1):match(' (%w+)$') or ''
state.compltype = state.complpat ~= '' and 'option' or state.compltype
end
-- Autocomplete ---------------------------------------------------------------
H.autocomplete = function()
H.timers.autocomplete:stop()
-- Do not complete if predicate says so
local state, state_prev = H.cache.state, H.cache.state_prev
local data = { line = state.line, pos = state.pos, line_prev = state_prev.line, pos_prev = state_prev.pos }
if not H.cache.autocomplete_predicate(data) then return H.hide_wild() end
-- Do nothing in some problematic cases (when wildmenu does not work)
-- TODO: Remove after compatibility with Neovim=0.11 is dropped
if H.block_autocomplete() then return end
local delay = H.cache.config.autocomplete.delay
if delay == 0 then return H.trigger_complete() end
H.timers.autocomplete:start(delay, 0, H.trigger_complete_scheduled)
end
H.block_autocomplete = function() return false end
if vim.fn.has('nvim-0.12') == 0 then
H.block_autocomplete = function()
-- Block for non-interactive command type
if H.cache.cmd_type ~= ':' then return true end
-- Block for cases when there is no completion candidates. This affects
-- performance (completion candidates are computed twice), but it is
-- the most robust way of dealing with problematic situations:
-- - Some commands don't have completion defined: `:s`, `:g`, etc.
-- - Some cases result in verbatim `^I` inserted. Like after bang (`:q!`).
--
-- The `vim.fn.getcmdcompltype() == ''` condition is too wide as it denies
-- legitimate cases of when there are available completion candidates.
-- Like in user commands created with `vim.api.nvim_create_user_command()`.
local line_before_pos = H.cache.state.line:sub(1, H.cache.state.pos - 1)
-- `getcompletion` may result in error, like after `:ltag `
local ok, candidates = pcall(vim.fn.getcompletion, line_before_pos, 'cmdline')
return not (ok and #candidates > 0)
end
end
H.trigger_complete = function()
if vim.fn.mode() ~= 'c' then return end
H.trigger_wild()
end
H.trigger_complete_scheduled = vim.schedule_wrap(H.trigger_complete)
H.trigger_wild = function() vim.fn.wildtrigger() end
if vim.fn.has('nvim-0.12') == 0 then
H.trigger_wild = function()
-- Not triggering when wildmenu is shown helps avoiding trigger after
-- manually pressing wildchar (as text is also changes).
if vim.fn.wildmenumode() == 1 then return end
-- Type `<C-z>` which is "Trigger 'wildmode', but always available."
vim.api.nvim_feedkeys('\26', 'nt', false)
end
end
H.hide_wild = function()
-- Ensure that there is no outdated pmenu after `wildtrigger()`
vim.cmd('redraw')
-- This works, but it triggers wildmenu, which is not usable when the point
-- is to hide pmenu if `autocomplete.predicate()` returned `false` ()
-- local keys = (vim.fn.wildmenumode() == 0 and '\26' or '') .. '\5'
-- vim.api.nvim_feedkeys(keys, 'nt', false)
end
if vim.fn.has('nvim-0.12') == 0 then H.hide_wild = function() end end
-- Autocorrect ----------------------------------------------------------------
H.autocorrect = function(is_final)
-- Act only for normal Ex commands after a word is just finished typing
if not (H.cache.cmd_type == ':' and H.cache.state.line:find('%S') ~= nil) then return end
local state, state_prev = H.cache.state, H.cache.state_prev
local line, line_prev = state.line, state_prev.line
local pos, pos_prev = state.pos, state_prev.pos
-- Act only when a char is appended. It makes tweaking autocorrected text
-- easier by going back and editing it. This is also easier to implement.
local is_at_end, was_at_end = (pos - 1) == line:len(), (pos_prev - 1) == line_prev:len()
local new = line:sub(pos_prev)
local is_char_append = is_at_end and was_at_end and line == (line_prev .. new) and vim.fn.strchars(new) <= 1
local is_word_finished = not vim.startswith(state.complpat, state_prev.complpat)
if not (is_char_append and (is_word_finished or is_final)) then return end
-- Compute autocorrection
local state_to_use = is_final and state or state_prev
local word = state_to_use.complpat
if word == '' or vim.fn.maparg(word, 'c', true) ~= '' then return end
local func = H.cache.config.autocorrect.func or MiniCmdline.default_autocorrect_func
local new_word = func({ word = word, type = state_to_use.compltype }) or word
if word == new_word then return end
if type(new_word) ~= 'string' then return H.notify('Can not autocorrect for ' .. vim.inspect(word), 'WARN') end
-- Set corrected word
local init_pos = state_to_use.pos
local new_line = line:sub(1, init_pos - word:len() - 1) .. new_word .. line:sub(init_pos)
-- - Use `noautocmd` to not conflict with `autocomplete` on `CmdlineChanged`
local cmd = string.format('call setcmdline(%s, %s)', vim.inspect(new_line), new_line:len() + 1)
vim.cmd('noautocmd ' .. cmd)
end
H.get_autocorrect_candidates = function(data)
if data.type == 'help' then
local help_buf = vim.api.nvim_create_buf(false, true)
vim.bo[help_buf].buftype = 'help'
-- - NOTE: no dedicated buffer name because it is immediately wiped out
local tags = vim.api.nvim_buf_call(help_buf, function() return vim.fn.taglist('.*') end)
vim.api.nvim_buf_delete(help_buf, { force = true })
return vim.tbl_map(function(x) return x.name end, tags)
end
if data.type == 'option' then
local all = {}
for name, info in pairs(vim.api.nvim_get_all_options_info()) do
table.insert(all, name)
local is_bool = info.type == 'boolean'
table.insert(all, is_bool and ('no' .. name) or nil)
table.insert(all, is_bool and ('inv' .. name) or nil)
local has_shortname = info.shortname ~= ''
table.insert(all, has_shortname and info.shortname or nil)
table.insert(all, (has_shortname and is_bool) and ('no' .. info.shortname) or nil)
table.insert(all, (has_shortname and is_bool) and ('inv' .. info.shortname) or nil)
end
return all
end
local ok, all = pcall(vim.fn.getcompletion, '', data.type)
return ok and all or { data.word }
end
H.get_nearest_command = function(ref, all)
-- Do not alter `:=` command, as it is a special Lua shorthand command
if ref:sub(1, 1) == '=' then return ref end
-- Allow trailing special punctuation (specific to commands)
local word, suffix = ref:match('^(.+)([!|]?)$')
-- Account for the fact that commands can be abbreviated (`:h |20.2|`):
local cmd_abbr_lens, usr_cmds, usr_max_len = {}, {}, 0
for _, cmd in ipairs(all) do
-- User command abbreviation needs to uniquely identify command name
if cmd:find('^[A-Z]') ~= nil then
usr_cmds[cmd] = true
usr_max_len = math.max(usr_max_len, cmd:len())
else
-- ANY abbreviation of ANY built-in command is a valid command (may be
-- different; `wincmd`->`w`==`write`). Its an internal optimization.
-- EXCEPT: `:def` abbreviation of `:defer` is not allowed.
cmd_abbr_lens[cmd] = cmd == 'defer' and 4 or 1
end
end
-- Slice user commands with increasing abbreviation length to find which
-- ones can be uniquely identified by it
for cur_abbr_len = 1, usr_max_len do
local cur_abbrs = {}
for cmd, _ in pairs(usr_cmds) do
local abbr = cmd:sub(1, cur_abbr_len)
cur_abbrs[abbr] = cur_abbrs[abbr] or {}
table.insert(cur_abbrs[abbr], cmd)
end
for _, cmd_arr in pairs(cur_abbrs) do
if #cmd_arr == 1 then
local cmd = cmd_arr[1]
cmd_abbr_lens[cmd] = math.min(cur_abbr_len, cmd:len())
usr_cmds[cmd] = nil
end
end
end
local abbr_lens = vim.tbl_map(function(x) return cmd_abbr_lens[x] end, all)
return H.get_nearest_abbr(word, all, abbr_lens) .. suffix
end
H.get_nearest_abbr = function(word, candidates, abbr_lens)
local tolower = vim.fn.tolower
local word_split = vim.split(word, '')
local word_split_l = vim.split(tolower(word), '')
-- Prefer closest string respecting case first, then try ignorecase
local res, res_dist = nil, math.huge
for i, cand in ipairs(candidates) do
local min_abbr_len = abbr_lens[i]
local d, abbr_len = H.string_abbr_dist(word_split, vim.split(cand, ''), min_abbr_len)
if d < res_dist then
res, res_dist = cand:sub(1, abbr_len), d
end
end
for i, cand in ipairs(candidates) do
local min_abbr_len = abbr_lens[i]
local cand_word_l = tolower(cand)
local d_l, abbr_len_l = H.string_abbr_dist(word_split_l, vim.split(cand_word_l, ''), min_abbr_len)
if d_l < res_dist then
res, res_dist = cand:sub(1, abbr_len_l), d_l
end
end
return res
end
H.string_abbr_dist = function(ref, cand, min_abbr_len)
-- Source: https://en.wikipedia.org/wiki/Damerau-Levenshtein_distance
-- d[i][j] - distance between `ref[1:i]` and `cand[1:j]` abbreviations
local d = {}
for i = 0, #ref do
d[i] = { [0] = i }
end
for j = 0, #cand do
d[0][j] = j
end
for i = 1, #ref do
for j = 1, #cand do
local cost = ref[i] == cand[j] and 0 or 1
-- Account for deletion, insertion, substitution
d[i][j] = math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost)
-- Account for transposition. Slightly favor them over others, as it is
-- a common source for autocorrection
if i > 1 and j > 1 and ref[i] == cand[j - 1] and ref[i - 1] == cand[j] then
d[i][j] = math.min(d[i][j], d[i - 2][j - 2] + 0.99 * cost)
end
end
end
-- Find the candidate abbreviation with the smallest distance
local abbr_d = d[#ref]
local dist, abbr_len = math.huge, nil
for j = min_abbr_len, #cand do
if abbr_d[j] < dist then
dist, abbr_len = abbr_d[j], j
end
end
return dist, abbr_len
end
-- Autopeek -------------------------------------------------------------------
H.autopeek = function(force)
-- Decide if peek needs to be shown or hidden
if not (H.cache.cmd_type == ':' and not H.cache.buf_is_cmdwin) then return end
local line = H.cache.state.line
if line:find('%S') == nil then H.peek_hide() end
local parsed = H.parse_cmd(line)
local range, cmd = parsed.range, parsed.cmd
if range[1] == nil and range[2] == nil then return H.peek_hide() end
-- Normalize range lines
local n_lines = vim.api.nvim_buf_line_count(0)
local left = H.clamp(range[1] or range[2], 1, n_lines)
local right = H.clamp(range[2] or range[1], 1, n_lines)
local from = right < left and right or left
local to = right < left and left or right
local data = { left = left, right = right, cmd = parsed.cmd }
-- Do not peek if predicate says so
if not H.cache.autopeek_predicate(data) then return H.peek_hide() end
-- Force peek update if command line height has changed when typing
local cmdheight = math.ceil((vim.fn.strdisplaywidth(line) + 1) / vim.o.columns)
cmdheight = math.max(cmdheight, vim.o.cmdheight)
force = force or cmdheight ~= H.cache.peek.cmdheight
-- Skip peek update if command line state is the same (for performance)
local cur_data = H.cache.peek.data or {}
local is_data_same = left == cur_data.left and right == cur_data.right and cmd == cur_data.cmd
if not force and is_data_same then return end
H.cache.peek.cmdheight = cmdheight
H.cache.peek.data = data
H.cache.peek.win_id = H.peek_show(from, to)
end
H.peek_show = function(from, to)
-- Compute height to show union of left and right contexts plus possible fold
local n_context = math.max(H.cache.config.autopeek.n_context, 0)
local n_lines = vim.api.nvim_buf_line_count(0)
local cont_from = H.peek_new_context(from, n_context, n_lines)
local cont_to = H.peek_new_context(to, n_context, n_lines)
local height = H.peek_get_height(cont_from, cont_to)
-- Ensure opened window of enough height.
-- NOTE: This uses current buffer directly, which means that all its extmarks
-- are shown. This is both good (for extra text highlighting) and bad
-- (virtual lines and text) thing. Ideally, extmark "owners" should take care
-- of where they should be shown. Usually setting something like "only show
-- inside this particular window" makes sense. Like in 'mini.diff' and
-- 'mini.indentscope'. This is currently not (robustly) possible. Sources:
-- - https://github.com/neovim/neovim/issues/19654
-- - https://github.com/neovim/neovim/pull/27361
-- - https://github.com/neovim/neovim/pull/28432
-- - https://github.com/neovim/neovim/pull/28728
local config = H.peek_get_config(height)
local win_id = H.cache.peek.win_id
win_id = H.is_valid_win(win_id) and win_id or vim.api.nvim_open_win(0, false, config)
vim.api.nvim_win_set_config(win_id, config)
0