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

Skip to content

Latest commit

 

History

History
2233 lines (1961 loc) · 91.9 KB

surround.lua

File metadata and controls

2233 lines (1961 loc) · 91.9 KB
--- *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 correspon 10000 ding 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 = 'y 10BC8 s',
--- 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 |vim.treesitter.query.get()| or |get_query()|, depending on your
--- Neovim version).
--- 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 plugin
--- 'nvim-treesitter/nvim-treesitter-textobjects', 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') and pcall(require, 'nvim-treesitter.query')
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
0