dotfiles/.config/nvim/lua/mini/surround.lua

2234 lines
92 KiB
Lua

--- *mini.surround* Surround actions
--- *MiniSurround*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Fast and feature-rich surrounding. Can be configured to have experience
--- similar to 'tpope/vim-surround' (see |MiniSurround-vim-surround-config|).
---
--- Features:
--- - Actions (all of them are dot-repeatable out of the box and respect
--- |[count]|) with configurable keymappings:
--- - Add surrounding with `sa` (in visual mode or on motion).
--- - Delete surrounding with `sd`.
--- - Replace surrounding with `sr`.
--- - Find surrounding with `sf` or `sF` (move cursor right or left).
--- - Highlight surrounding with `sh`.
--- - Change number of neighbor lines with `sn` (see |MiniSurround-algorithm|).
---
--- - Surrounding is identified by a single character as both "input" (in
--- `delete` and `replace` start, `find`, and `highlight`) and "output" (in
--- `add` and `replace` end):
--- - 'f' - function call (string of alphanumeric symbols or '_' or '.'
--- followed by balanced '()'). In "input" finds function call, in
--- "output" prompts user to enter function name.
--- - 't' - tag. In "input" finds tag with same identifier, in "output"
--- prompts user to enter tag name.
--- - All symbols in brackets '()', '[]', '{}', '<>". In "input' represents
--- balanced brackets (open - with whitespace pad, close - without), in
--- "output" - left and right parts of brackets.
--- - '?' - interactive. Prompts user to enter left and right parts.
--- - All other alphanumeric, punctuation, or space characters represent
--- surrounding with identical left and right parts.
---
--- - Configurable search methods to find not only covering but possibly next,
--- previous, or nearest surrounding. See more in |MiniSurround.config|.
---
--- - All actions involving finding surrounding (delete, replace, find,
--- highlight) can be used with suffix that changes search method to find
--- previous/last. See more in |MiniSurround.config|.
---
--- Known issues which won't be resolved:
--- - Search for surrounding is done using Lua patterns (regex-like approach).
--- So certain amount of false positives should be expected.
---
--- - When searching for "input" surrounding, there is no distinction if it is
--- inside string or comment. So in this case there will be not proper match
--- for a function call: 'f(a = ")", b = 1)'.
---
--- - Tags are searched using regex-like methods, so issues are inevitable.
--- Overall it is pretty good, but certain cases won't work. Like self-nested
--- tags won't match correctly on both ends: '<a><a></a></a>'.
---
--- # Setup~
---
--- This module needs a setup with `require('mini.surround').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniSurround` which you can use for scripting or manually (with
--- `:lua MiniSurround.*`).
---
--- See |MiniSurround.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minisurround_config` which should have same structure as
--- `MiniSurround.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Example usage~
---
--- Regular mappings:
--- - `saiw)` - add (`sa`) for inner word (`iw`) parenthesis (`)`).
--- - `saiw?[[<CR>]]<CR>` - add (`sa`) for inner word (`iw`) interactive
--- surrounding (`?`): `[[` for left and `]]` for right.
--- - `2sdf` - delete (`sd`) second (`2`) surrounding function call (`f`).
--- - `sr)tdiv<CR>` - replace (`sr`) surrounding parenthesis (`)`) with tag
--- (`t`) with identifier 'div' (`div<CR>` in command line prompt).
--- - `sff` - find right (`sf`) part of surrounding function call (`f`).
--- - `sh}` - highlight (`sh`) for a brief period of time surrounding curly
--- brackets (`}`).
---
--- Extended mappings (temporary force "prev"/"next" search methods):
--- - `sdnf` - delete (`sd`) next (`n`) function call (`f`).
--- - `srlf(` - replace (`sd`) last (`l`) function call (`f`) with padded
--- bracket (`(`).
--- - `2sfnt` - find (`sf`) second (2) next (`n`) tag (`t`).
--- - `shl}` - highlight (`sh`) last (`l`) second (`2`) curly bracket (`}`).
---
--- # Comparisons~
---
--- - 'tpope/vim-surround':
--- - 'vim-surround' has completely different, with other focus set of
--- default mappings, while 'mini.surround' has a more coherent set.
--- - 'mini.surround' supports dot-repeat, customized search path (see
--- |MiniSurround.config|), customized specifications (see
--- |MiniSurround-surround-specification|) allowing usage of tree-sitter
--- queries (see |MiniSurround.gen_spec.input.treesitter()|),
--- highlighting and finding surrounding, "last"/"next" extended
--- mappings. While 'vim-surround' does not.
--- - 'machakann/vim-sandwich':
--- - Both have same keybindings for common actions (add, delete, replace).
--- - Otherwise same differences as with 'tpop/vim-surround' (except
--- dot-repeat because 'vim-sandwich' supports it).
--- - 'kylechui/nvim-surround':
--- - 'nvim-surround' is designed after 'tpope/vim-surround' with same
--- default mappings and logic, while 'mini.surround' has mappings
--- similar to 'machakann/vim-sandwich'.
--- - 'mini.surround' has more flexible customization of input surrounding
--- (with composed patterns, region pair(s), search methods).
--- - 'mini.surround' supports |[count]| in both input and output
--- surrounding (see |MiniSurround-count|) while 'nvim-surround' doesn't.
--- - 'mini.surround' supports "last"/"next" extended mappings.
--- - |mini.ai|:
--- - Both use similar logic for finding target: textobject in 'mini.ai'
--- and surrounding pair in 'mini.surround'. While 'mini.ai' uses
--- extraction pattern for separate `a` and `i` textobjects,
--- 'mini.surround' uses it to select left and right surroundings
--- (basically a difference between `a` and `i` textobjects).
--- - Some builtin specifications are slightly different:
--- - Quotes in 'mini.ai' are balanced, in 'mini.surround' they are not.
--- - The 'mini.surround' doesn't have argument surrounding.
--- - Default behavior in 'mini.ai' selects one of the edges into `a`
--- textobject, while 'mini.surround' - both.
---
--- # Highlight groups~
---
--- * `MiniSurround` - highlighting of requested surrounding.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling~
---
--- To disable, set `vim.g.minisurround_disable` (globally) or
--- `vim.b.minisurround_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.
--- Builtin surroundings~
---
--- This table describes all builtin surroundings along with what they
--- represent. Explanation:
--- - `Key` represents the surrounding identifier: single character which should
--- be typed after action mappings (see |MiniSurround.config.mappings|).
--- - `Name` is a description of surrounding.
--- - `Example line` contains a string for which examples are constructed. The
--- `*` denotes the cursor position over `a` character.
--- - `Delete` shows the result of typing `sd` followed by surrounding identifier.
--- It aims to demonstrate "input" surrounding which is also used in replace
--- with `sr` (surrounding id is typed first), highlight with `sh`, find with
--- `sf` and `sF`.
--- - `Replace` shows the result of typing `sr!` followed by surrounding
--- identifier (with possible follow up from user). It aims to demonstrate
--- "output" surrounding which is also used in adding with `sa` (followed by
--- textobject/motion or in Visual mode).
---
--- Example: typing `sd)` with cursor on `*` (covers `a` character) changes line
--- `!( *a (bb) )!` into `! aa (bb) !`. Typing `sr!)` changes same initial line
--- into `(( aa (bb) ))`.
--- >
--- |Key| Name | Example line | Delete | Replace |
--- |---|---------------|---------------|-------------|-----------------|
--- | ( | Balanced () | !( *a (bb) )! | !aa (bb)! | ( ( aa (bb) ) ) |
--- | [ | Balanced [] | ![ *a [bb] ]! | !aa [bb]! | [ [ aa [bb] ] ] |
--- | { | Balanced {} | !{ *a {bb} }! | !aa {bb}! | { { aa {bb} } } |
--- | < | Balanced <> | !< *a <bb> >! | !aa <bb>! | < < aa <bb> > > |
--- |---|---------------|---------------|-------------|-----------------|
--- | ) | Balanced () | !( *a (bb) )! | ! aa (bb) ! | (( aa (bb) )) |
--- | ] | Balanced [] | ![ *a [bb] ]! | ! aa [bb] ! | [[ aa [bb] ]] |
--- | } | Balanced {} | !{ *a {bb} }! | ! aa {bb} ! | {{ aa {bb} }} |
--- | > | Balanced <> | !< *a <bb> >! | ! aa <bb> ! | << aa <bb> >> |
--- | b | Alias for | !( *a {bb} )! | ! aa {bb} ! | (( aa {bb} )) |
--- | | ), ], or } | | | |
--- |---|---------------|---------------|-------------|-----------------|
--- | q | Alias for | !'aa'*a'aa'! | !'aaaaaa'! | "'aa'aa'aa'" |
--- | | ", ', or ` | | | |
--- |---|---------------|---------------|-------------|-----------------|
--- | ? | User prompt | !e * o! | ! a ! | ee a oo |
--- | |(typed e and o)| | | |
--- |---|---------------|---------------|-------------|-----------------|
--- | t | Tag | !<x>*</x>! | !a! | <y><x>a</x></y> |
--- | | | | | (typed y) |
--- |---|---------------|---------------|-------------|-----------------|
--- | f | Function call | !f(*a, bb)! | !aa, bb! | g(f(*a, bb)) |
--- | | | | | (typed g) |
--- |---|---------------|---------------|-------------|-----------------|
--- | | Default | !_a*a_! | !aaa! | __aaa__ |
--- | | (typed _) | | | |
--- |---|---------------|---------------|-------------|-----------------|
--- <
--- Notes:
--- - All examples assume default `config.search_method`.
--- - Open brackets differ from close brackets by how they treat inner edge
--- whitespace: open includes it left and right parts, close does not.
--- - Output value of `b` alias is same as `)`. For `q` alias - same as `"`.
--- - Default surrounding is activated for all characters which are not
--- configured surrounding identifiers. Note: due to special handling of
--- underlying `x.-x` Lua pattern (see |MiniSurround-search-algorithm|), it
--- doesn't really support non-trivial `[count]` for "cover" search method.
---@tag MiniSurround-surround-builtin
--- Note: this is similar to |MiniAi-glossary|.
---
--- - REGION - table representing region in a buffer. Fields: <from> and
--- <to> for inclusive start and end positions (<to> might be `nil` to
--- describe empty region). Each position is also a table with line <line>
--- and column <col> (both start at 1). Examples:
--- - `{ from = { line = 1, col = 1 }, to = { line = 2, col = 1 } }`
--- - `{ from = { line = 10, col = 10 } }` - empty region.
--- - REGION PAIR - table representing regions for left and right surroundings.
--- Fields: <left> and <right> with regions. Examples:
--- `{`
--- `left = { from = { line = 1, col = 1 }, to = { line = 1, col = 1 } },`
--- `right = { from = { line = 1, col = 3 } },`
--- `}`
--- - PATTERN - string describing Lua pattern.
--- - SPAN - interval inside a string (end-exclusive). Like [1, 5). Equal
--- `from` and `to` edges describe empty span at that point.
--- - SPAN `A = [a1, a2)` COVERS `B = [b1, b2)` if every element of
--- `B` is within `A` (`a1 <= b < a2`).
--- It also is described as B IS NESTED INSIDE A.
--- - NESTED PATTERN - array of patterns aimed to describe nested spans.
--- - SPAN MATCHES NESTED PATTERN if there is a sequence of consecutively
--- nested spans each matching corresponding pattern within substring of
--- previous span (or input string for first span). Example:
--- Nested patterns: `{ '%b()', '^. .* .$' }` (balanced `()` with inner space)
--- Input string: `( ( () ( ) ) )`
--- `123456789012345`
--- Here are all matching spans [1, 15) and [3, 13). Both [5, 7) and [8, 10)
--- match first pattern but not second. All other combinations of `(` and `)`
--- don't match first pattern (not balanced).
--- - COMPOSED PATTERN: array with each element describing possible pattern
--- (or array of them) at that place. Composed pattern basically defines all
--- possible combinations of nested pattern (their cartesian product).
--- Examples:
--- 1. Composed pattern: `{ { '%b()', '%b[]' }, '^. .* .$' }`
--- Composed pattern expanded into equivalent array of nested patterns:
--- `{ '%b()', '^. .* .$' }` and `{ '%b[]', '^. .* .$' }`
--- Description: either balanced `()` or balanced `[]` but both with
--- inner edge space.
--- 2. Composed pattern:
--- `{ { { '%b()', '^. .* .$' }, { '%b[]', '^.[^ ].*[^ ].$' } }, '.....' }`
--- Composed pattern expanded into equivalent array of nested patterns:
--- `{ '%b()', '^. .* .$', '.....' }` and
--- `{ '%b[]', '^.[^ ].*[^ ].$', '.....' }`
--- Description: either "balanced `()` with inner edge space" or
--- "balanced `[]` with no inner edge space", both with 5 or more characters.
--- - SPAN MATCHES COMPOSED PATTERN if it matches at least one nested pattern
--- from expanded composed pattern.
---@tag MiniSurround-glossary
--- Surround specification is a table with keys:
--- - <input> - defines how to find and extract surrounding for "input"
--- operations (like `delete`). See more in 'Input surrounding' setction.
--- - <output> - defines what to add on left and right for "output" operations
--- (like `add`). See more in 'Output surrounding' section.
---
--- Example of surround info for builtin `)` identifier: >
--- {
--- input = { '%b()', '^.().*().$' },
--- output = { left = '(', right = ')' }
--- }
--- <
--- # Input surrounding ~
---
--- Specification for input surrounding has a structure of composed pattern
--- (see |MiniSurround-glossary|) with two differences:
--- - Last pattern(s) should have two or four empty capture groups denoting
--- how the last string should be processed to extract surrounding parts:
--- - Two captures represent left part from start of string to first
--- capture and right part - from second capture to end of string.
--- Example: `a()b()c` defines left surrounding as 'a', right - 'c'.
--- - Four captures define left part inside captures 1 and 2, right part -
--- inside captures 3 and 4. Example: `a()()b()c()` defines left part as
--- empty, right part as 'c'.
--- - Allows callable objects (see |vim.is_callable()|) in certain places
--- (enables more complex surroundings in exchange of increase in configuration
--- complexity and computations):
--- - If specification itself is a callable, it will be called without
--- arguments and should return one of:
--- - Composed pattern. Useful for implementing user input. Example of
--- simplified variant of input surrounding for function call with
--- name taken from user prompt:
--- >
--- function()
--- local left_edge = vim.pesc(vim.fn.input('Function name: '))
--- return { string.format('%s+%%b()', left_edge), '^.-%(().*()%)$' }
--- end
--- <
--- - Single region pair (see |MiniSurround-glossary|). Useful to allow
--- full control over surrounding. Will be taken as is. Example of
--- returning first and last lines of a buffer:
--- >
--- function()
--- local n_lines = vim.fn.line('$')
--- return {
--- left = {
--- from = { line = 1, col = 1 },
--- to = { line = 1, col = vim.fn.getline(1):len() },
--- },
--- right = {
--- from = { line = n_lines, col = 1 },
--- to = { line = n_lines, col = vim.fn.getline(n_lines):len() },
--- },
--- }
--- end
--- <
--- - Array of region pairs. Useful for incorporating other instruments,
--- like treesitter (see |MiniSurround.gen_spec.treesitter()|). The
--- best region pair will be picked in the same manner as with composed
--- pattern (respecting options `n_lines`, `search_method`, etc.) using
--- output region (from start of left region to end of right region).
--- Example using edges of "best" line with display width more than 80:
--- >
--- function()
--- local make_line_region_pair = function(n)
--- local left = { line = n, col = 1 }
--- local right = { line = n, col = vim.fn.getline(n):len() }
--- return {
--- left = { from = left, to = left },
--- right = { from = right, to = right },
--- }
--- end
---
--- local res = {}
--- for i = 1, vim.fn.line('$') do
--- if vim.fn.getline(i):len() > 80 then
--- table.insert(res, make_line_region_pair(i))
--- end
--- end
--- return res
--- end
--- <
--- - If there is a callable instead of assumed string pattern, it is expected
--- to have signature `(line, init)` and behave like `pattern:find()`.
--- It should return two numbers representing span in `line` next after
--- or at `init` (`nil` if there is no such span).
--- !IMPORTANT NOTE!: it means that output's `from` shouldn't be strictly
--- to the left of `init` (it will lead to infinite loop). Not allowed as
--- last item (as it should be pattern with captures).
--- Example of matching only balanced parenthesis with big enough width:
--- >
--- {
--- '%b()',
--- function(s, init)
--- if init > 1 or s:len() < 5 then return end
--- return 1, s:len()
--- end,
--- '^.().*().$'
--- }
--- >
--- More examples:
--- - See |MiniSurround.gen_spec| for function wrappers to create commonly used
--- surrounding specifications.
---
--- - Pair of balanced brackets from set (used for builtin `b` identifier):
--- `{ { '%b()', '%b[]', '%b{}' }, '^.().*().$' }`
---
--- - Lua block string: `{ '%[%[().-()%]%]' }`
---
--- # Output surrounding ~
---
--- A table with <left> (plain text string) and <right> (plain text string)
--- fields. Strings can contain new lines character `\n` to add multiline parts.
---
--- Examples:
--- - Lua block string: `{ left = '[[', right = ']]' }`
--- - Brackets on separate lines (indentation is not preserved):
--- `{ left = '(\n', right = '\n)' }`
---@tag MiniSurround-surround-specification
--- Count with actions
---
--- |[count]| is supported by all actions in the following ways:
---
--- - In add, two types of `[count]` is supported in Normal mode:
--- `[count1]sa[count2][textobject]`. The `[count1]` defines how many times
--- left and right parts of output surrounding will be repeated and `[count2]` is
--- used for textobject.
--- In Visual mode `[count]` is treated as `[count1]`.
--- Example: `2sa3aw)` and `v3aw2sa)` will result into textobject `3aw` being
--- surrounded by `((` and `))`.
---
--- - In delete/replace/find/highlight `[count]` means "find n-th surrounding
--- and execute operator on it".
--- Example: `2sd)` on line `(a(b(c)b)a)` with cursor on `c` will result into
--- `(ab(c)ba)` (and not in `(abcba)` if it would have meant "delete n times").
---@tag MiniSurround-count
--- Search algorithm design
---
--- Search for the input surrounding relies on these principles:
--- - Input surrounding specification is constructed based on surrounding
--- identifier (see |MiniSurround-surround-specification|).
--- - General search is done by converting some 2d buffer region (neighborhood
--- of reference region) into 1d string (each line is appended with `\n`).
--- Then search for a best span matching specification is done inside string
--- (see |MiniSurround-glossary|). After that, span is converted back into 2d
--- region. Note: first search is done inside reference region lines, and
--- only after that - inside its neighborhood within `config.n_lines` (see
--- |MiniSurround.config|).
--- - The best matching span is chosen by iterating over all spans matching
--- surrounding specification and comparing them with "current best".
--- Comparison also depends on reference region (tighter covering is better,
--- otherwise closer is better) and search method (if span is even considered).
--- - Extract pair of spans (for left and right regions in region pair) based
--- on extraction pattern (last item in nested pattern).
--- - For |[count]| greater than 1, steps are repeated with current best match
--- becoming reference region. One such additional step is also done if final
--- region is equal to reference region.
---
--- Notes:
--- - Iteration over all matched spans is done in depth-first fashion with
--- respect to nested pattern.
--- - It is guaranteed that span is compared only once.
--- - For the sake of increasing functionality, during iteration over all
--- matching spans, some Lua patterns in composed pattern are handled
--- specially.
--- - `%bxx` (`xx` is two identical characters). It denotes balanced pair
--- of identical characters and results into "paired" matches. For
--- example, `%b""` for `"aa" "bb"` would match `"aa"` and `"bb"`, but
--- not middle `" "`.
--- - `x.-y` (`x` and `y` are different strings). It results only in matches with
--- smallest width. For example, `e.-o` for `e e o o` will result only in
--- middle `e o`. Note: it has some implications for when parts have
--- quantifiers (like `+`, etc.), which usually can be resolved with
--- frontier pattern `%f[]`.
---@tag MiniSurround-search-algorithm
-- Module definition ==========================================================
local MiniSurround = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniSurround.config|.
---
---@usage `require('mini.surround').setup({})` (replace `{}` with your `config` table)
MiniSurround.setup = function(config)
-- Export module
_G.MiniSurround = MiniSurround
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text *MiniSurround-vim-surround-config*
--- # Setup similar to 'tpope/vim-surround'~
---
--- This module is primarily designed after 'machakann/vim-sandwich'. To get
--- behavior closest to 'tpope/vim-surround' (but not identical), use this setup:
--- >
--- require('mini.surround').setup({
--- mappings = {
--- add = 'ys',
--- delete = 'ds',
--- find = '',
--- find_left = '',
--- highlight = '',
--- replace = 'cs',
--- update_n_lines = '',
---
--- -- Add this only if you don't want to use extended mappings
--- suffix_last = '',
--- suffix_next = '',
--- },
--- search_method = 'cover_or_next',
--- })
---
--- -- Remap adding surrounding to Visual mode selection
--- vim.keymap.del('x', 'ys')
--- vim.keymap.set('x', 'S', [[:<C-u>lua MiniSurround.add('visual')<CR>]], { silent = true })
---
--- -- Make special mapping for "add surrounding for line"
--- vim.keymap.set('n', 'yss', 'ys_', { remap = true })
--- <
--- # Options~
---
--- ## Custom surroundings~
---
--- User can define own surroundings by supplying `config.custom_surroundings`.
--- It should be a **table** with keys being single character surrounding
--- identifier and values - surround specification (see
--- |MiniSurround-surround-specification|).
---
--- General recommendations:
--- - In `config.custom_surroundings` only some data can be defined (like only
--- `output`). Other fields will be taken from builtin surroundings.
--- - Function returning surround info at <input> or <output> fields of
--- specification is helpful when user input is needed (like asking for
--- function name). Use |input()| or |MiniSurround.user_input()|. Return
--- `nil` to stop any current surround operation.
---
--- Examples of using `config.custom_surroundings` (see more examples at
--- |MiniSurround.gen_spec|):
--- >
--- local surround = require('mini.surround')
--- surround.setup({
--- custom_surroundings = {
--- -- Make `)` insert parts with spaces. `input` pattern stays the same.
--- [')'] = { output = { left = '( ', right = ' )' } },
---
--- -- Use function to compute surrounding info
--- ['*'] = {
--- input = function()
--- local n_star = MiniSurround.user_input('Number of * to find: ')
--- local many_star = string.rep('%*', tonumber(n_star) or 1)
--- return { many_star .. '().-()' .. many_star }
--- end,
--- output = function()
--- local n_star = MiniSurround.user_input('Number of * to output: ')
--- local many_star = string.rep('*', tonumber(n_star) or 1)
--- return { left = many_star, right = many_star }
--- end,
--- },
--- },
--- })
---
--- -- Create custom surrounding for Lua's block string `[[...]]`. Use this inside
--- -- autocommand or 'after/ftplugin/lua.lua' file.
--- vim.b.minisurround_config = {
--- custom_surroundings = {
--- s = {
--- input = { '%[%[().-()%]%]' },
--- output = { left = '[[', right = ']]' },
--- },
--- },
--- }
--- <
--- ## Respect selection type ~
---
--- Boolean option `config.respect_selection_type` controls whether to respect
--- selection type when adding and deleting surrounding. When enabled:
--- - Linewise adding places surroundings on separate lines while indenting
--- surrounded lines ones.
--- - Deleting surroundings which look like they were the result of linewise
--- adding will act to revert it: delete lines with surroundings and dedent
--- surrounded lines ones.
--- - Blockwise adding places surroundings on whole edges, not only start and
--- end of selection. Note: it doesn't really work outside of text and in
--- presence of multibyte characters; and probably won't due to
--- implementation difficulties.
---
--- ## Search method ~
---
--- Value of `config.search_method` defines how best match search is done.
--- Based on its value, one of the following matches will be selected:
--- - Covering match. Left/right edge is before/after left/right edge of
--- reference region.
--- - Previous match. Left/right edge is before left/right edge of reference
--- region.
--- - Next match. Left/right edge is after left/right edge of reference region.
--- - Nearest match. Whichever is closest among previous and next matches.
---
--- Possible values are:
--- - `'cover'` - use only covering match. Don't use either previous or
--- next; report that there is no surrounding found.
--- - `'cover_or_next'` (default) - use covering match. If not found, use next.
--- - `'cover_or_prev'` - use covering match. If not found, use previous.
--- - `'cover_or_nearest'` - use covering match. If not found, use nearest.
--- - `'next'` - use next match.
--- - `'previous'` - use previous match.
--- - `'nearest'` - use nearest match.
---
--- Note: search is first performed on the reference region lines and only
--- after failure - on the whole neighborhood defined by `config.n_lines`. This
--- means that with `config.search_method` not equal to `'cover'`, "previous"
--- or "next" surrounding will end up as search result if they are found on
--- first stage although covering match might be found in bigger, whole
--- neighborhood. This design is based on observation that most of the time
--- operation is done within reference region lines (usually cursor line).
---
--- Here is an example of how replacing `)` with `]` surrounding is done based
--- on a value of `'config.search_method'` when cursor is inside `bbb` word:
--- - `'cover'`: `(a) bbb (c)` -> `(a) bbb (c)` (with message)
--- - `'cover_or_next'`: `(a) bbb (c)` -> `(a) bbb [c]`
--- - `'cover_or_prev'`: `(a) bbb (c)` -> `[a] bbb (c)`
--- - `'cover_or_nearest'`: depends on cursor position.
--- For first and second `b` - as in `cover_or_prev` (as previous match is
--- nearer), for third - as in `cover_or_next` (as next match is nearer).
--- - `'next'`: `(a) bbb (c)` -> `(a) bbb [c]`. Same outcome for `(bbb)`.
--- - `'prev'`: `(a) bbb (c)` -> `[a] bbb (c)`. Same outcome for `(bbb)`.
--- - `'nearest'`: depends on cursor position (same as in `'cover_or_nearest'`).
---
--- ## Search suffixes~
---
--- To provide more searching possibilities, 'mini.surround' creates extended
--- mappings force "prev" and "next" methods for particular search. It does so
--- by appending mapping with certain suffix: `config.mappings.suffix_last` for
--- mappings which will use "prev" search method, `config.mappings.suffix_next`
--- - "next" search method.
---
--- Notes:
--- - It creates new mappings only for actions involving surrounding search:
--- delete, replace, find (right and left), highlight.
--- - All new mappings behave the same way as if `config.search_method` is set
--- to certain search method. They are dot-repeatable, respect |[count]|, etc.
--- - Supply empty string to disable creation of corresponding set of mappings.
---
--- Example with default values (`n` for `suffix_next`, `l` for `suffix_last`)
--- and initial line `(aa) (bb) (cc)`.
--- - Typing `sdn)` with cursor inside `(aa)` results into `(aa) bb (cc)`.
--- - Typing `sdl)` with cursor inside `(cc)` results into `(aa) bb (cc)`.
--- - Typing `2srn)]` with cursor inside `(aa)` results into `(aa) (bb) [cc]`.
MiniSurround.config = {
-- Add custom surroundings to be used on top of builtin ones. For more
-- information with examples, see `:h MiniSurround.config`.
custom_surroundings = nil,
-- Duration (in ms) of highlight when calling `MiniSurround.highlight()`
highlight_duration = 500,
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
add = 'sa', -- Add surrounding in Normal and Visual modes
delete = 'sd', -- Delete surrounding
find = 'sf', -- Find surrounding (to the right)
find_left = 'sF', -- Find surrounding (to the left)
highlight = 'sh', -- Highlight surrounding
replace = 'sr', -- Replace surrounding
update_n_lines = 'sn', -- Update `n_lines`
suffix_last = 'l', -- Suffix to search with "prev" method
suffix_next = 'n', -- Suffix to search with "next" method
},
-- Number of lines within which surrounding is searched
n_lines = 20,
-- Whether to respect selection type:
-- - Place surroundings on separate lines in linewise mode.
-- - Place surroundings on each line in blockwise mode.
respect_selection_type = false,
-- How to search for surrounding (first inside current line, then inside
-- neighborhood). One of 'cover', 'cover_or_next', 'cover_or_prev',
-- 'cover_or_nearest', 'next', 'prev', 'nearest'. For more details,
-- see `:h MiniSurround.config`.
search_method = 'cover',
-- Whether to disable showing non-error feedback
silent = false,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Add surrounding
---
--- No need to use it directly, everything is setup in |MiniSurround.setup|.
---
---@param mode string Mapping mode (normal by default).
MiniSurround.add = function(mode)
-- Needed to disable in visual mode
if H.is_disabled() then return '<Esc>' end
-- Get marks' positions based on current mode
local marks = H.get_marks_pos(mode)
-- Get surround info. Try take from cache only in not visual mode (as there
-- is no intended dot-repeatability).
local surr_info
if mode == 'visual' then
surr_info = H.get_surround_spec('output', false)
else
surr_info = H.get_surround_spec('output', true)
end
if surr_info == nil then return '<Esc>' end
-- Extend parts based on provided `[count]` before operator (if this is not
-- from dot-repeat and was done already)
if not surr_info.did_count then
local count = H.cache.count or vim.v.count1
surr_info.left, surr_info.right = surr_info.left:rep(count), surr_info.right:rep(count)
surr_info.did_count = true
end
-- Add surrounding.
-- Possibly deal with linewise and blockwise addition separately
local respect_selection_type = H.get_config().respect_selection_type
if not respect_selection_type or marks.selection_type == 'charwise' then
-- Begin insert from right to not break column numbers
-- Insert after the right mark (`+ 1` is for that)
H.region_replace({ from = { line = marks.second.line, col = marks.second.col + 1 } }, surr_info.right)
H.region_replace({ from = marks.first }, surr_info.left)
-- Set cursor to be on the right of left surrounding
H.set_cursor(marks.first.line, marks.first.col + surr_info.left:len())
return
end
if marks.selection_type == 'linewise' then
local from_line, to_line = marks.first.line, marks.second.line
-- Save current range indent and indent surrounded lines
local init_indent = H.get_range_indent(from_line, to_line)
H.shift_indent('>', from_line, to_line)
-- Put cursor on the start of first surrounded line
H.set_cursor_nonblank(from_line)
-- Put surroundings on separate lines
vim.fn.append(to_line, init_indent .. surr_info.right)
vim.fn.append(from_line - 1, init_indent .. surr_info.left)
return
end
if marks.selection_type == 'blockwise' then
-- NOTE: this doesn't work with mix of multibyte and normal characters, as
-- well as outside of text lines.
local from_col, to_col = marks.first.col, marks.second.col
-- - Ensure that `to_col` is to the right of `from_col`. Can be not the
-- case if visual block was selected from "south-west" to "north-east".
from_col, to_col = math.min(from_col, to_col), math.max(from_col, to_col)
for i = marks.first.line, marks.second.line do
H.region_replace({ from = { line = i, col = to_col + 1 } }, surr_info.right)
H.region_replace({ from = { line = i, col = from_col } }, surr_info.left)
end
H.set_cursor(marks.first.line, from_col + surr_info.left:len())
return
end
end
--- Delete surrounding
---
--- No need to use it directly, everything is setup in |MiniSurround.setup|.
MiniSurround.delete = function()
-- Find input surrounding region
local surr = H.find_surrounding(H.get_surround_spec('input', true))
if surr == nil then return '<Esc>' end
-- Delete surrounding region. Begin with right to not break column numbers.
H.region_replace(surr.right, {})
H.region_replace(surr.left, {})
-- Set cursor to be on the right of deleted left surrounding
local from = surr.left.from
H.set_cursor(from.line, from.col)
-- Possibly tweak deletion of linewise surrounding. Should act as reverse to
-- linewise addition.
if not H.get_config().respect_selection_type then return end
local from_line, to_line = surr.left.from.line, surr.right.from.line
local is_linewise_delete = from_line < to_line and H.is_line_blank(from_line) and H.is_line_blank(to_line)
if is_linewise_delete then
-- Dedent surrounded lines
H.shift_indent('<', from_line, to_line)
-- Place cursor on first surrounded line
H.set_cursor_nonblank(from_line + 1)
-- Delete blank lines left after deleting surroundings
local buf_id = vim.api.nvim_get_current_buf()
vim.fn.deletebufline(buf_id, to_line)
vim.fn.deletebufline(buf_id, from_line)
end
end
--- Replace surrounding
---
--- No need to use it directly, everything is setup in |MiniSurround.setup|.
MiniSurround.replace = function()
-- Find input surrounding region
local surr = H.find_surrounding(H.get_surround_spec('input', true))
if surr == nil then return '<Esc>' end
-- Get output surround info
local new_surr_info = H.get_surround_spec('output', true)
if new_surr_info == nil then return '<Esc>' end
-- Replace by parts starting from right to not break column numbers
H.region_replace(surr.right, new_surr_info.right)
H.region_replace(surr.left, new_surr_info.left)
-- Set cursor to be on the right of left surrounding
local from = surr.left.from
H.set_cursor(from.line, from.col + new_surr_info.left:len())
end
--- Find surrounding
---
--- No need to use it directly, everything is setup in |MiniSurround.setup|.
MiniSurround.find = function()
-- Find surrounding region
local surr = H.find_surrounding(H.get_surround_spec('input', true))
if surr == nil then return '<Esc>' end
-- Make array of unique positions to cycle through
local pos_array = H.surr_to_pos_array(surr)
-- Cycle cursor through positions
local dir = H.cache.direction or 'right'
H.cursor_cycle(pos_array, dir)
-- Open 'enough folds' to show cursor
vim.cmd('normal! zv')
end
--- Highlight surrounding
---
--- No need to use it directly, everything is setup in |MiniSurround.setup|.
MiniSurround.highlight = function()
-- Find surrounding region
local surr = H.find_surrounding(H.get_surround_spec('input', true))
if surr == nil then return '<Esc>' end
-- Highlight surrounding region
local config = H.get_config()
local buf_id = vim.api.nvim_get_current_buf()
H.region_highlight(buf_id, surr.left)
H.region_highlight(buf_id, surr.right)
vim.defer_fn(function()
H.region_unhighlight(buf_id, surr.left)
H.region_unhighlight(buf_id, surr.right)
end, config.highlight_duration)
end
--- Update `MiniSurround.config.n_lines`
---
--- Convenient wrapper for updating `MiniSurround.config.n_lines` in case the
--- default one is not appropriate.
MiniSurround.update_n_lines = function()
if H.is_disabled() then return '<Esc>' end
local n_lines = MiniSurround.user_input('New number of neighbor lines', MiniSurround.config.n_lines)
n_lines = math.floor(tonumber(n_lines) or MiniSurround.config.n_lines)
MiniSurround.config.n_lines = n_lines
end
--- Ask user for input
---
--- This is mainly a wrapper for |input()| which allows empty string as input,
--- cancelling with `<Esc>` and `<C-c>`, and slightly modifies prompt. Use it
--- to ask for input inside function custom surrounding (see |MiniSurround.config|).
MiniSurround.user_input = function(prompt, text)
-- Major issue with both `vim.fn.input()` is that the only way to distinguish
-- cancelling with `<Esc>` and entering empty string with immediate `<CR>` is
-- through `cancelreturn` option (see `:h input()`). In that case the return
-- of `cancelreturn` will mean actual cancel, which removes possibility of
-- using that string. Although doable with very obscure string, this is not
-- very clean.
-- Overcome this by adding temporary keystroke listener.
local on_key = vim.on_key or vim.register_keystroke_callback
local was_cancelled = false
on_key(function(key)
if key == vim.api.nvim_replace_termcodes('<Esc>', true, true, true) then was_cancelled = true end
end, H.ns_id.input)
-- Ask for input
-- NOTE: it would be GREAT to make this work with `vim.ui.input()` but I
-- didn't find a way to make it work without major refactor of whole module.
-- The main issue is that `vim.ui.input()` is designed to perform action in
-- callback and current module design is to get output immediately. Although
-- naive approach of
-- `local res; vim.ui.input({...}, function(input) res = input end)`
-- works in default `vim.ui.input`, its reimplementations can return from it
-- immediately and proceed in main event loop. Couldn't find a relatively
-- simple way to stop execution of this current function until `ui.input()`'s
-- callback finished execution.
local opts = { prompt = '(mini.surround) ' .. prompt .. ': ', default = text or '' }
vim.cmd('echohl Question')
-- Use `pcall` to allow `<C-c>` to cancel user input
local ok, res = pcall(vim.fn.input, opts)
vim.cmd([[echohl None | echo '' | redraw]])
-- Stop key listening
on_key(nil, H.ns_id.input)
if not ok or was_cancelled then return end
return res
end
--- Generate common surrounding specifications
---
--- This is a table with two sets of generator functions: <input> and <output>
--- (currently empty). Each is a table with values being function generating
--- corresponding surrounding specification.
---
--- Example: >
--- local ts_input = require('mini.surround').gen_spec.input.treesitter
--- require('mini.surround').setup({
--- custom_surroundings = {
--- -- Use tree-sitter to search for function call
--- f = {
--- input = ts_input({ outer = '@call.outer', inner = '@call.inner' })
--- },
--- }
--- })
---
---@seealso |MiniAi.gen_spec|
MiniSurround.gen_spec = { input = {}, output = {} }
--- Treesitter specification for input surrounding
---
--- This is a specification in function form. When called with a pair of
--- treesitter captures, it returns a specification function outputting an
--- array of region pairs derived from <outer> and <inner> captures. It first
--- searches for all matched nodes of outer capture and then completes each one
--- with the biggest match of inner capture inside that node (if any). The result
--- region pair is a difference between regions of outer and inner captures.
---
--- In order for this to work, apart from working treesitter parser for desired
--- language, user should have a reachable language-specific 'textobjects'
--- query (see |get_query()|). The most straightforward way for this is to have
--- 'textobjects.scm' query file with treesitter captures stored in some
--- recognized path. This is primarily designed to be compatible with
--- 'nvim-treesitter/nvim-treesitter-textobjects' plugin, but can be used
--- without it.
---
--- Two most common approaches for having a query file:
--- - Install 'nvim-treesitter/nvim-treesitter-textobjects'. It has curated and
--- well maintained builtin query files for many languages with a standardized
--- capture names, like `call.outer`, `call.inner`, etc.
--- - Manually create file 'after/queries/<language name>/textobjects.scm' in
--- your |$XDG_CONFIG_HOME| directory. It should contain queries with
--- captures (later used to define surrounding parts). See |lua-treesitter-query|.
--- To verify that query file is reachable, run (example for "lua" language)
--- `:lua print(vim.inspect(vim.treesitter.query.get_files('lua', 'textobjects')))`
--- (output should have at least an intended file).
---
--- Example configuration for function definition textobject with
--- 'nvim-treesitter/nvim-treesitter-textobjects' captures:
--- >
--- local ts_input = require('mini.surround').gen_spec.input.treesitter
--- require('mini.surround').setup({
--- custom_textobjects = {
--- f = ts_input({ outer = '@call.outer', inner = '@call.inner' }),
--- }
--- })
--- >
---
--- Notes:
--- - By default query is done using 'nvim-treesitter' plugin if it is present
--- (falls back to builtin methods otherwise). This allows for a more
--- advanced features (like multiple buffer languages, custom directives, etc.).
--- See `opts.use_nvim_treesitter` for how to disable this.
--- - It uses buffer's |filetype| to determine query language.
--- - On large files it is slower than pattern-based textobjects. Still very
--- fast though (one search should be magnitude of milliseconds or tens of
--- milliseconds on really large file).
---
---@param captures table Captures for outer and inner parts of region pair:
--- table with <outer> and <inner> fields with captures for outer
--- (`[left.form; right.to]`) and inner (`(left.to; right.from)` both edges
--- exclusive, i.e. they won't be a part of surrounding) regions. Each value
--- should be a string capture starting with `'@'`.
---@param opts table|nil Options. Possible values:
--- - <use_nvim_treesitter> - whether to try to use 'nvim-treesitter' plugin
--- (if present) to do the query. It implements more advanced behavior at
--- cost of increased execution time. Provides more coherent experience if
--- 'nvim-treesitter-textobjects' queries are used. Default: `true`.
---
---@return function Function which returns array of current buffer region pairs
--- representing differences between outer and inner captures.
---
---@seealso |MiniSurround-surround-specification| for how this type of
--- surrounding specification is processed.
--- |get_query()| for how query is fetched in case of no 'nvim-treesitter'.
--- |Query:iter_captures()| for how all query captures are iterated in case of
--- no 'nvim-treesitter'.
--- |MiniAi.gen_spec.treesitter()| for similar 'mini.ai' generator.
MiniSurround.gen_spec.input.treesitter = function(captures, opts)
opts = vim.tbl_deep_extend('force', { use_nvim_treesitter = true }, opts or {})
captures = H.prepare_captures(captures)
return function()
-- Get array of matched treesitter nodes
local has_nvim_treesitter, _ = pcall(require, 'nvim-treesitter')
local node_pair_querier = (has_nvim_treesitter and opts.use_nvim_treesitter) and H.get_matched_node_pairs_plugin
or H.get_matched_node_pairs_builtin
local matched_node_pairs = node_pair_querier(captures)
-- Return array of region pairs
return vim.tbl_map(function(node_pair)
-- `node:range()` returns 0-based numbers for end-exclusive region
local left_from_line, left_from_col, right_to_line, right_to_col = node_pair.outer:range()
local left_from = { line = left_from_line + 1, col = left_from_col + 1 }
local right_to = { line = right_to_line + 1, col = right_to_col }
local left_to, right_from
if node_pair.inner == nil then
left_to = right_to
right_from = H.pos_to_right(right_to)
right_to = nil
else
local left_to_line, left_to_col, right_from_line, right_from_col = node_pair.inner:range()
left_to = { line = left_to_line + 1, col = left_to_col + 1 }
right_from = { line = right_from_line + 1, col = right_from_col }
-- Take into account that inner capture should be both edges exclusive
left_to, right_from = H.pos_to_left(left_to), H.pos_to_right(right_from)
end
return { left = { from = left_from, to = left_to }, right = { from = right_from, to = right_to } }
end, matched_node_pairs)
end
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniSurround.config)
-- Namespaces to be used within module
H.ns_id = {
highlight = vim.api.nvim_create_namespace('MiniSurroundHighlight'),
input = vim.api.nvim_create_namespace('MiniSurroundInput'),
}
--stylua: ignore
-- Builtin surroundings
H.builtin_surroundings = {
-- Use balanced pair for brackets. Use opening ones to possibly
-- replace/delete innder edge whitespace.
['('] = { input = { '%b()', '^.%s*().-()%s*.$' }, output = { left = '( ', right = ' )' } },
[')'] = { input = { '%b()', '^.().*().$' }, output = { left = '(', right = ')' } },
['['] = { input = { '%b[]', '^.%s*().-()%s*.$' }, output = { left = '[ ', right = ' ]' } },
[']'] = { input = { '%b[]', '^.().*().$' }, output = { left = '[', right = ']' } },
['{'] = { input = { '%b{}', '^.%s*().-()%s*.$' }, output = { left = '{ ', right = ' }' } },
['}'] = { input = { '%b{}', '^.().*().$' }, output = { left = '{', right = '}' } },
['<'] = { input = { '%b<>', '^.%s*().-()%s*.$' }, output = { left = '< ', right = ' >' } },
['>'] = { input = { '%b<>', '^.().*().$' }, output = { left = '<', right = '>' } },
-- Derived from user prompt
['?'] = {
input = function()
local left = MiniSurround.user_input('Left surrounding')
if left == nil or left == '' then return end
local right = MiniSurround.user_input('Right surrounding')
if right == nil or right == '' then return end
return { vim.pesc(left) .. '().-()' .. vim.pesc(right) }
end,
output = function()
local left = MiniSurround.user_input('Left surrounding')
if left == nil then return end
local right = MiniSurround.user_input('Right surrounding')
if right == nil then return end
return { left = left, right = right }
end,
},
-- Brackets
['b'] = { input = { { '%b()', '%b[]', '%b{}' }, '^.().*().$' }, output = { left = '(', right = ')' } },
-- Function call
['f'] = {
input = { '%f[%w_%.][%w_%.]+%b()', '^.-%(().*()%)$' },
output = function()
local fun_name = MiniSurround.user_input('Function name')
if fun_name == nil then return nil end
return { left = ('%s('):format(fun_name), right = ')' }
end,
},
-- Tag
['t'] = {
input = { '<(%w-)%f[^<%w][^<>]->.-</%1>', '^<.->().*()</[^/]->$' },
output = function()
local tag_full = MiniSurround.user_input('Tag name')
if tag_full == nil then return nil end
local tag_name = tag_full:match('^%S*')
return { left = '<' .. tag_full .. '>', right = '</' .. tag_name .. '>' }
end,
},
-- Quotes
['q'] = { input = { { "'.-'", '".-"', '`.-`' }, '^.().*().$' }, output = { left = '"', right = '"' } },
}
-- Cache for dot-repeatability. This table is currently used with these keys:
-- - 'input' - surround info for searching (in 'delete' and 'replace' start).
-- - 'output' - surround info for adding (in 'add' and 'replace' end).
-- - 'direction' - direction in which `MiniSurround.find()` should go. Used to
-- enable same `operatorfunc` pattern for dot-repeatability.
-- - 'search_method' - search method.
-- - 'msg_shown' - whether helper message was shown.
H.cache = {}
-- 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', vim.deepcopy(H.default_config), config or {})
-- Validate per nesting level to produce correct error message
vim.validate({
custom_surroundings = { config.custom_surroundings, 'table', true },
highlight_duration = { config.highlight_duration, 'number' },
mappings = { config.mappings, 'table' },
n_lines = { config.n_lines, 'number' },
respect_selection_type = { config.respect_selection_type, 'boolean' },
search_method = { config.search_method, H.is_search_method },
silent = { config.silent, 'boolean' },
})
vim.validate({
['mappings.add'] = { config.mappings.add, 'string' },
['mappings.delete'] = { config.mappings.delete, 'string' },
['mappings.find'] = { config.mappings.find, 'string' },
['mappings.find_left'] = { config.mappings.find_left, 'string' },
['mappings.highlight'] = { config.mappings.highlight, 'string' },
['mappings.replace'] = { config.mappings.replace, 'string' },
['mappings.update_n_lines'] = { config.mappings.update_n_lines, 'string' },
['mappings.suffix_last'] = { config.mappings.suffix_last, 'string' },
['mappings.suffix_next'] = { config.mappings.suffix_next, 'string' },
})
return config
end
H.apply_config = function(config)
MiniSurround.config = config
local expr_map = function(lhs, rhs, desc) H.map('n', lhs, rhs, { expr = true, desc = desc }) end
--stylua: ignore start
-- Make regular mappings
local m = config.mappings
expr_map(m.add, H.make_operator('add', nil, nil, true), 'Add surrounding')
expr_map(m.delete, H.make_operator('delete'), 'Delete surrounding')
expr_map(m.replace, H.make_operator('replace'), 'Replace surrounding')
expr_map(m.find, H.make_operator('find', 'right'), 'Find right surrounding')
expr_map(m.find_left, H.make_operator('find', 'left'), 'Find left surrounding')
expr_map(m.highlight, H.make_operator('highlight'), 'Highlight surrounding')
H.map('n', m.update_n_lines, MiniSurround.update_n_lines, { desc = 'Update `MiniSurround.config.n_lines`' })
H.map('x', m.add, [[:<C-u>lua MiniSurround.add('visual')<CR>]], { desc = 'Add surrounding to selection' })
-- Make extended mappings
local suffix_map = function(lhs, suffix, rhs, desc)
-- Don't create extended mapping if user chose not to create regular one
if lhs == '' then return end
expr_map(lhs .. suffix, rhs, desc)
end
if m.suffix_last ~= '' then
local operator_prev = function(method, direction)
return H.make_operator(method, direction, 'prev')
end
local suff = m.suffix_last
suffix_map(m.delete, suff, operator_prev('delete'), 'Delete previous surrounding')
suffix_map(m.replace, suff, operator_prev('replace'), 'Replace previous surrounding')
suffix_map(m.find, suff, operator_prev('find', 'right'), 'Find previous right surrounding')
suffix_map(m.find_left, suff, operator_prev('find', 'left'), 'Find previous left surrounding')
suffix_map(m.highlight, suff, operator_prev('highlight'), 'Highlight previous surrounding')
end
if m.suffix_next ~= '' then
local operator_next = function(method, direction)
return H.make_operator(method, direction, 'next')
end
local suff = m.suffix_next
suffix_map(m.delete, suff, operator_next('delete'), 'Delete next surrounding')
suffix_map(m.replace, suff, operator_next('replace'), 'Replace next surrounding')
suffix_map(m.find, suff, operator_next('find', 'right'), 'Find next right surrounding')
suffix_map(m.find_left, suff, operator_next('find', 'left'), 'Find next left surrounding')
suffix_map(m.highlight, suff, operator_next('highlight'), 'Highlight next surrounding')
end
--stylua: ignore end
end
H.create_default_hl = function() vim.api.nvim_set_hl(0, 'MiniSurround', { default = true, link = 'IncSearch' }) end
H.is_disabled = function() return vim.g.minisurround_disable == true or vim.b.minisurround_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniSurround.config, vim.b.minisurround_config or {}, config or {})
end
H.is_search_method = function(x, x_name)
x = x or H.get_config().search_method
x_name = x_name or '`config.search_method`'
local allowed_methods = vim.tbl_keys(H.span_compare_methods)
if vim.tbl_contains(allowed_methods, x) then return true end
table.sort(allowed_methods)
local allowed_methods_string = table.concat(vim.tbl_map(vim.inspect, allowed_methods), ', ')
local msg = ([[%s should be one of %s.]]):format(x_name, allowed_methods_string)
return false, msg
end
H.validate_search_method = function(x, x_name)
local is_valid, msg = H.is_search_method(x, x_name)
if not is_valid then H.error(msg) end
end
-- Mappings -------------------------------------------------------------------
H.make_operator = function(task, direction, search_method, ask_for_textobject)
return function()
if H.is_disabled() then
-- Using `<Esc>` helps to stop moving cursor caused by current
-- implementation detail of adding `' '` inside expression mapping
return [[\<Esc>]]
end
H.cache = { count = vim.v.count1, direction = direction, search_method = search_method }
vim.o.operatorfunc = 'v:lua.MiniSurround.' .. task
-- NOTEs:
-- - Prepend with command to reset `vim.v.count1` to allow
-- `[count1]sa[count2][textobject]`.
-- - Concatenate `' '` to operator output to "disable" motion
-- required by `g@`. It is used to enable dot-repeatability.
return '<Cmd>echon ""<CR>g@' .. (ask_for_textobject and '' or ' ')
end
end
-- Work with surrounding info -------------------------------------------------
H.get_surround_spec = function(sur_type, use_cache)
local res
-- Try using cache
if use_cache then
res = H.cache[sur_type]
if res ~= nil then return res end
else
H.cache = {}
end
-- Prompt user to enter identifier of surrounding
local char = H.user_surround_id(sur_type)
if char == nil then return nil end
-- Get surround specification
res = H.make_surrounding_table()[char][sur_type]
-- Allow function returning spec or surrounding region(s)
if vim.is_callable(res) then res = res() end
-- Do nothing if supplied not appropriate structure
if not H.is_surrounding_info(res, sur_type) then return nil end
-- Wrap callable tables to be an actual functions. Otherwise they might be
-- confused with list of patterns.
if H.is_composed_pattern(res) then res = vim.tbl_map(H.wrap_callable_table, res) end
-- Track identifier for possible messages. Use metatable to pass
-- `vim.tbl_islist()` check.
res = setmetatable(res, { __index = { id = char } })
-- Cache result
if use_cache then H.cache[sur_type] = res end
return res
end
H.make_surrounding_table = function()
-- Extend builtins with data from `config`
local surroundings = vim.tbl_deep_extend('force', H.builtin_surroundings, H.get_config().custom_surroundings or {})
-- Add possibly missing information from default surrounding info
for char, info in pairs(surroundings) do
local default = H.get_default_surrounding_info(char)
surroundings[char] = vim.tbl_deep_extend('force', default, info)
end
-- Use default surrounding info for not supplied single character identifier
--stylua: ignore start
return setmetatable(surroundings, {
__index = function(_, key) return H.get_default_surrounding_info(key) end,
})
--stylua: ignore end
end
H.get_default_surrounding_info = function(char)
local char_esc = vim.pesc(char)
return { input = { char_esc .. '().-()' .. char_esc }, output = { left = char, right = char } }
end
H.is_surrounding_info = function(x, sur_type)
if sur_type == 'input' then
return H.is_composed_pattern(x) or H.is_region_pair(x) or H.is_region_pair_array(x)
elseif sur_type == 'output' then
return (type(x) == 'table' and type(x.left) == 'string' and type(x.right) == 'string')
end
end
H.is_region = function(x)
if type(x) ~= 'table' then return false end
local from_is_valid = type(x.from) == 'table' and type(x.from.line) == 'number' and type(x.from.col) == 'number'
-- Allow `to` to be `nil` to describe empty regions
local to_is_valid = true
if x.to ~= nil then
to_is_valid = type(x.to) == 'table' and type(x.to.line) == 'number' and type(x.to.col) == 'number'
end
return from_is_valid and to_is_valid
end
H.is_region_pair = function(x)
if type(x) ~= 'table' then return false end
return H.is_region(x.left) and H.is_region(x.right)
end
H.is_region_pair_array = function(x)
if not vim.tbl_islist(x) then return false end
for _, v in ipairs(x) do
if not H.is_region_pair(v) then return false end
end
return true
end
H.is_composed_pattern = function(x)
if not (vim.tbl_islist(x) and #x > 0) then return false end
for _, val in ipairs(x) do
local val_type = type(val)
if not (val_type == 'table' or val_type == 'string' or vim.is_callable(val)) then return false end
end
return true
end
-- Work with finding surrounding ----------------------------------------------
---@param surr_spec table Composed pattern. Last item(s) - extraction template.
---@param opts table Options.
---@private
H.find_surrounding = function(surr_spec, opts)
if surr_spec == nil then return end
if H.is_region_pair(surr_spec) then return surr_spec end
opts = vim.tbl_deep_extend('force', H.get_default_opts(), opts or {})
H.validate_search_method(opts.search_method, 'search_method')
local region_pair = H.find_surrounding_region_pair(surr_spec, opts)
if region_pair == nil then
local msg = ([[No surrounding '%s%s' found within %d line%s and `config.search_method = '%s'`.]]):format(
opts.n_times > 1 and opts.n_times or '',
surr_spec.id,
opts.n_lines,
opts.n_lines > 1 and 's' or '',
opts.search_method
)
H.message(msg)
end
return region_pair
end
H.find_surrounding_region_pair = function(surr_spec, opts)
local reference_region, n_times, n_lines = opts.reference_region, opts.n_times, opts.n_lines
if n_times == 0 then return end
-- Find `n_times` matching spans evolving from reference region span
-- First try to find inside 0-neighborhood
local neigh = H.get_neighborhood(reference_region, 0)
local reference_span = neigh.region_to_span(reference_region)
local find_next = function(cur_reference_span)
local res = H.find_best_match(neigh, surr_spec, cur_reference_span, opts)
-- If didn't find in 0-neighborhood, possibly try extend one
if res.span == nil then
-- Stop if no need to extend neighborhood
if n_lines == 0 or neigh.n_neighbors > 0 then return {} end
-- Update data with respect to new neighborhood
local cur_reference_region = neigh.span_to_region(cur_reference_span)
neigh = H.get_neighborhood(reference_region, n_lines)
reference_span = neigh.region_to_span(reference_region)
cur_reference_span = neigh.region_to_span(cur_reference_region)
-- Recompute based on new neighborhood
res = H.find_best_match(neigh, surr_spec, cur_reference_span, opts)
end
return res
end
local find_res = { span = reference_span }
for _ = 1, n_times do
find_res = find_next(find_res.span)
if find_res.span == nil then return end
end
-- Extract final span
local extract = function(span, extract_pattern)
-- Use table extract pattern to allow array of regions as surrounding spec
-- Pair of spans is constructed based on best region pair
if type(extract_pattern) == 'table' then return extract_pattern end
-- First extract local (with respect to best matched span) surrounding spans
local s = neigh['1d']:sub(span.from, span.to - 1)
local local_surr_spans = H.extract_surr_spans(s, extract_pattern)
-- Convert local spans to global
local off = span.from - 1
local left, right = local_surr_spans.left, local_surr_spans.right
return {
left = { from = left.from + off, to = left.to + off },
right = { from = right.from + off, to = right.to + off },
}
end
local final_spans = extract(find_res.span, find_res.extract_pattern)
local outer_span = { from = final_spans.left.from, to = final_spans.right.to }
-- Ensure that output region is different from reference.
if H.is_span_covering(reference_span, outer_span) then
find_res = find_next(find_res.span)
if find_res.span == nil then return end
final_spans = extract(find_res.span, find_res.extract_pattern)
outer_span = { from = final_spans.left.from, to = final_spans.right.to }
if H.is_span_covering(reference_span, outer_span) then return end
end
-- Convert to region pair
return { left = neigh.span_to_region(final_spans.left), right = neigh.span_to_region(final_spans.right) }
end
H.get_default_opts = function()
local config = H.get_config()
local cur_pos = vim.api.nvim_win_get_cursor(0)
return {
n_lines = config.n_lines,
n_times = H.cache.count or vim.v.count1,
-- Empty region at cursor position
reference_region = { from = { line = cur_pos[1], col = cur_pos[2] + 1 } },
search_method = H.cache.search_method or config.search_method,
}
end
-- Work with treesitter surrounding -------------------------------------------
H.prepare_captures = function(captures)
local is_capture = function(x) return type(x) == 'string' and x:sub(1, 1) == '@' end
if not (type(captures) == 'table' and is_capture(captures.outer) and is_capture(captures.inner)) then
H.error('Wrong format for `captures`. See `MiniSurround.gen_spec.input.treesitter()` for details.')
end
return { outer = captures.outer, inner = captures.inner }
end
H.get_matched_node_pairs_plugin = function(captures)
-- Hope that 'nvim-treesitter.query' is stable enough
local ts_queries = require('nvim-treesitter.query')
local ts_parsers = require('nvim-treesitter.parsers')
-- This is a modified version of `ts_queries.get_capture_matches_recursively`
-- source code which keeps track of match language
local matches = {}
local parser = ts_parsers.get_parser(0)
if parser then
parser:for_each_tree(function(tree, lang_tree)
local lang = lang_tree:lang()
local lang_matches = ts_queries.get_capture_matches(0, captures.outer, 'textobjects', tree:root(), lang)
for _, m in pairs(lang_matches) do
m.lang = lang
end
vim.list_extend(matches, lang_matches)
end)
end
return vim.tbl_map(
function(match)
local node_outer = match.node
-- Pick inner node as the biggest node matching inner query. This is
-- needed because query output is not quaranteed to come in order.
local matches_inner = ts_queries.get_capture_matches(0, captures.inner, 'textobjects', node_outer, match.lang)
local nodes_inner = vim.tbl_map(function(x) return x.node end, matches_inner)
return { outer = node_outer, inner = H.get_biggest_node(nodes_inner) }
end,
-- This call should handle multiple languages in buffer
matches
)
end
H.get_matched_node_pairs_builtin = function(captures)
-- Fetch treesitter data for buffer
local lang = vim.bo.filetype
local ok, parser = pcall(vim.treesitter.get_parser, 0, lang)
if not ok then H.error_treesitter('parser', lang) end
local query = vim.treesitter.get_query(lang, 'textobjects')
if query == nil then H.error_treesitter('query', lang) end
-- Remove leading '@'
local capture_outer, capture_inner = captures.outer:sub(2), captures.inner:sub(2)
-- Compute nodes matching outer capture
local nodes_outer = {}
for _, tree in ipairs(parser:trees()) do
vim.list_extend(nodes_outer, H.get_builtin_matched_nodes(capture_outer, tree:root(), query))
end
-- Make node pairs with biggest node matching inner capture inside outer node
return vim.tbl_map(function(node_outer)
local nodes_inner = H.get_builtin_matched_nodes(capture_inner, node_outer, query)
return { outer = node_outer, inner = H.get_biggest_node(nodes_inner) }
end, nodes_outer)
end
H.get_builtin_matched_nodes = function(capture, root, query)
local res = {}
for capture_id, node, _ in query:iter_captures(root, 0) do
if query.captures[capture_id] == capture then table.insert(res, node) end
end
return res
end
H.get_biggest_node = function(node_arr)
local best_node, best_byte_count = nil, -math.huge
for _, node in ipairs(node_arr) do
local _, _, start_byte = node:start()
local _, _, end_byte = node:end_()
local byte_count = end_byte - start_byte + 1
if best_byte_count < byte_count then
best_node, best_byte_count = node, byte_count
end
end
return best_node
end
H.error_treesitter = function(failed_get, lang)
local bufnr = vim.api.nvim_get_current_buf()
local msg = string.format([[Can not get %s for buffer %d and language '%s'.]], failed_get, bufnr, lang)
H.error(msg)
end
-- Work with matching spans ---------------------------------------------------
---@param neighborhood table Output of `get_neighborhood()`.
---@param surr_spec table
---@param reference_span table Span to cover.
---@param opts table Fields: <search_method>.
---@private
H.find_best_match = function(neighborhood, surr_spec, reference_span, opts)
local best_span, best_nested_pattern, current_nested_pattern
local f = function(span)
if H.is_better_span(span, best_span, reference_span, opts) then
best_span = span
best_nested_pattern = current_nested_pattern
end
end
if H.is_region_pair_array(surr_spec) then
-- Iterate over all spans representing outer regions in array
for _, region_pair in ipairs(surr_spec) do
-- Construct outer region used to find best region pair
local outer_region = { from = region_pair.left.from, to = region_pair.right.to or region_pair.right.from }
-- Consider outer region only if it is completely within neighborhood
if neighborhood.is_region_inside(outer_region) then
-- Make future extract pattern based directly on region pair
current_nested_pattern = {
{
left = neighborhood.region_to_span(region_pair.left),
right = neighborhood.region_to_span(region_pair.right),
},
}
f(neighborhood.region_to_span(outer_region))
end
end
else
-- Iterate over all matched spans
for _, nested_pattern in ipairs(H.cartesian_product(surr_spec)) do
current_nested_pattern = nested_pattern
H.iterate_matched_spans(neighborhood['1d'], nested_pattern, f)
end
end
local extract_pattern
if best_nested_pattern ~= nil then extract_pattern = best_nested_pattern[#best_nested_pattern] end
return { span = best_span, extract_pattern = extract_pattern }
end
H.iterate_matched_spans = function(line, nested_pattern, f)
local max_level = #nested_pattern
-- Keep track of visited spans to ensure only one call of `f`.
-- Example: `((a) (b))`, `{'%b()', '%b()'}`
local visited = {}
local process
process = function(level, level_line, level_offset)
local pattern = nested_pattern[level]
local next_span = function(s, init) return H.string_find(s, pattern, init) end
if vim.is_callable(pattern) then next_span = pattern end
local is_same_balanced = type(pattern) == 'string' and pattern:match('^%%b(.)%1$') ~= nil
local init = 1
while init <= level_line:len() do
local from, to = next_span(level_line, init)
if from == nil then break end
if level == max_level then
local found_match = H.new_span(from + level_offset, to + level_offset)
local found_match_id = string.format('%s_%s', found_match.from, found_match.to)
if not visited[found_match_id] then
f(found_match)
visited[found_match_id] = true
end
else
local next_level_line = level_line:sub(from, to)
local next_level_offset = level_offset + from - 1
process(level + 1, next_level_line, next_level_offset)
end
-- Start searching from right end to implement "balanced" pair.
-- This doesn't work with regular balanced pattern because it doesn't
-- capture nested brackets.
init = (is_same_balanced and to or from) + 1
end
end
process(1, line, 0)
end
-- NOTE: spans are end-exclusive to allow empty spans via `from == to`
H.new_span = function(from, to) return { from = from, to = to == nil and from or (to + 1) } end
---@param candidate table Candidate span to test against `current`.
---@param current table|nil Current best span.
---@param reference table Reference span to cover.
---@param opts table Fields: <search_method>.
---@private
H.is_better_span = function(candidate, current, reference, opts)
-- Candidate should be never equal or nested inside reference
if H.is_span_covering(reference, candidate) or H.is_span_equal(candidate, reference) then return false end
return H.span_compare_methods[opts.search_method](candidate, current, reference)
end
H.span_compare_methods = {
cover = function(candidate, current, reference)
local res = H.is_better_covering_span(candidate, current, reference)
if res ~= nil then return res end
-- If both are not covering, `candidate` is not better (as it must cover)
return false
end,
cover_or_next = function(candidate, current, reference)
local res = H.is_better_covering_span(candidate, current, reference)
if res ~= nil then return res end
-- If not covering, `candidate` must be "next" and closer to reference
if not H.is_span_on_left(reference, candidate) then return false end
if current == nil then return true end
local dist = H.span_distance.next
return dist(candidate, reference) < dist(current, reference)
end,
cover_or_prev = function(candidate, current, reference)
local res = H.is_better_covering_span(candidate, current, reference)
if res ~= nil then return res end
-- If not covering, `candidate` must be "previous" and closer to reference
if not H.is_span_on_left(candidate, reference) then return false end
if current == nil then return true end
local dist = H.span_distance.prev
return dist(candidate, reference) < dist(current, reference)
end,
cover_or_nearest = function(candidate, current, reference)
local res = H.is_better_covering_span(candidate, current, reference)
if res ~= nil then return res end
-- If not covering, `candidate` must be closer to reference
if current == nil then return true end
local dist = H.span_distance.near
return dist(candidate, reference) < dist(current, reference)
end,
next = function(candidate, current, reference)
if H.is_span_covering(candidate, reference) then return false end
-- `candidate` must be "next" and closer to reference
if not H.is_span_on_left(reference, candidate) then return false end
if current == nil then return true end
local dist = H.span_distance.next
return dist(candidate, reference) < dist(current, reference)
end,
prev = function(candidate, current, reference)
if H.is_span_covering(candidate, reference) then return false end
-- `candidate` must be "previous" and closer to reference
if not H.is_span_on_left(candidate, reference) then return false end
if current == nil then return true end
local dist = H.span_distance.prev
return dist(candidate, reference) < dist(current, reference)
end,
nearest = function(candidate, current, reference)
if H.is_span_covering(candidate, reference) then return false end
-- `candidate` must be closer to reference
if current == nil then return true end
local dist = H.span_distance.near
return dist(candidate, reference) < dist(current, reference)
end,
}
H.span_distance = {
-- Other possible choices of distance between [a1, a2] and [b1, b2]:
-- - Hausdorff distance: max(|a1 - b1|, |a2 - b2|).
-- Source:
-- https://math.stackexchange.com/questions/41269/distance-between-two-ranges
-- - Minimum distance: min(|a1 - b1|, |a2 - b2|).
-- Distance is chosen so that "next span" in certain direction is the closest
next = function(span_1, span_2) return math.abs(span_1.from - span_2.from) end,
prev = function(span_1, span_2) return math.abs(span_1.to - span_2.to) end,
near = function(span_1, span_2) return math.min(math.abs(span_1.from - span_2.from), math.abs(span_1.to - span_2.to)) end,
}
H.is_better_covering_span = function(candidate, current, reference)
local candidate_is_covering = H.is_span_covering(candidate, reference)
local current_is_covering = H.is_span_covering(current, reference)
if candidate_is_covering and current_is_covering then
-- Covering candidate is better than covering current if it is narrower
return (candidate.to - candidate.from) < (current.to - current.from)
end
if candidate_is_covering and not current_is_covering then return true end
if not candidate_is_covering and current_is_covering then return false end
-- Return `nil` if neither span is covering
return nil
end
--stylua: ignore
H.is_span_covering = function(span, span_to_cover)
if span == nil or span_to_cover == nil then return false end
if span.from == span.to then
return (span.from == span_to_cover.from) and (span_to_cover.to == span.to)
end
if span_to_cover.from == span_to_cover.to then
return (span.from <= span_to_cover.from) and (span_to_cover.to < span.to)
end
return (span.from <= span_to_cover.from) and (span_to_cover.to <= span.to)
end
H.is_span_equal = function(span_1, span_2)
if span_1 == nil or span_2 == nil then return false end
return (span_1.from == span_2.from) and (span_1.to == span_2.to)
end
H.is_span_on_left = function(span_1, span_2)
if span_1 == nil or span_2 == nil then return false end
return (span_1.from <= span_2.from) and (span_1.to <= span_2.to)
end
H.is_point_inside_spans = function(point, spans)
for _, span in ipairs(spans) do
if span[1] <= point and point <= span[2] then return true end
end
return false
end
-- Work with operator marks ---------------------------------------------------
H.get_marks_pos = function(mode)
-- Region is inclusive on both ends
local mark1, mark2
if mode == 'visual' then
mark1, mark2 = '<', '>'
else
mark1, mark2 = '[', ']'
end
local pos1 = vim.api.nvim_buf_get_mark(0, mark1)
local pos2 = vim.api.nvim_buf_get_mark(0, mark2)
local selection_type = H.get_selection_type(mode)
-- Tweak position in linewise mode as marks are placed on the first column
if selection_type == 'linewise' then
-- Move start mark past the indent
local _, line1_indent = vim.fn.getline(pos1[1]):find('^%s*')
pos1[2] = line1_indent
-- Move end mark to the last character (` - 2` here because `col()` returns
-- column right after the last 1-based column)
pos2[2] = vim.fn.col({ pos2[1], '$' }) - 2
end
-- Make columns 1-based instead of 0-based. This is needed because
-- `nvim_buf_get_mark()` returns the first 0-based byte of mark symbol and
-- all the following operations are done with Lua's 1-based indexing.
pos1[2], pos2[2] = pos1[2] + 1, pos2[2] + 1
-- Tweak second position to respect multibyte characters. Reasoning:
-- - These positions will be used with `region_replace()` to add some text,
-- which operates on byte columns.
-- - For the first mark we want the first byte of symbol, then text will be
-- insert to the left of the mark.
-- - For the second mark we want last byte of symbol. To add surrounding to
-- the right, use `pos2[2] + 1`.
if mode == 'visual' and vim.o.selection == 'exclusive' then
-- Respect 'selection' option
pos2[2] = pos2[2] - 1
else
local line2 = vim.fn.getline(pos2[1])
-- Use `math.min()` because it might lead to 'index out of range' error
-- when mark is positioned at the end of line (that extra space which is
-- selected when selecting with `v$`)
local utf_index = vim.str_utfindex(line2, math.min(#line2, pos2[2]))
-- This returns the last byte inside character because `vim.str_byteindex()`
-- 'rounds upwards to the end of that sequence'.
pos2[2] = vim.str_byteindex(line2, utf_index)
end
return {
first = { line = pos1[1], col = pos1[2] },
second = { line = pos2[1], col = pos2[2] },
selection_type = selection_type,
}
end
H.get_selection_type = function(mode)
if (mode == 'char') or (mode == 'visual' and vim.fn.visualmode() == 'v') then return 'charwise' end
if (mode == 'line') or (mode == 'visual' and vim.fn.visualmode() == 'V') then return 'linewise' end
if (mode == 'block') or (mode == 'visual' and vim.fn.visualmode() == '\22') then return 'blockwise' end
end
-- Work with cursor -----------------------------------------------------------
H.set_cursor = function(line, col) vim.api.nvim_win_set_cursor(0, { line, col - 1 }) end
H.set_cursor_nonblank = function(line)
H.set_cursor(line, 1)
vim.cmd('normal! ^')
end
H.compare_pos = function(pos1, pos2)
if pos1.line < pos2.line then return '<' end
if pos1.line > pos2.line then return '>' end
if pos1.col < pos2.col then return '<' end
if pos1.col > pos2.col then return '>' end
return '='
end
H.cursor_cycle = function(pos_array, dir)
local cur_pos = vim.api.nvim_win_get_cursor(0)
cur_pos = { line = cur_pos[1], col = cur_pos[2] + 1 }
local compare, to_left, to_right, res_pos
-- NOTE: `pos_array` should be an increasingly ordered array of positions
for _, pos in pairs(pos_array) do
compare = H.compare_pos(cur_pos, pos)
-- Take position when moving to left if cursor is strictly on right.
-- This will lead to updating `res_pos` until the rightmost such position.
to_left = compare == '>' and dir == 'left'
-- Take position when moving to right if cursor is strictly on left.
-- This will update result only once leading to the leftmost such position.
to_right = res_pos == nil and compare == '<' and dir == 'right'
if to_left or to_right then res_pos = pos end
end
res_pos = res_pos or (dir == 'right' and pos_array[1] or pos_array[#pos_array])
H.set_cursor(res_pos.line, res_pos.col)
end
-- Work with user input -------------------------------------------------------
H.user_surround_id = function(sur_type)
-- Get from user single character surrounding identifier
local needs_help_msg = true
vim.defer_fn(function()
if not needs_help_msg then return end
local msg = string.format('Enter %s surrounding identifier (single character) ', sur_type)
H.echo(msg)
H.cache.msg_shown = true
end, 1000)
local ok, char = pcall(vim.fn.getcharstr)
needs_help_msg = false
H.unecho()
-- Terminate if couldn't get input (like with <C-c>) or it is `<Esc>`
if not ok or char == '\27' then return nil end
if char:find('^[%w%p%s]$') == nil then
H.message('Input must be single character: alphanumeric, punctuation, or space.')
return nil
end
return char
end
-- Work with positions --------------------------------------------------------
H.pos_to_left = function(pos)
if pos.line == 1 and pos.col == 1 then return { line = pos.line, col = pos.col } end
if pos.col == 1 then return { line = pos.line - 1, col = H.get_line_cols(pos.line - 1) } end
return { line = pos.line, col = pos.col - 1 }
end
H.pos_to_right = function(pos)
local n_cols = H.get_line_cols(pos.line)
-- Using `>` and not `>=` helps with removing '\n' and in the last line
if pos.line == vim.api.nvim_buf_line_count(0) and pos.col > n_cols then return { line = pos.line, col = n_cols } end
if pos.col > n_cols then return { line = pos.line + 1, col = 1 } end
return { line = pos.line, col = pos.col + 1 }
end
-- Work with regions ----------------------------------------------------------
H.region_replace = function(region, text)
-- Compute start and end position for `vim.api.nvim_buf_set_text()`.
-- Indexing is zero-based. Rows - end-inclusive, columns - end-exclusive.
local start_row, start_col = region.from.line - 1, region.from.col - 1
local end_row, end_col
-- Allow empty region
if H.region_is_empty(region) then
end_row, end_col = start_row, start_col
else
end_row, end_col = region.to.line - 1, region.to.col
-- Possibly correct to allow removing new line character
if end_row < vim.api.nvim_buf_line_count(0) and H.get_line_cols(end_row + 1) < end_col then
end_row, end_col = end_row + 1, 0
end
end
-- Allow single string as replacement
if type(text) == 'string' then text = { text } end
-- Allow `\n` in string to denote new lines
if #text > 0 then text = vim.split(table.concat(text, '\n'), '\n') end
-- Replace. Use `pcall()` to do nothing if some position is out of bounds.
pcall(vim.api.nvim_buf_set_text, 0, start_row, start_col, end_row, end_col, text)
end
H.surr_to_pos_array = function(surr)
local res = {}
local append_position = function(pos, correction_direction)
if pos == nil then return end
-- Don't go past the line if it is not empty
if H.get_line_cols(pos.line) < pos.col and pos.col > 1 then
pos = correction_direction == 'left' and H.pos_to_left(pos) or H.pos_to_right(pos)
end
-- Don't add duplicate. Assumes that positions are used increasingly.
local line, col = pos.line, pos.col
local last = res[#res]
if not (last ~= nil and last.line == line and last.col == col) then
table.insert(res, { line = line, col = col })
end
end
-- Possibly correct position towards inside of surrounding region
-- Also don't add positions from empty regions
if not H.region_is_empty(surr.left) then
append_position(surr.left.from, 'right')
append_position(surr.left.to, 'right')
end
if not H.region_is_empty(surr.right) then
append_position(surr.right.from, 'left')
append_position(surr.right.to, 'left')
end
return res
end
H.region_highlight = function(buf_id, region)
-- Don't highlight empty region
if H.region_is_empty(region) then return end
local ns_id = H.ns_id.highlight
-- Indexing is zero-based. Rows - end-inclusive, columns - end-exclusive.
local from_line, from_col, to_line, to_col =
region.from.line - 1, region.from.col - 1, region.to.line - 1, region.to.col
vim.highlight.range(buf_id, ns_id, 'MiniSurround', { from_line, from_col }, { to_line, to_col })
end
H.region_unhighlight = function(buf_id, region)
local ns_id = H.ns_id.highlight
-- Remove highlights from whole lines as it is the best available granularity
vim.api.nvim_buf_clear_namespace(buf_id, ns_id, region.from.line - 1, (region.to or region.from).line)
end
H.region_is_empty = function(region) return region.to == nil end
-- Work with text -------------------------------------------------------------
H.get_range_indent = function(from_line, to_line)
local n_indent, indent = math.huge, nil
local lines = vim.api.nvim_buf_get_lines(0, from_line - 1, to_line, true)
local n_indent_cur, indent_cur
for _, l in ipairs(lines) do
_, n_indent_cur, indent_cur = l:find('^(%s*)')
-- Don't indent blank lines
if n_indent_cur < n_indent and n_indent_cur < l:len() then
n_indent, indent = n_indent_cur, indent_cur
end
end
return indent or ''
end
H.shift_indent = function(command, from_line, to_line)
if to_line < from_line then return end
vim.cmd('silent ' .. from_line .. ',' .. to_line .. command)
end
H.is_line_blank = function(line_num) return vim.fn.nextnonblank(line_num) ~= line_num end
-- Work with Lua patterns -----------------------------------------------------
H.extract_surr_spans = function(s, extract_pattern)
local positions = { s:match(extract_pattern) }
local is_all_numbers = true
for _, pos in ipairs(positions) do
if type(pos) ~= 'number' then is_all_numbers = false end
end
local is_valid_positions = is_all_numbers and (#positions == 2 or #positions == 4)
if not is_valid_positions then
local msg = 'Could not extract proper positions (two or four empty captures) from '
.. string.format([[string '%s' with extraction pattern '%s'.]], s, extract_pattern)
H.error(msg)
end
if #positions == 2 then
return { left = H.new_span(1, positions[1] - 1), right = H.new_span(positions[2], s:len()) }
end
return { left = H.new_span(positions[1], positions[2] - 1), right = H.new_span(positions[3], positions[4] - 1) }
end
-- Work with cursor neighborhood ----------------------------------------------
---@param reference_region table Reference region.
---@param n_neighbors number Maximum number of neighbors to include before
--- start line and after end line.
---@private
H.get_neighborhood = function(reference_region, n_neighbors)
-- Compute '2d neighborhood' of (possibly empty) region
local from_line, to_line = reference_region.from.line, (reference_region.to or reference_region.from).line
local line_start = math.max(1, from_line - n_neighbors)
local line_end = math.min(vim.api.nvim_buf_line_count(0), to_line + n_neighbors)
local neigh2d = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false)
-- Append 'newline' character to distinguish between lines in 1d case
for k, v in pairs(neigh2d) do
neigh2d[k] = v .. '\n'
end
-- '1d neighborhood': position is determined by offset from start
local neigh1d = table.concat(neigh2d, '')
-- Convert 2d buffer position to 1d offset
local pos_to_offset = function(pos)
if pos == nil then return nil end
local line_num = line_start
local offset = 0
while line_num < pos.line do
offset = offset + neigh2d[line_num - line_start + 1]:len()
line_num = line_num + 1
end
return offset + pos.col
end
-- Convert 1d offset to 2d buffer position
local offset_to_pos = function(offset)
if offset == nil then return nil end
local line_num = 1
local line_offset = 0
while line_num <= #neigh2d and line_offset + neigh2d[line_num]:len() < offset do
line_offset = line_offset + neigh2d[line_num]:len()
line_num = line_num + 1
end
return { line = line_start + line_num - 1, col = offset - line_offset }
end
-- Convert 2d region to 1d span
local region_to_span = function(region)
if region == nil then return nil end
local is_empty = region.to == nil
local to = region.to or region.from
return { from = pos_to_offset(region.from), to = pos_to_offset(to) + (is_empty and 0 or 1) }
end
-- Convert 1d span to 2d region
local span_to_region = function(span)
if span == nil then return nil end
-- NOTE: this might lead to outside of line positions due to added `\n` at
-- the end of lines in 1d-neighborhood.
local res = { from = offset_to_pos(span.from) }
-- Convert empty span to empty region
if span.from < span.to then res.to = offset_to_pos(span.to - 1) end
return res
end
local is_region_inside = function(region)
local res = line_start <= region.from.line
if region.to ~= nil then res = res and (region.to.line <= line_end) end
return res
end
return {
n_neighbors = n_neighbors,
region = reference_region,
['1d'] = neigh1d,
['2d'] = neigh2d,
pos_to_offset = pos_to_offset,
offset_to_pos = offset_to_pos,
region_to_span = region_to_span,
span_to_region = span_to_region,
is_region_inside = is_region_inside,
}
end
-- Utilities ------------------------------------------------------------------
H.echo = function(msg, is_important)
if H.get_config().silent then return end
-- Construct message chunks
msg = type(msg) == 'string' and { { msg } } or msg
table.insert(msg, 1, { '(mini.surround) ', 'WarningMsg' })
-- Avoid hit-enter-prompt
local max_width = vim.o.columns * math.max(vim.o.cmdheight - 1, 0) + vim.v.echospace
local chunks, tot_width = {}, 0
for _, ch in ipairs(msg) do
local new_ch = { vim.fn.strcharpart(ch[1], 0, max_width - tot_width), ch[2] }
table.insert(chunks, new_ch)
tot_width = tot_width + vim.fn.strdisplaywidth(new_ch[1])
if tot_width >= max_width then break end
end
-- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
vim.cmd([[echo '' | redraw]])
vim.api.nvim_echo(chunks, is_important, {})
end
H.unecho = function()
if H.cache.msg_shown then vim.cmd([[echo '' | redraw]]) end
end
H.message = function(msg) H.echo(msg, true) end
H.error = function(msg) error(string.format('(mini.surround) %s', msg)) end
H.map = function(mode, lhs, rhs, opts)
if lhs == '' then return end
opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
vim.keymap.set(mode, lhs, rhs, opts)
end
H.get_line_cols = function(line_num) return vim.fn.getline(line_num):len() end
H.string_find = function(s, pattern, init)
init = init or 1
-- Match only start of full string if pattern says so.
-- This is needed because `string.find()` doesn't do this.
-- Example: `string.find('(aaa)', '^.*$', 4)` returns `4, 5`
if pattern:sub(1, 1) == '^' then
if init > 1 then return nil end
return string.find(s, pattern)
end
-- Handle patterns `x.-y` differently: make match as small as possible. This
-- doesn't allow `x` be present inside `.-` match, just as with `yyy`. Which
-- also leads to a behavior similar to punctuation id (like with `va_`): no
-- covering is possible, only next, previous, or nearest.
local check_left, _, prev = string.find(pattern, '(.)%.%-')
local is_pattern_special = check_left ~= nil and prev ~= '%'
if not is_pattern_special then return string.find(s, pattern, init) end
-- Make match as small as possible
local from, to = string.find(s, pattern, init)
if from == nil then return end
local cur_from, cur_to = from, to
while cur_to == to do
from, to = cur_from, cur_to
cur_from, cur_to = string.find(s, pattern, cur_from + 1)
end
return from, to
end
---@param arr table List of items. If item is list, consider as set for
--- product. Else - make it single item list.
---@private
H.cartesian_product = function(arr)
if not (type(arr) == 'table' and #arr > 0) then return {} end
arr = vim.tbl_map(function(x) return vim.tbl_islist(x) and x or { x } end, arr)
local res, cur_item = {}, {}
local process
process = function(level)
for i = 1, #arr[level] do
table.insert(cur_item, arr[level][i])
if level == #arr then
-- Flatten array to allow tables as elements of step tables
table.insert(res, vim.tbl_flatten(cur_item))
else
process(level + 1)
end
table.remove(cur_item, #cur_item)
end
end
process(1)
return res
end
H.wrap_callable_table = function(x)
if vim.is_callable(x) and type(x) == 'table' then return function(...) return x(...) end end
return x
end
return MiniSurround