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

Skip to content

Latest commit

 

History

History
2171 lines (1934 loc) · 88.6 KB

input.lua

File metadata and controls

2171 lines (1934 loc) · 88.6 KB
--- *mini.input* Get user input
---
--- MIT License Copyright (c) 2026 Evgeni Chasnovski
--- Features:
---
--- - Get user input with fully customizable key and view handling.
---
--- - Built-in configurable views as floating window, statusline/tabline/winbar,
--- virtual line/text.
---
--- - Implementation is non-blocking but waits to return the input. It also works
--- in any mode without requiring mode change. See |MiniInput-lifecycle|.
---
--- - |vim.ui.input()| implementation. To adjust, use |MiniInput.ui_input()| or
--- save-restore `vim.ui.input` manually after calling |MiniInput.setup()|.
---
--- Sources with more details:
--- - |MiniInput.get()|
--- - |MiniInput.default_key()|
--- - |MiniInput-state|
--- - |MiniInput-examples|
--- - |MiniInput-in-other-plugins| (for plugin authors)
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.input').setup({})` (replace `{}`
--- with your `config` table). It will create global Lua table `MiniInput` which
--- you can use for scripting or manually (with `:lua MiniInput.*`).
---
--- See |MiniInput.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.miniinput_config` which should have same structure as
--- `MiniInput.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - [folke/snacks.nvim#input](https://github.com/folke/snacks.nvim):
--- - Both provide |vim.ui.input()| implementation.
--- - Has asynchronous implementation (i.e. does not wait for user to finish
--- input), while this module has synchronous non-blocking implementation.
--- - Uses floating window and forced Insert mode. This module allows more
--- view customizations and can be used in any mode without interruptions.
---
--- - |input()|:
--- - Both are synchronous.
--- - Both allow custom highlight and completion.
--- - This module also allows supplying input scope and visibility,
--- customizing keys and view.
---
--- # Highlight groups ~
--- *MiniInput-hl-groups*
---
--- - `MiniInputAdded` - added text during completion navigation.
--- - `MiniInputBorder` - border of a |MiniInput.gen_view.floatwin()| handler.
--- - `MiniInputCaret` - caret symbol shown in a prompt area.
--- - `MiniInputHide` - input is hidden, usually used instead of `MiniInputPrompt`.
--- - `MiniInputHint` - hints shown during completion navigation.
--- - `MiniInputNormal` - basic foreground/background.
--- - `MiniInputPrompt` - input prompt (intention of the input).
--- - `MiniInputSpecial` - special keys (like literal `\t`, `\n`, etc.) in input.
---
--- # Using in other plugins ~
--- *MiniInput-in-other-plugins*
---
--- - Prefer using |vim.ui.input()| for more user coverage. Use |MiniInput.get()|
--- only when getting input synchronously is absolutely necessary.
---
--- - Perform a `_G.MiniInput ~= nil` check before using any feature. This ensures
--- that user explicitly set up the module.
---@tag MiniInput
--- Information about the state of the input. It is passed as a handler argument.
--- Use |MiniInput.get_state()| to get information about the current state (if any).
--- A table with the following fields:
---
--- - <caret> `(number)` - character (not byte) index at which to modify input.
--- Should be from 1 (prepend input) to "input width plus 1" (append input).
---
--- - <complete> `(table|nil)` - information about active completion navigation.
--- If present, it means that completion navigation is in action.
--- Its fields describe the state of navigation:
--- - <base> `(string)` - reference text to the left of caret at the start
--- of completion. Used to compute candidates. Can be empty string.
--- - <id> `(number)` - identifier of current completion item. Can be zero to
--- mean that the base is shown. If not zero, must mean that a `items[id]`
--- candidate is now shown to the left of caret as the part of the input.
--- - <items> `(table)` - string array of completion candidates. May be empty.
--- Are intended to fully replace <base>, so should contain it in any sense.
--- - <method> `(string)` - completion method. Like `"default"`, `"history"`, etc.
--- Can be `""` (empty string) to mean "default method of complete handler".
---
--- - <data> `(table)` - any information to be reused within the same input session.
--- Note: handlers should not change fields that they don't "own".
---
--- - <errmsg> `(errmsg)` - first error message caught during input process.
---
--- - <highlight> `(table|nil)` - information about current input highlighting.
--- If absent, the whole input is highlighted using `MiniInputNormal` group.
--- Should be an array of highlight ranges. They might be not ordered, overlap,
--- go outside of input width. It is up to the view handler to decide how to
--- interpret them. See |MiniInput.state_to_chunks()| for a helper.
--- Fields of a single highlight range:
--- - <from> `(number)` - character index (one-indexed) of range start.
--- - <to> `(number)` - character index (one-indexed) of range end (inclusive).
--- Should not be smaller than <from>. Can be |math.huge|.
--- - <hl> `(string)` - highlight group to use for highlighting.
---
--- - <input> `(string)` - current user input. Returned by |MiniInput.get()|.
---
--- - <opts> `(table)` - input options, same as in |MiniInput.get()|.
---
--- - <prompt> `(string)` - intention of the input. May be empty.
---
--- - <status> `(string)` - one of `"start"`, `"progress"`, `"accept"`, `"cancel"`.
---@tag MiniInput-state
--- # General ~
---
--- ## Initial input ~
---
--- Use `opts.init_keys` to imitate the initial state of the input: >lua
---
--- MiniInput.get({ init_keys = { 'Default' } })
--- <
--- ## Custom mappings ~
---
--- Override `handlers.key` in |MiniInput.config|: >lua
---
--- local key_handler = function(state, key)
--- -- <C-a> - move caret to start of line
--- if key == '\1' then
--- state.caret = 1
--- -- <S-BS> - clear all input
--- elseif key == vim.keycode('<S-BS>') then
--- state.input, state.caret = '', 1
--- else
--- -- IMPORTANT: Fall back to processing as usual
--- return MiniInput.default_key(state, key)
--- end
--- end
---
--- require('mini.input').setup({ handlers = { key = key_handler } })
--- <
--- ## Basic command line ~
---
--- An alternative |Command-line| with highlighting and completion: >lua
---
--- -- Construct reusable `MiniInput.get()` options
--- local cmdline_opts = { prompt = 'Command', scope = 'editor' }
--- -- - Highlight using bundled Vim tree-sitter parser and default handler
--- local highlight_vim = MiniInput.gen_highlight.treesitter('vim')
--- local highlight_cmdline = function(state)
--- state = highlight_vim(state) or state
--- return MiniInput.default_highlight(state) or state
--- end
--- cmdline_opts.handlers = { highlight = highlight_cmdline }
--- -- - Complete as if it is Command line input
--- cmdline_opts.completion = 'cmdline'
---
--- -- Create a mapping for `:`
--- local input_cmdline = function()
--- local cmd = MiniInput.get(cmdline_opts)
--- if cmd ~= nil then vim.cmd(cmd) end
--- end
--- vim.keymap.set('n', ':', input_cmdline)
--- <
--- # Handlers ~
---
--- ## Key ~
---
--- Perform custom actions based on arbitrary conditions: >lua
---
--- local key_handler = function(state, key)
--- -- Adjust prompt
--- state.opts.prompt = state.opts.prompt:gsub('[?:]%s*$', '')
---
--- -- Adjust scope
--- if state.opts.prompt == 'Editor action' then
--- state.opts.scope = 'editor'
--- end
---
--- -- Hide from view and history
--- if state.opts.prompt:find('[Pp]assword') ~= nil then
--- state.opts.hide = true
--- end
---
--- -- IMPORTANT: Process as usual
--- state = MiniInput.default_key(state, key) or state
---
--- -- Auto fill and accept
--- if state.input == 'AF' then
--- state.input, state.status = 'Autofilled input', 'accept'
--- end
--- end
--- require('mini.input').setup({ handlers = { key = key_handler } })
--- <
--- ## View ~
---
--- Show no view: >lua
---
--- require('mini.input').setup({ handlers = { view = function() end } })
--- <
--- Compute initial style depending on scope: >lua
---
--- local input = require('mini.input')
--- local view_virtline = input.gen_view.virtual({ style = 'above' })
--- local view_tabline = input.gen_view.uiline({ style = 'tabline' })
--- local view_winbar = input.gen_view.uiline({ style = 'winbar' })
--- local view_handler = function(state)
--- -- NOTE: does not support interactive scope change
--- local scope, view = state.opts.scope, view_tabline
--- if scope == 'buffer' or scope == 'window' then view = view_winbar end
--- if scope == 'cursor' or scope == 'line' then view = view_virtline end
--- return view(state)
--- end
---
--- input.setup({ handlers = { view = view_handler } })
--- <
--- Change symbols for caret and hidden input: see |MiniInput.gen_view|.
---@tag MiniInput-examples
---@alias __input_to_chunks - <to_chunks> `(function)` - a function that takes |MiniInput-state| and
--- `max_width` arguments and returns an array of `{ text, hl }` chunks that fit
--- into `max_width` display width. See |MiniInput.state_to_chunks()|.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
-- Module definition ==========================================================
local MiniInput = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniInput.config|.
---
---@usage >lua
--- require('mini.input').setup() -- use default config
--- -- OR
--- require('mini.input').setup({}) -- replace {} with your config table
--- <
MiniInput.setup = function(config)
-- Export module
_G.MiniInput = MiniInput
-- 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()
-- Set custom implementation
vim.ui.input = MiniInput.ui_input
-- Adjust terminal emulator's pasting with active input
H.adjust_vim_paste()
end
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Handlers ~
--- *MiniInput.config.handlers*
---
--- `config.handlers` defines functions that are applied during |MiniInput-lifecycle|.
--- Every handler takes |MiniInput-state| as the first argument and is expected to
--- either modify it in place or return a new state table.
---
--- Use |MiniInput.apply_handler()| to apply a handler for a given |MiniInput-state|.
--- They can be set up as part of the config (will be used as default) or passed
--- directly as a part of |MiniInput.get()| call.
---
--- ## Complete ~
---
--- `handlers.complete` is a handler intended to compute completion suggestions.
--- Takes |MiniInput-state| and `method` as arguments and is expected to modify
--- <complete> field. Default: |MiniInput.default_complete()|.
---
--- Only `complete.base` and `complete.items` are expected to be set by a complete
--- handler. Setting and modifying other fields (`complete.id` and `complete.method`)
--- is done in other handlers and as part of |MiniInput.apply_handler()|. Actually
--- showing completion information is up to the view handler.
---
--- This handler is usually applied manually inside a key handler via using
--- `MiniInput.apply_handler(state, 'complete', method)`. Like, for example,
--- in |MiniInput.default_key()| after <Tab> or <Up>.
---
--- Here is an example of a simple demo complete handler: >lua
---
--- local complete_handler = function(state, method)
--- if method == '' or method == 'xy' then
--- local text = vim.fn.strcharpart(state.input, 0, state.caret - 1)
--- local base = text:match('%S*$')
--- state.complete = { base = base, items = { base .. 'x', base .. 'y' } }
--- return
--- end
--- return MiniInput.default_complete(state, method)
--- end
---
--- require('mini.input').setup({ handlers = { complete = complete_handler } })
--- <
--- ## Key ~
---
--- `handlers.key` is a handler intended to process every user key press.
--- Takes |MiniInput-state| and `key` as arguments. Argument `key` represents a key
--- that needs to be processed: a string if from the user input or `nil` if input
--- needs to be set up, refreshed, or torn down. Default: |MiniInput.default_key()|.
---
--- A string `key` can be two kinds:
--- - Forwarded from |getcharstr()| verbatim as a result of interactive key press.
--- Meaning it will be in escaped form and not as a |key-notation|: i.e. `"\r"`
--- and not `"<CR>"`. It also means that all kinds of combos (`<M-...>`, `<C-S-...>`),
--- mouse clicks, and wheel scrolls are also forwarded to key handler.
--- - Any string as part of `opts.init_keys` in |MiniInput.get()|. If a string
--- doesn't look like it came from |getcharstr()|, it is usually a good idea
--- to insert this string at caret as is.
---
--- The suggested overall approach for custom key handler is "if `key` is special -
--- act on it, if can be used in the input - insert at caret, ignore otherwise".
--- It is also important to never change the current mode (as in |vim-modes|)
--- for |MiniInput.get()| to work as expected.
---
--- Here is an example of a basic custom key handler: >lua
---
--- local custom_actions = {
--- [vim.keycode('<Left>')] = function(state)
--- state.caret = math.max(state.caret - 1, 1)
--- end,
--- [vim.keycode('<Right>')] = function(state)
--- local input_width = vim.fn.strchars(state.input)
--- state.caret = math.min(state.caret + 1, input_width + 1)
--- end,
--- [vim.keycode('<CR>')] = function(state) state.status = 'accept' end,
--- [vim.keycode('<Esc>')] = function(state) state.status = 'cancel' end,
--- }
---
--- local key_handler = function(state, key)
--- -- No need for special setup or teardown
--- if key == nil then return end
---
--- -- If key is special - act on it
--- if custom_actions[key] then return custom_actions[key](state) end
---
--- -- If key is not printable - do nothing
--- if vim.fn.match(key, '^[[:print:]]\\+$') < 0 then return end
---
--- -- Insert at caret
--- local caret, input = state.caret, state.input
--- local before_caret = vim.fn.strcharpart(input, 0, caret - 1)
--- local after_caret = vim.fn.strcharpart(input, caret - 1)
--- state.input = before_caret .. key .. after_caret
--- state.caret = caret + vim.fn.strchars(key)
---
--- -- No need to return anything as `state` is modified in place
--- end
---
--- require('mini.input').setup({ handlers = { key = key_handler } })
--- <
--- ## Highlight ~
---
--- `handlers.highlight` is a handler intended to compute and set highlight info
--- about the current input. Takes |MiniInput-state| as the only argument and is
--- expected to modify <highlight> field. Default: |MiniInput.default_highlight()|.
---
--- See |MiniInput.gen_highlight| for built-in highlight handler generators.
---
--- It is usually a good idea to append to a <highlight> if it already exists.
--- This makes it work more robustly when combining highlights.
---
--- Here is a basic example that highlights all letters `a`: >lua
---
--- local hl_handler = function(state)
--- local highlight = {}
--- for col in string.gmatch(state.input, '()a') do
--- -- NOTE: range should use character (not byte) indexes
--- local char_col = vim.fn.charidx(state.input, col)
--- local range = { from = char_col, to = char_col, hl = 'Special' }
--- table.insert(highlight, range)
--- end
---
--- state.highlight = vim.list_extend(state.highlight or {}, highlight)
---
--- -- Possibly also apply default handler afterwards
--- return MiniInput.default_highlight(state)
--- end
---
--- require('mini.input').setup({ handlers = { highlight = hl_handler } })
--- <
--- ## View ~
---
--- `handlers.view` is a handler intended to show the input state on screen.
--- Takes |MiniInput-state| as the only argument. Default: |MiniInput.default_view()|.
---
--- See |MiniInput.gen_view| for built-in view handler generators.
---
--- Example of view that uses |nvim_echo()| to show the input: >lua
---
--- local view_handler = function(state)
--- -- Process start and end of the input lifecycle
--- local is_start = state.status == 'start'
--- local is_end = state.status == 'accept' or state.status == 'cancel'
--- if is_start or is_end then vim.cmd('mode') end
--- if is_end then return end
---
--- -- Compute text-hl chunks that fit and show them
--- local chunks = MiniInput.state_to_chunks(state, vim.v.echospace)
--- vim.api.nvim_echo(chunks, false, {})
--- end
---
--- require('mini.input').setup({ handlers = { view = view_handler } })
--- <
--- # Scope ~
---
--- `config.scope` is a string that defines an input scope. It is meant as an extra
--- information for handlers to tweak their behavior (`view` style, etc.). Possible
--- values: `"cursor"`, `"line"`, `"buffer"`, `"window"`, `"tabpage"`, `"editor"`, `"project"`.
MiniInput.config = {
-- Functions that control input lifecycle
handlers = {
-- Compute completion candidates
complete = nil,
-- Compute highlighting of current input
highlight = nil,
-- Handle input start, every key press, and input end
key = nil,
-- Show current input state
view = nil,
},
-- Default input scope: cursor/line/buffer/window/tabpage/editor/project
scope = 'editor',
}
--minidoc_afterlines_end
--- Get input from the user
---
--- # Lifecycle ~
--- *MiniInput-lifecycle*
---
--- This module implements custom lifecycle to interact with the user. It starts
--- when calling |MiniInput.get()| and ends when a value is returned.
--- Only one active input is allowed simultaneously.
---
--- The basic cycle unit is a step that processes a `key` (string or `nil`). It is
--- done by calling relevant handlers in order: key handler with `key` as a second
--- argument, highlight handler, view handler. |MiniInput.apply_handler()| is used
--- to apply each handler and the output of one is used as the input for the next.
---
--- During a step some state fields are automatically removed:
--- - <highlight> is removed before applying a key handler to always have the most
--- up to date highlighting.
--- - <complete> is removed if it was not changed after applying a key handler but
--- something else in the state besides <highlight> did change. This is meant as
--- an automatic stop of completion when it is not advancing.
---
--- If a step sets an ending state <status> (i.e. `"accept"` or `"cancel"`), the input
--- is finished by extra finishing step (see below).
---
--- The order of operations is as follows:
--- - Create initial |MiniInput-state| based on the input `opts`, with defaults
--- inferred from |MiniInput.config|, and `vim.b.miniinput_config`.
--- - Set <status> to `"start"`.
--- - Advance one step with `key=nil`. This is meant as a "setup" step for handlers.
--- - Set <status> to `"progress"`.
--- - Process `opts.init_keys` one item per step with `key` set to the string item.
--- - Wait for user to press a key (via |getcharstr()|). The key string (in escaped
--- form and not as a |key-notation|; i.e. `"\r"` and not `"<CR>"`) is then used
--- to advance a step. Note: <C-c> is hard coded to cancel the input.
--- - Repeat previous step until the ending <status> (`"accept"` or `"cancel"`).
--- - Finish the input:
--- - Perform a "teardown" step with `key=nil`.
--- - If <errmsg> is set, throw an |error()|.
--- - If input is accepted (even if empty) and not hidden, add <input> to
--- the history. Get the whole history with |MiniInput.get_history()|.
--- - Return <input> if <status> is `"accept"`, `nil` otherwise.
---
---@param opts table|nil Options. Possible fields:
--- - <completion> `(string)` - completion method. Default: `''` to use default
--- completion method of the `complete` handler.
--- - <handlers> `(table)` - same as in |MiniInput.config.handlers|, used only for
--- the duration of the current input.
--- - <hide> `(boolean)` - whether input should be hidden. Default: `false`.
--- Note: this does not guarantee a total security of the input, only that
--- the typed characters are expected to not be shown on screen and not added
--- to the history. If set:
--- - The `view` handler is expected to not directly show current input.
--- Like replace characters with pre-defined string or fully not show.
--- - The `complete` and `highlight` handlers are not called.
--- - Accepted input will not be added to the history.
--- - <init_keys> `(table)` - array of string keys that are emulated before asking
--- for the user input. Using values that can be an output of |getcharstr()|
--- should be preferred, but a key handler should work with any string.
--- Default: `{}`.
--- - <prompt> `(string)` - intention of the input, same as in |input()|.
--- Default: `"Input"`.
--- - <scope> `(string)` - same as in |MiniInput.config|. Default: the value from
--- `MiniInput.config` with some hard coded exceptions (on Neovim<0.12.3):
--- - |vim.lsp.buf.rename()| will use `"cursor"` if no `new_name` is supplied.
---
---@return string|nil User input (from the <input> state field) if accepted, even if
--- empty. `nil` if canceled or there was an active input.
---
---@usage >lua
--- local input = MiniInput.get({
--- -- Intention of the input
--- prompt = 'New value',
--- -- The input is for something at cursor
--- scope = 'cursor',
--- -- Emulate pressing `a`, `<BS>`, and `b`
--- init_keys = { 'a', vim.keycode('<BS>'), 'b' },
--- })
--- <
MiniInput.get = function(opts)
H.check_type('opts', opts, 'table', true)
opts = opts or {}
opts.scope = opts.scope or (_G.MiniInput or {})._temp_default_scope
-- Only allow one input at a time
if H.state ~= nil then return nil end
local init_state = H.state_new(H.get_config(opts))
H.state_validate(init_state)
H.state = init_state
H.handle_step(nil)
if H.state_is_end() then return H.state_finish() end
H.state.status = 'progress'
for _, k in ipairs(H.state.opts.init_keys) do
H.handle_step(k, true)
if H.state_is_end() then return H.state_finish() end
end
H.redraw()
local lmap = H.get_lmap()
for _ = 1, 1000000 do
local key = H.getcharstr(lmap)
if key == nil and not H.state_is_end() then H.state.status = 'cancel' end
if H.state_is_end() then break end
H.handle_step(key)
if H.state_is_end() then break end
end
return H.state_finish()
end
--- A |vim.ui.input()| implementation
---
--- Function which can be used to directly override |vim.ui.input()| to use this
--- module functionality. Set automatically in |MiniInput.setup()|.
---
---@usage To preserve original `vim.ui.input()`: >lua
---
--- local ui_input_orig = vim.ui.input
--- require('mini.input').setup()
--- vim.ui.input = ui_input_orig
--- <
MiniInput.ui_input = function(opts, on_confirm)
opts = opts or {}
local input_opts = { completion = opts.completion, prompt = opts.prompt, scope = opts.scope }
input_opts.handlers = { highlight = H.make_ui_select_hl_fun(opts.highlight) }
input_opts.init_keys = { opts.default }
on_confirm(MiniInput.get(input_opts))
end
--- Get current input state
---
---@return table|nil Current |MiniInput-state| if input is active, `nil` otherwise. Notes:
--- - For hidden input (`state.opts.hide=true`), both <caret> and <input> are `nil`
--- to actually hide the input.
MiniInput.get_state = function()
if H.state == nil then return nil end
local res = H.copy_tables(H.state)
if res.opts.hide then
res.input, res.caret = nil, nil
end
return res
end
--- Get input history
---
---@return table Array with data about all previous non-hidden inputs (from earliest
--- to latest). Each element is a table with the following fields:
--- - <cwd> `(string)` - |current-directory| at the time of input's end.
--- - <input> `(string)` - input result.
--- - <prompt> `(string)` - `opts.prompt` supplied in |MiniInput.get()|.
--- - <scope> `(string)` - `opts.scope` supplied in |MiniInput.get()|.
MiniInput.get_history = function() return vim.deepcopy(H.history) end
--- Set input history
---
---@param history table Array describing all previous inputs. Same structure
--- as |MiniInput.get_history()| output.
MiniInput.set_history = function(history)
H.check_array_of('history', history, 'table')
for i, h in ipairs(history) do
local item = string.format('history[%d]', i)
H.check_type(item .. '.cwd', h.cwd, 'string')
H.check_type(item .. '.input', h.input, 'string')
H.check_type(item .. '.prompt', h.prompt, 'string')
H.check_one_of(item .. '.scope', h.scope, H.allowed_scopes)
end
H.history = vim.deepcopy(history)
end
--- Refresh active input
---
--- Performs one step of |MiniInput-lifecycle| with `key=nil`.
MiniInput.refresh = function()
if H.state == nil then return end
H.handle_step(nil)
if H.state_is_end() then H.state_finish() end
vim.schedule(H.redraw)
end
--- Highlight generators
---
--- This is a table with function elements. Call to actually get a view function.
MiniInput.gen_highlight = {}
--- Highlight with tree-sitter
---
---@param lang string A language of tree-sitter parser to use.
---
---@return function A highlight handler. Seem |MiniInput.config.handlers|.
MiniInput.gen_highlight.treesitter = function(lang)
local append_ts_hl_range = function(arr, line, tstree, tree)
local query = tstree and vim.treesitter.query.get(tree:lang(), 'highlights')
if query == nil then return end
for capture, node in query:iter_captures(tstree:root(), line) do
-- Ignore private captures
if query.captures[capture]:sub(1, 1) ~= '_' then
local _, from, _, to = node:range()
local hl = string.format('@%s.%s', query.captures[capture], query.lang)
from, to = vim.fn.charidx(line, from) + 1, vim.fn.charidx(line, to)
if from <= to then table.insert(arr, { from = from, to = to, hl = hl }) end
end
end
end
return function(state)
local line = state.input
local ok, parser = pcall(vim.treesitter.get_string_parser, line, lang)
if not ok or parser == nil then return end
-- Traverse all trees and compute highlight ranges
parser:parse(true)
local highlight = {}
parser:for_each_tree(function(tstree, tree) append_ts_hl_range(highlight, line, tstree, tree) end)
-- Respect maybe already present <highlight>
state.highlight = vim.list_extend(state.highlight or {}, highlight)
end
end
--- View generators
---
--- This is a table with function elements. Call to actually get a view function.
---
--- Each element accepts <style> option which fine tunes the input view.
--- With default key handler (|MiniInput.default_key()|) it can be adjusted
--- interactively by pressing <C-s>.
---
--- Each element also accepts <to_chunks> option. It is a function that takes
--- a |MiniInput-state| and `max_width` arguments and returns an array of `{ text, hl }`
--- chunks that fit into `max_width` display width (as in |strdisplaywidth()|).
---
--- This is a way to adjust how input is shown. Like caret/hide symbols, etc.
--- By default uses |MiniInput.state_to_chunks()|, which also means:
--- - All control characters (like literal `\t`, `\n`, etc.) will be translated
--- via |keytrans()|.
---
--- Example: >lua
---
--- local input = require('mini.input')
---
--- -- Adjust how state is converted to text-hl chunks
--- local to_chunks_opts = {
--- symbol_caret = '_',
--- symbol_hide = '*',
--- -- Do not include prompt and hint in `floatwin` handler
--- include_prompt = false,
--- include_hint = false,
--- }
--- local to_chunks = function(state, max_width)
--- return MiniInput.state_to_chunks(state, max_width, to_chunks_opts)
--- end
---
--- -- Supply custom `to_chunks` as an option
--- local view_opts = { to_chunks = to_chunks }
--- input.setup({ handlers = { view = input.gen_view.floatwin(view_opts) } })
--- <
MiniInput.gen_view = {}
--- Floating window view
---
--- Show input inside a floating window. Prompt and completion hints are shown
--- in title and footer.
---
--- Window position and dimensions are computed based on state's <scope> and
--- `opts.style`:
--- - `scope="cursor"` is shown near the cursor. For example, `style="BL"` will
--- have bottom left corner at nearest top right cell relative to the cursor.
--- - `scope="line"` is shown near the current line. For example, `style="BL"` will
--- have bottom left corner at left side above the line.
--- - `scope="buffer"` and `scope="window"` will be shown relative to the current
--- window. For example, `style="BL"` will be shown in the bottom left corner.
--- - `scope="tabpage"`, `scope="editor"`, `scope="project"` will be shown relative to
--- the overall Neovim instance. For example, `style="BL"` will be shown in the
--- bottom left corner.
---
--- Identifiers of floating window and its buffer are stored as `floatwin_win_id`
--- and `floatwin_buf_id` in |MiniInput-state| <data> field. Floating window is
--- not focused and cursor position is not the same as caret position.
---
---@param opts table|nil Options. Possible fields:
--- - <adjust_config> `(function)` - function to adjust default config. Will be
--- called with two arguments: current |MiniInput-state| and a window config
--- (always with `relative="editor"` and `anchor="NW"`) computed based
--- on `opts.style`. Should return an adjusted window config.
--- Default: `function(state, config) return config end`.
---
--- - <style> `(string)` - a two character description of how to show the window.
--- Default: `"BL"`.
---
--- First character describes vertical position:
--- - `"T"` - top window border will be at scope's reference top border.
--- - `"M"` - middle between top and bottom window borders will be at the
--- middle of scope's reference top and bottom borders.
--- - `"B"` - bottom window border will be at scope's reference bottom border.
---
--- Second character describes horizontal position:
--- - `"L"` - left window border will be at scope's reference left border.
--- - `"M"` - middle between left and right window borders will be at the
--- middle of scope's reference left and right borders.
--- - `"R"` - right window border will be at scope's reference right border.
---
--- __input_to_chunks
---
---@usage >lua
--- local input = require('mini.input')
---
--- -- Use border different from 'winborder'
--- local adjust_config = function(_, config)
--- config.border = 'double'
--- return config
--- end
---
--- -- Choose initial style based on the scope
--- local floatwin = input.gen_view.floatwin
--- local view_tm = floatwin({ style = 'TM', adjust_config = adjust_config })
--- local view_bl = floatwin({ style = 'BL', adjust_config = adjust_config })
--- local view_handler = function(state)
--- local scope, view = state.opts.scope, view_tm
--- if scope == 'cursor' or scope == 'line' then view = view_bl end
--- return view(state)
--- end
---
--- input.setup({ handlers = { view = view_handler } })
--- <
MiniInput.gen_view.floatwin = function(opts)
local default_opts = { style = 'BL' }
default_opts.adjust_config = function(_, config) return config end
local default_to_chunks_opts = { include_prompt = false, include_hint = false }
default_opts.to_chunks = function(state, max_width)
return MiniInput.state_to_chunks(state, max_width, default_to_chunks_opts)
end
opts = vim.tbl_extend('force', default_opts, opts or {})
H.check_type('opts.adjust_config', opts.adjust_config, 'callable')
H.check_one_of('opts.style', opts.style, { 'TL', 'TM', 'TR', 'ML', 'MM', 'MR', 'BL', 'BM', 'BR' })
H.check_type('opts.to_chunks', opts.to_chunks, 'callable')
-- Change style in vertical-horizontal order
local all_styles = { 'BL', 'ML', 'TL', 'BM', 'MM', 'TM', 'BR', 'MR', 'TR' }
return function(state)
if H.state_is_end(state) then
pcall(vim.api.nvim_win_close, state.data.floatwin_win_id, true)
pcall(vim.api.nvim_buf_delete, state.data.floatwin_buf_id, { force = true })
return
end
local style, _ = H.handle_view_style(state, opts.style, all_styles)
-- Try to fit all chunks first, but later still truncate to window width
local winborder = vim.fn.exists('+winborder') == 0 and '' or vim.o.winborder
default_to_chunks_opts = { include_prompt = winborder == 'none', include_hint = winborder == 'none' }
local chunks = H.get_chunks(opts, state)
local default_config = H.default_floatwin_config(state, style, H.get_chunks_displaywidth(chunks))
local config = opts.adjust_config(state, default_config)
chunks = H.get_chunks(opts, state, config.width)
H.ensure_floatwin_buf(state, chunks)
H.ensure_floatwin_win(state, config)
end
end
--- UI line (statusline, tabline, winbar) view
---
---@param opts table|nil Options. Possible fields:
--- - <style> `(string)` - which UI line to use. One of `"statusline"`,
--- `"tabline"`, `"winbar"`. Default: `"statusline"`.
---
--- __input_to_chunks
---
---@usage >lua
--- local input = require('mini.input')
---
--- -- Choose initial style based on the scope
--- local view_tabline = input.gen_view.uiline({ style = 'tabline' })
--- local view_winbar = input.gen_view.uiline({ style = 'winbar' })
--- local view_handler = function(state)
--- local scope, view = state.opts.scope, view_winbar
--- if scope == 'tabpage' or scope == 'editor' or scope == 'project' then
--- view = view_tabline
--- end
--- return view(state)
--- end
---
--- input.setup({ handlers = { view = view_handler } })
--- <
MiniInput.gen_view.uiline = function(opts)
opts = vim.tbl_extend('force', { style = 'statusline', to_chunks = MiniInput.state_to_chunks }, opts or {})
H.check_one_of('opts.style', opts.style, { 'statusline', 'tabline', 'winbar' })
H.check_type('opts.to_chunks', opts.to_chunks, 'callable')
local all_styles = { 'statusline', 'winbar', 'tabline' }
local escape_stl = function(x) return (x:gsub('%%', '%%%%')) end
return function(state)
local style, style_is_new = H.handle_view_style(state, opts.style, all_styles)
H.uiline_handle_option_values(state, style_is_new)
if H.state_is_end(state) then return end
local max_width = style == 'tabline' and vim.o.columns or vim.fn.winwidth(0)
local chunks = H.get_chunks(opts, state, max_width)
local parts = vim.tbl_map(function(c) return '%#' .. escape_stl(c[2]) .. '#' .. escape_stl(c[1]) end, chunks)
local uiline_value = table.concat(parts) .. '%#MiniInputNormal#'
local opt = style == 'tabline' and vim.o or vim.wo
opt[style] = uiline_value
if style == 'statusline' and vim.o.laststatus < 2 then vim.o.laststatus = 2 end
if style == 'tabline' and vim.o.showtabline < 2 then vim.o.showtabline = 2 end
end
end
--- Virtual (line, text) view
---
---@param opts table|nil Options. Possible fields:
--- - <style> `(string)` - how to display virtual text. One of `"above"`, `"below"`,
--- `"inline"`. Default: `"above"`.
---
--- __input_to_chunks
---
---@usage >lua
--- -- Choose different initial style
--- local input = require('mini.input')
--- local view_handler = input.gen_view.virtual({ style = 'inline' })
--- input.setup({ handlers = { view = view_handler } })
--- <
MiniInput.gen_view.virtual = function(opts)
opts = vim.tbl_extend('force', { style = 'above', to_chunks = MiniInput.state_to_chunks }, opts or {})
H.check_one_of('opts.style', opts.style, { 'above', 'below', 'inline' })
H.check_type('opts.to_chunks', opts.to_chunks, 'callable')
local ns_id = H.ns_id.view
local all_styles = { 'above', 'below', 'inline' }
return function(state)
-- Cleanup
local buf_id, extmark_id = vim.api.nvim_get_current_buf(), state.data.extmark_id
if H.state_is_end(state) then
pcall(vim.api.nvim_buf_del_extmark, state.data.buf_id, ns_id, extmark_id)
return
end
local ok_extmark = pcall(vim.api.nvim_get_extmark_by_id, state.data.buf_id, ns_id, extmark_id, {})
if not (buf_id == state.data.buf_id and ok_extmark) then
pcall(vim.api.nvim_buf_del_extmark, state.data.buf_id, ns_id, extmark_id)
end
state.data.buf_id = buf_id
local style, style_is_new = H.handle_view_style(state, opts.style, all_styles)
local is_virtline = style == 'above' or style == 'below'
-- Get chunks
local win_info = H.get_curwin_info()
local max_width = win_info.width - win_info.textoff
local chunks = H.get_chunks(opts, state, max_width)
local chunks_width = H.get_chunks_displaywidth(chunks)
if is_virtline and chunks_width < max_width then
chunks = vim.deepcopy(chunks)
table.insert(chunks, { string.rep(' ', max_width - chunks_width), 'MiniInputNormal' })
end
-- Set
local extmark_opts = { id = state.data.extmark_id, priority = 4096 }
if is_virtline then
extmark_opts.virt_lines = { chunks }
extmark_opts.virt_lines_above = style == 'above'
else
extmark_opts.virt_text = chunks
extmark_opts.virt_text_pos = 'inline'
end
local cur_pos = vim.api.nvim_win_get_cursor(0)
state.data.extmark_id = vim.api.nvim_buf_set_extmark(0, ns_id, cur_pos[1] - 1, cur_pos[2], extmark_opts)
-- Ensure that both current and virtual lines are visible
if is_virtline and (state.status == 'start' or style_is_new) then
local win_line, win_height = vim.fn.winline(), vim.fn.winheight(0)
if win_line == 1 and style == 'above' then vim.cmd('normal! \25') end
-- With 'above' the cursor line is moved down, with 'below' - no move
-- local is_below_screen = style == 'above' and win_line > win_height or win_line >= win_height
local is_below_screen = (win_line - (style == 'above' and 1 or 0)) >= win_height
if is_below_screen then vim.cmd('normal! \5') end
end
end
end
--- Default key handler
---
--- Emulates most of |Command-line-mode| editing (|cmdline-editing|):
---
--- - Accept: <CR>. To insert literal newline, type `<C-j>`.
---
--- - Cancel: <Esc> or <C-c>.
---
--- - Move caret:
--- - <Left>, <Right> - one character to left / right.
--- - <M-h>, <M-l> - one character to left / right.
--- - <S-Left>, <S-Right> - one word to left / right.
--- - <C-b>, <C-e> (if no completion) - to start / end of input.
--- - <Home>, <End> - to start / end of input.
---
--- - Delete:
--- - <BS> / <C-h> - to caret's left. If `opts.autopair` is enabled, also delete
--- a character to caret's right if it formed a respected character pair.
--- See "Autopair".
--- - <Del> - at caret.
--- - <C-u> - from start to caret. As |c_CTRL-U|.
--- - <C-w> - contiguous keyword or non-keyword to caret's left. As |c_CTRL-W|.
---
--- - Insert at caret:
--- - <C-k> - digraph based on the next two pressed keys. As |c_CTRL-K|.
--- - <C-r> - content of a register. As |c_CTRL-R| including support for
--- special <C-a>, <C-f>, <C-l>, <C-w> keys for a register.
--- - <C-v>, <C-q> - next key literally. As |c_CTRL-V| and |i_CTRL-V_digit|
--- (all digits must be typed in full or stopped with <C-c>).
--- - Pasting from system |clipboard| is supported for "non-streaming" paste (as
--- described in |vim.paste()|).
---
--- - Autopair (if `opts.autopair` is set) is similar to |mini.pairs|:
--- - Opening characters `(`, `[`, `{` always insert a `()`, `[]`, `{}` pair and places
--- caret inside of it.
--- - Closing characters `)`, `]`, `}` move caret to the right if there is the same
--- character to the right.
--- - Closeopen characters single+double quotes and backtick perform "close"
--- action if possible and "open" action if not.
--- - In all cases press <C-v> before special character to insert it verbatim.
---
--- - Completion:
--- - <Tab>, <S-Tab> - start with `state.opts.completion` method if not active
--- and advance through active completion (i.e. replace currently displayed
--- at caret item with the next one).
--- Note: type `<C-v><Tab>` to insert literal `\t`.
--- - <C-n>, <C-p>, <Up>, <Down> - start with `"history"` method if not active
--- and advance through active completion.
--- - <C-e> - cancel and return to initial input and caret.
--- - <C-y> - accept current candidate. Note: it will also be accepted after
--- any key that doesn't advance completion.
---
--- - Miscellaneous:
--- - <C-o> - change scope of the input. Cycles through all available ones.
--- - <C-s> - change view style. Works only with |MiniInput.gen_view| view
--- handlers. Cycles through all available ones.
--- - <C-x> - toggle hide/unhide of the input.
---
--- - Special keys (combo, mouse, but not whitespace) not listed above - ignored.
--- Anything else (even more than a single character) - inserted at caret.
---
---@param state table Current |MiniInput-state|.
---@param key string|nil A key to process. Should be escaped (`"\r"`, not `"<CR>"`).
--- See |vim.keycode()|.
---@param opts table|nil Options. Possible fields:
--- - <autopair> `(boolean)` - whether perform add autopair. Default: `false`.
MiniInput.default_key = function(state, key, opts)
opts = opts or {}
if key == nil or H.state_is_end(state) then return end
0