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

Skip to content

Latest commit

 

History

History
2300 lines (2023 loc) · 93.9 KB

surround.lua

File metadata and controls

2300 lines (2023 loc) · 93.9 KB
---
--- 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'` (default) - use only covering match. Don't use either previous or
--- next; report that there is no surrounding found.
--- - `'cover_or_next'` - 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 preserve dot-repeat support, respect |[count]|.
--- - 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
-- This also affects (purely informational) helper messages shown after
-- idle time if user input is required.
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 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 '<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 function values generating
--- corresponding surrounding specification.
---
---@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,
--- output should have at least an intended file): >vim
---
--- :lua print(vim.inspect(vim.treesitter.query.get_files('lua','textobjects')))
--- <
--- Example configuration for function definition textobject with
--- 'nvim-treesitter/nvim-treesitter-textobjects' captures: >lua
---
--- 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' })
--- },
--- }
--- })
--- <
--- Notes:
0