--- *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: ''. --- --- # 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?[[]]` - add (`sa`) for inner word (`iw`) interactive --- surrounding (`?`): `[[` for left and `]]` for right. --- - `2sdf` - delete (`sd`) second (`2`) surrounding function call (`f`). --- - `sr)tdiv` - replace (`sr`) surrounding parenthesis (`)`) with tag --- (`t`) with identifier 'div' (`div` 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 >! | !aa ! | < < aa > > | --- |---|---------------|---------------|-------------|-----------------| --- | ) | Balanced () | !( *a (bb) )! | ! aa (bb) ! | (( aa (bb) )) | --- | ] | Balanced [] | ![ *a [bb] ]! | ! aa [bb] ! | [[ aa [bb] ]] | --- | } | Balanced {} | !{ *a {bb} }! | ! aa {bb} ! | {{ aa {bb} }} | --- | > | Balanced <> | !< *a >! | ! aa ! | << aa >> | --- | 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 | !*! | !a! | a | --- | | | | | (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: and --- for inclusive start and end positions ( might be `nil` to --- describe empty region). Each position is also a table with line --- and column (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: and 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: --- - - defines how to find and extract surrounding for "input" --- operations (like `delete`). See more in 'Input surrounding' setction. --- - - 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 (plain text string) and (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', [[:lua MiniSurround.add('visual')]], { 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 or 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 '' 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 '' 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 '' 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 '' end -- Get output surround info local new_surr_info = H.get_surround_spec('output', true) if new_surr_info == nil then return '' 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 '' 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 '' 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 '' 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 `` and ``, 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 `` and entering empty string with immediate `` 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('', 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 `` 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: and --- (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 and 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//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 and 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: --- - - 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][^<>]->.-', '^<.->().*()$' }, 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 = '' } 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, [[:lua MiniSurround.add('visual')]], { 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 `` helps to stop moving cursor caused by current -- implementation detail of adding `' '` inside expression mapping return [[\]] 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 'echon ""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: . ---@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: . ---@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 ) or it is `` 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