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

Skip to content

Latest commit

 

History

History
2013 lines (1696 loc) · 80.8 KB

bracketed.lua

File metadata and controls

2013 lines (1696 loc) · 80.8 KB
---@text Options ~
---
--- Each entry configures target with the same name and can have data configuring
--- mapping suffix and target options.
---
--- Example of configuration: >lua
---
--- require('mini.bracketed').setup({
--- -- Map [N, [n, ]n, ]N for conflict marker like in 'tpope/vim-unimpaired'
--- conflict = { suffix = 'n' },
---
--- -- Make diagnostic advance only by errors
--- diagnostic = { options = { severity = vim.diagnostic.severity.ERROR } },
---
--- -- Disable creation of mappings for `indent` target (for example,
--- -- in favor of ones from |mini.indentscope|)
--- indent = { suffix = '' },
---
--- -- Disable mappings for `window` target in favor of custom ones
--- window = { suffix = '' },
--- })
---
--- -- Create custom `window` mappings
--- local map = vim.keymap.set
--- map('n', '<Leader>wH', "<Cmd>lua MiniBracketed.window('first')<CR>")
--- map('n', '<Leader>wh', "<Cmd>lua MiniBracketed.window('backward')<CR>")
--- map('n', '<Leader>wl', "<Cmd>lua MiniBracketed.window('forward')<CR>")
--- map('n', '<Leader>wL', "<Cmd>lua MiniBracketed.window('last')<CR>")
--- <
--- ## Suffix ~
---
--- The `suffix` key is used to create target mappings.
---
--- Supply empty string to disable mapping creation for that particular target.
--- To create a completely different mapping (like with |<Leader>|) use target
--- function manually.
---
--- Using `lower-suffix` and `upper-suffix` (lower and upper case suffix) for
--- a single target the following mappings are created:
--- - `[` + `upper-suffix` : go first.
--- - `[` + `lower-suffix` : go backward.
--- - `]` + `lower-suffix` : go forward.
--- - `]` + `upper-suffix` : go last.
---
--- When supplied with a non-letter, only forward/backward mappings are created.
---
--- ## Options ~
---
--- The `options` key is directly forwarded as `opts` to corresponding Lua function.
MiniBracketed.config = {
-- First-level elements are tables describing behavior of a target:
--
-- - <suffix> - single character suffix. Used after `[` / `]` in mappings.
-- For example, with `b` creates `[B`, `[b`, `]b`, `]B` mappings.
-- Supply empty string `''` to not create mappings.
--
-- - <options> - table overriding target options.
--
-- See `:h MiniBracketed.config` for more info.
buffer = { suffix = 'b', options = {} },
comment = { suffix = 'c', options = {} },
conflict = { suffix = 'x', options = {} },
diagnostic = { suffix = 'd', options = {} },
file = { suffix = 'f', options = {} },
indent = { suffix = 'i', options = {} },
jump = { suffix = 'j', options = {} },
location = { suffix = 'l', options = {} },
oldfile = { suffix = 'o', options = {} },
quickfix = { suffix = 'q', options = {} },
treesitter = { suffix = 't', options = {} },
undo = { suffix = 'u', options = {} },
window = { suffix = 'w', options = {} },
yank = { suffix = 'y', options = {} },
}
--minidoc_afterlines_end
--- Listed buffer
---
--- Go to next/previous listed buffer. Order by their number (see |bufnr()|).
---
--- Direction "forward" increases number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.buffer = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'buffer')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().buffer.options, opts or {})
-- Define iterator that traverses all valid listed buffers
-- (should be same as `:bnext` / `:bprev`)
local buf_list = vim.api.nvim_list_bufs()
local is_listed = function(buf_id) return vim.api.nvim_buf_is_valid(buf_id) and vim.bo[buf_id].buflisted end
local iterator = {}
iterator.next = function(buf_id)
for id = buf_id + 1, buf_list[#buf_list] do
if is_listed(id) then return id end
end
end
iterator.prev = function(buf_id)
for id = buf_id - 1, buf_list[1], -1 do
if is_listed(id) then return id end
end
end
iterator.state = vim.api.nvim_get_current_buf()
iterator.start_edge = buf_list[1] - 1
iterator.end_edge = buf_list[#buf_list] + 1
-- Iterate
local res_buf_id = MiniBracketed.advance(iterator, direction, opts)
if res_buf_id == iterator.state then return end
-- Apply
vim.api.nvim_set_current_buf(res_buf_id)
end
--- Comment block
---
--- Go to next/previous comment block. Only linewise comments using
--- 'commentsring' are recognized.
---
--- Direction "forward" increases line number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
--- __bracketed_add_to_jumplist
--- - <block_side> `(string)` - which side of comment block to use. One of
--- "near" (default; use nearest side), "start" (use first line), "end"
10000
--- (use last line), "both" (use both first and last lines).
MiniBracketed.comment = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'comment')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, block_side = 'near', n_times = vim.v.count1, wrap = true },
H.get_config().comment.options,
opts or {}
)
-- Compute loop data to traverse target commented lines in current buffer
local is_commented = H.make_comment_checker()
if is_commented == nil then return end
local predicate = ({
near = function(_, cur, _, recent) return cur and not recent end,
start = function(above, cur, _, _) return cur and not above end,
['end'] = function(_, cur, below, _) return cur and not below end,
both = function(above, cur, below, _) return cur and not (above and below) end,
})[opts.block_side]
if predicate == nil then return end
-- Define iterator
local iterator = {}
local n_lines = vim.api.nvim_buf_line_count(0)
iterator.next = function(line_num)
local above, cur = is_commented(line_num), is_commented(line_num + 1)
for lnum = line_num + 1, n_lines do
local below = is_commented(lnum + 1)
if predicate(above, cur, below, above) then return lnum end
above, cur = cur, below
end
end
iterator.prev = function(line_num)
local cur, below = is_commented(line_num - 1), is_commented(line_num)
for lnum = line_num - 1, 1, -1 do
local above = is_commented(lnum - 1)
if predicate(above, cur, below, below) then return lnum end
below, cur = cur, above
end
end
iterator.state = vim.fn.line('.')
iterator.start_edge = 0
iterator.end_edge = n_lines + 1
-- Iterate
local res_line_num = MiniBracketed.advance(iterator, direction, opts)
local is_outside = res_line_num <= 0 or n_lines < res_line_num
if res_line_num == nil or res_line_num == iterator.state or is_outside then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply. Open just enough folds and put cursor on first non-blank.
H.set_cursor(res_line_num, 0)
vim.cmd('normal! zv^')
end
--- Git conflict marker
---
--- Go to next/previous lines containing Git conflict marker. That is, if it
--- starts with "<<<<<<< ", ">>>>>>> ", or is "=======".
---
--- Direction "forward" increases line number, "backward" - decreases.
---
--- Notes:
--- - Using this target in Operator-pending mode allows the following approach
--- at resolving merge conflicts:
--- - Place cursor on `=======` line.
--- - Execute one of these: `d]x[xdd` (choose upper part) or
--- `d[x]xdd` (choose lower part).
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
--- __bracketed_add_to_jumplist
MiniBracketed.conflict = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'conflict')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, n_times = vim.v.count1, wrap = true },
H.get_config().conflict.options,
opts or {}
)
-- Define iterator that traverses all conflict markers in current buffer
local n_lines = vim.api.nvim_buf_line_count(0)
local iterator = {}
iterator.next = function(line_num)
for lnum = line_num + 1, n_lines do
if H.is_conflict_mark(lnum) then return lnum end
end
end
iterator.prev = function(line_num)
for lnum = line_num - 1, 1, -1 do
if H.is_conflict_mark(lnum) then return lnum end
end
end
iterator.state = vim.fn.line('.')
iterator.start_edge = 0
iterator.end_edge = n_lines + 1
-- Iterate
local res_line_num = MiniBracketed.advance(iterator, direction, opts)
local is_outside = res_line_num <= 0 or n_lines < res_line_num
if res_line_num == nil or res_line_num == iterator.state or is_outside then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply. Open just enough folds and put cursor on first non-blank.
H.set_cursor(res_line_num, 0)
vim.cmd('normal! zv^')
end
--- Diagnostic
---
--- Go to next/previous diagnostic. This is mostly similar to built-in
--- |vim.diagnostic.jump()| (on Neovim<0.11 it is |vim.diagnostic.goto_next()| and
--- |vim.diagnostic.goto_prev()|) which has an interface and behavior
--- consistent with other methods of the module.
---
--- Direction "forward" increases line number, "backward" - decreases.
---
--- Notes:
--- - Using `severity` option, this target can be used in mappings like "go to
--- next/previous error" (), etc. Using code similar to this: >lua
---
--- local severity_error = vim.diagnostic.severity.ERROR
--- -- Use these inside custom mappings
--- MiniBracketed.diagnostic('forward', { severity = severity_error })
--- MiniBracketed.diagnostic('backward', { severity = severity_error })
--- <
---@param direction __bracketed_direction
---@param opts __bracketed_opts
--- - <float> `(boolean|table)` - control floating window after movement.
--- For available values see |vim.diagnostic.goto_next()|.
--- - <severity> `(string|table)` - which severity to use.
--- For available values see |diagnostic-severity|.
MiniBracketed.diagnostic = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'diagnostic')
local default_opts = { float = vim.diagnostic.config().float, n_times = vim.v.count1, severity = nil, wrap = true }
local buf_id = vim.api.nvim_get_current_buf()
if vim.is_callable(default_opts.float) then default_opts.float = default_opts.float(nil, buf_id) end
opts = vim.tbl_extend('force', default_opts, H.get_config().diagnostic.options, opts or {})
-- Define iterator that traverses all diagnostic entries in current buffer
local to_cursor_pos = function(pos) return { pos[1] + 1, pos[2] } end
local pos_field = vim.fn.has('nvim-0.11') == 1 and 'pos' or 'cursor_position'
local iterator = {}
iterator.next = function(position)
local next_opts = { [pos_field] = to_cursor_pos(position), severity = opts.severity, wrap = false }
local next = vim.diagnostic.get_next(next_opts)
if next == nil then return end
return { next.lnum, next.col }
end
iterator.prev = function(position)
local prev_opts = { [pos_field] = to_cursor_pos(position), severity = opts.severity, wrap = false }
local prev = vim.diagnostic.get_prev(prev_opts)
if prev == nil then return end
return { prev.lnum, prev.col }
end
-- - Define states with zero-based indexing as used in `vim.diagnostic`.
-- - Go outside of proper buffer position for `start_edge` and `end_edge` to
-- correctly spot diagnostic entry right and start and end of buffer.
local cursor_pos = vim.api.nvim_win_get_cursor(0)
iterator.state = { cursor_pos[1] - 1, cursor_pos[2] }
iterator.start_edge = { 0, -1 }
local last_line = vim.api.nvim_buf_line_count(0)
iterator.end_edge = { last_line - 1, vim.fn.col({ last_line, '$' }) - 1 }
-- Iterate
local res_pos = MiniBracketed.advance(iterator, direction, opts)
if res_pos == nil or res_pos == iterator.state then return end
-- Apply. Use built-in jump with offsetted cursor position to make it respect
-- `vim.diagnostic.config()`.
H.diagnostic_jump({ res_pos[1] + 1, res_pos[2] - 1 }, opts.float, opts.severity)
end
--- File on disk
---
--- Go to next/previous file on disk alphabetically. Files are taken from
--- directory of file in current buffer (or current working directory if buffer
--- doesn't contain a readable file). Only first-level files are used, i.e. it
--- doesn't go inside subdirectories.
---
--- Direction "forward" goes forward alphabetically, "backward" - backward.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.file = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'file')
opts = vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().file.options, opts or {})
-- Get file data
local file_data = H.get_file_data()
if file_data == nil then return end
local file_basenames, directory = file_data.file_basenames, file_data.directory
-- Define iterator that traverses all found files
local iterator = {}
local n_files = #file_basenames
iterator.next = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return 1 end
if n_files <= ind then return end
return ind + 1
end
iterator.prev = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return n_files end
if ind <= 1 then return end
return ind - 1
end
-- - Find filename array index of current buffer
local cur_basename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t')
local cur_basename_ind
if cur_basename ~= '' then
for i, f in ipairs(file_basenames) do
if cur_basename == f then
cur_basename_ind = i
break
end
end
end
iterator.state = cur_basename_ind
iterator.start_edge = 0
iterator.end_edge = n_files + 1
-- Iterate
local res_ind = MiniBracketed.advance(iterator, direction, opts)
if res_ind == iterator.state then return end
-- Apply. Open target_path.
local path_sep = package.config:sub(1, 1)
local target_path = directory .. path_sep .. file_basenames[res_ind]
H.edit(target_path)
end
--- Indent change
---
--- Go to next/previous line with different indent (see |indent()|).
--- Can be used to go to lines with smaller, bigger, or different indent.
---
--- Notes:
--- - Directions "first" and "last" work differently from most other targets
--- for performance reasons. They are essentially "backward" and "forward"
--- with very big `n_times` option.
--- - For similar reasons, `wrap` is not supported.
--- - Blank line inherit indent from near non-blank line in direction of movement.
---
--- Direction "forward" increases line number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- __bracketed_add_to_jumplist
--- - <change_type> `(string)` - which type of indent change to use.
--- One of "less" (default; smaller indent), "more" (bigger indent),
--- "diff" (different indent).
MiniBracketed.indent = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'indent')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, change_type = 'less', n_times = vim.v.count1 },
H.get_config().indent.options,
opts or {}
)
opts.wrap = false
if direction == 'first' then
-- For some reason using `n_times = math.huge` leads to infinite loop
direction, opts.n_times = 'backward', vim.api.nvim_buf_line_count(0) + 1
end
if direction == 'last' then
direction, opts.n_times = 'forward', vim.api.nvim_buf_line_count(0) + 1
end
-- Compute loop data to traverse target commented lines in current buffer
local predicate = ({
less = function(new, cur) return new < cur or cur == 0 end,
more = function(new, cur) return new > cur end,
diff = function(new, cur) return new ~= cur end,
})[opts.change_type]
if predicate == nil then return end
-- Define iterator
local iterator = {}
iterator.next = function(cur_lnum)
-- Correctly process empty current line
cur_lnum = vim.fn.nextnonblank(cur_lnum)
local cur_indent = vim.fn.indent(cur_lnum)
local new_lnum, new_indent = cur_lnum, cur_indent
-- Check with `new_lnum > 0` because `nextnonblank()` returns -1 if line is
-- outside of line range
while new_lnum > 0 do
new_indent = vim.fn.indent(new_lnum)
if predicate(new_indent, cur_indent) then return new_lnum end
new_lnum = vim.fn.nextnonblank(new_lnum + 1)
end
end
iterator.prev = function(cur_lnum)
cur_lnum = vim.fn.prevnonblank(cur_lnum)
local cur_indent = vim.fn.indent(cur_lnum)
local new_lnum, new_indent = cur_lnum, cur_indent
while new_lnum > 0 do
new_indent = vim.fn.indent(new_lnum)
if predicate(new_indent, cur_indent) then return new_lnum end
new_lnum = vim.fn.prevnonblank(new_lnum - 1)
end
end
-- - Don't add first and last states as there is no wrapping around edges
iterator.state = vim.fn.line('.')
-- Iterate
local res_line_num = MiniBracketed.advance(iterator, direction, opts)
if res_line_num == nil or res_line_num == iterator.state then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply. Open just enough folds and put cursor on first non-blank.
H.set_cursor(res_line_num, 0)
vim.cmd('normal! zv^')
end
--- Jump inside current buffer
---
--- Go to next/previous jump from |jumplist| which is inside current buffer.
---
--- Notes:
--- - There are no Visual mode mappings due to implementation problems.
---
--- Direction "forward" increases jump number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.jump = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'jump')
opts = vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().jump.options, opts or {})
-- Define iterator that traverses all jumplist entries inside current buffer
local cur_buf_id = vim.api.nvim_get_current_buf()
local jump_list, cur_jump_num = unpack(vim.fn.getjumplist())
local n_list = #jump_list
if n_list == 0 then return end
-- - Correct for zero-based indexing
cur_jump_num = cur_jump_num + 1
local iterator = {}
local is_jump_num_from_current_buffer = function(jump_num)
local jump_entry = jump_list[jump_num]
if jump_entry == nil then return end
return jump_entry.bufnr == cur_buf_id
end
iterator.next = function(jump_num)
for num = jump_num + 1, n_list do
if is_jump_num_from_current_buffer(num) then return num end
end
end
iterator.prev = function(jump_num)
for num = jump_num - 1, 1, -1 do
if is_jump_num_from_current_buffer(num) then return num end
end
end
iterator.state = cur_jump_num
iterator.start_edge = 0
iterator.end_edge = n_list + 1
-- Iterate
local res_jump_num = MiniBracketed.advance(iterator, direction, opts)
if res_jump_num == nil or jump_list[res_jump_num] == nil then return end
-- Apply. Make jump. Allow jumping to current jump entry as it might be
-- different from current cursor position.
H.make_jump(jump_list, cur_jump_num, res_jump_num)
end
--- Location from location list
---
--- Go to next/previous location from |location-list|. This is similar to
--- |:lfirst|, |:lprevious|, |:lnext|, and |:llast| but with support of
--- wrapping around edges and |[count]| for "first"/"last" direction.
---
--- Direction "forward" increases location number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.location = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'location')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().location.options, opts or {})
H.qf_loc_implementation('location', direction, opts)
end
--- Old files from previous and current sessions
---
--- Go to older/newer readable file either from previous session (see |v:oldfiles|)
--- or the current one (updated automatically after |MiniBracketed.setup()| call).
---
--- Direction "forward" goes to more recent files, "backward" - to older.
---
--- Notes:
--- - In current session it tracks only normal buffers (see |'buftype'|) for
--- some readable file.
--- - No new file is tracked when advancing this target. Only after buffer
--- change is done not through this target (like with |MiniBracketed.buffer()|),
--- it updates recency of last advanced and new buffers.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.oldfile = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'oldfile')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().oldfile.options, opts or {})
-- Define iterator that traverses all old files
local cur_path = vim.api.nvim_buf_get_name(0)
H.oldfile_normalize()
local oldfile_arr = H.oldfile_get_array()
local n_oldfiles = #oldfile_arr
local iterator = {}
iterator.next = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return 1 end
if n_oldfiles <= ind then return end
return ind + 1
end
iterator.prev = function(ind)
-- Allow advance in untrackable current buffer
if ind == nil then return n_oldfiles end
if ind <= 1 then return end
return ind - 1
end
iterator.state = H.cache.oldfile.recency[cur_path]
iterator.start_edge = 0
iterator.end_edge = n_oldfiles + 1
-- Iterate
local res_arr_ind = MiniBracketed.advance(iterator, direction, opts)
if res_arr_ind == nil or res_arr_ind == iterator.state then return end
-- Apply. Edit file at path while marking it not for tracking.
H.cache.oldfile.is_advancing = true
H.edit(oldfile_arr[res_arr_ind])
end
--- Quickfix from quickfix list
---
--- Go to next/previous entry from |quickfix| list. This is similar to
--- |:cfirst|, |:cprevious|, |:cnext|, and |:clast| but with support of
--- wrapping around edges and |[count]| for "first"/"last" direction.
---
--- Direction "forward" increases location number, "backward" - decreases.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.quickfix = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'quickfix')
opts =
vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().quickfix.options, opts or {})
H.qf_loc_implementation('quickfix', direction, opts)
end
--- Tree-sitter node
---
--- Go to end/start of current tree-sitter node and its parents (except root).
---
--- Notes:
--- - Requires loaded tree-sitter parser in the current buffer.
--- - Directions "first" and "last" work differently from most other targets
--- for performance reasons. They are essentially "backward" and "forward"
--- with very big `n_times` option.
--- - For similar reasons, `wrap` is not supported.
---
--- Direction "forward" moves cursor forward to node's end, "backward" - backward
--- to node's start.
---
---@param direction __bracketed_direction
---@param opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- __bracketed_add_to_jumplist
MiniBracketed.treesitter = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'treesitter')
opts = vim.tbl_deep_extend(
'force',
{ add_to_jumplist = false, n_times = vim.v.count1 },
H.get_config().treesitter.options,
opts or {}
)
opts.wrap = false
if direction == 'first' then
direction, opts.n_times = 'backward', math.huge
end
if direction == 'last' then
direction, opts.n_times = 'forward', math.huge
end
-- Define iterator that traverses current node and its parents (except root)
local is_bad_node = function(node) return node == nil or node:parent() == nil end
local is_after = function(row_new, col_new, row_ref, col_ref)
return row_ref < row_new or (row_ref == row_new and col_ref < col_new)
end
local is_before = function(row_new, col_new, row_ref, col_ref) return is_after(row_ref, col_ref, row_new, col_new) end
local iterator = {}
-- Traverse node and parents until node's end is after current position
iterator.next = function(node_pos)
local node = node_pos.node
if is_bad_node(node) then return nil end
local init_row, init_col = node_pos.pos[1], node_pos.pos[2]
local cur_row, cur_col, cur_node = init_row, init_col, node
repeat
if is_bad_node(cur_node) then break end
cur_row, cur_col = cur_node:end_()
-- Correct for end-exclusiveness
cur_col = cur_col - 1
cur_node = cur_node:parent()
until is_after(cur_row, cur_col, init_row, init_col)
if not is_after(cur_row, cur_col, init_row, init_col) then return end
return { node = cur_node, pos = { cur_row, cur_col } }
end
-- Traverse node and parents until node's start is before current position
iterator.prev = function(node_pos)
local node = node_pos.node
if is_bad_node(node) then return nil end
local init_row, init_col = node_pos.pos[1], node_pos.pos[2]
local cur_row, cur_col, cur_node = init_row, init_col, node
repeat
if is_bad_node(cur_node) then break end
cur_row, cur_col = cur_node:start()
cur_node = cur_node:parent()
until is_before(cur_row, cur_col, init_row, init_col)
if not is_before(cur_row, cur_col, init_row, init_col) then return end
return { node = cur_node, pos = { cur_row, cur_col } }
end
local cur_pos = vim.api.nvim_win_get_cursor(0)
local ok, node = pcall(H.get_treesitter_node, cur_pos[1] - 1, cur_pos[2])
if not ok then
H.error(
'In `treesitter()` target can not find tree-sitter node under cursor.'
.. ' Do you have tree-sitter enabled in current buffer?'
)
end
iterator.state = { pos = { cur_pos[1] - 1, cur_pos[2] }, node = node }
-- Iterate
local res_node_pos = MiniBracketed.advance(iterator, direction, opts)
if res_node_pos == nil then return end
-- Possibly add current position to jumplist
if opts.add_to_jumplist then H.add_to_jumplist() end
-- Apply
H.set_cursor(res_node_pos.pos[1] + 1, res_node_pos.pos[2])
end
--- Undo along a tracked linear history
---
--- In a nutshell:
--- - Keys |u| and |CTRL-R| (although remapped) can be used as usual, but every
--- their execution new state is recorded in this module's linear undo history.
--- - Advancing this target goes along linear undo history revealing undo states
--- **in order they actually appeared**.
--- - One big difference with built-in methods is that tracked linear history
--- can repeat undo states (not consecutively, though).
---
--- Neovim's default way of managing undo history is through branches (see
--- |undo-branches|). Basically it means that if you undo several changes and then
--- make new ones, it creates new undo branch while usually (see |'undolevels'|)
--- saving previous buffer states in another branch. While there are commands
--- to navigate by time of undo state creation (like |:earlier| and |:later|),
--- there is no intuitive way to cycle through them. Existing |g-| and |g+|
--- cycle through undo states **based on their creation time**, which often
--- gets confusing really guickly in extensively edited buffer.
---
--- This `undo()` target provides a way to cycle through linear undo history
--- **in order states actually appeared**. It does so by registering any new undo
--- states plus every time |MiniBracketed.register_undo_state()| is called. To have
--- more "out of the box" experience, |u| and |CTRL-R| are remapped to call it after
--- they perform their undo/redo.
---
--- Example:
---
--- To show more clearly the difference between advancing this target and using
--- built-in functionality, here is an example:
---
--- - Create undo history in a new buffer (|:new|):
--- - Enter `one two three` text.
--- - Delete first word with `daw` and undo the change with `u`.
--- - Delete second word with `daw` and undo the change with `u`.
--- - Delete third word with `daw` and undo the change with `u`.
---
--- - Now try one of the following (each one after performing previous steps in
--- separate new buffer):
--- - Press `u`. It goes back to empty buffer. Press `<C-R>` twice and it
--- goes to the latest change (`one two`). No way to get to other states
--- (like `two three` or `one three`) with these two keys.
---
--- - Press `g-`. It goes to an empty buffer. Press `g+` 4 times. It cycles
--- through all available undo states **in order they were created**.
---
--- - Finally, press `[u`. It goes back to `one two` - state which was
--- **previously visited** by the user. Another `[u` restores `one two three`.
--- Use `]U` to go to latest visited undo state.
---
---@param direction __bracketed_direction
---@param opts __bracketed_opts
MiniBracketed.undo = function(direction, opts)
if H.is_disabled() then return end
H.validate_direction(direction, { 'first', 'backward', 'forward', 'last' }, 'undo')
opts = vim.tbl_deep_extend('force', { n_times = vim.v.count1, wrap = true }, H.get_config().undo.options, opts or {})
-- Define iterator that traverses undo states in order they appeared
local buf_id = vim.api.nvim_get_current_buf()
H.undo_sync(buf_id, vim.fn.undotree())
local iterator = {}
local buf_history = H.cache.undo[buf_id]
local n = #buf_history
iterator.next = function(id)
if id == nil or n <= id then return end
return id + 1
end
iterator.prev = function(id)
if id == nil or id <= 1 then return end
return id - 1
end
iterator.state = buf_history.current_id
iterator.start_edge = 0
iterator.end_edge = n + 1
-- Iterate
local res_id = MiniBracketed.advance(iterator, direction, opts)
if res_id == nil or res_id == iterator.state then return end
-- Apply. Move to undo state by number while recording current history id
buf_history.is_advancing = true
vim.cmd('undo ' .. buf_history[res_id])
buf_history.current_id = res_id
end
--- Register state for undo target
---
--- Use this function to add current undo state to this module's linear undo
--- history. It is used in |MiniBracketed.setup()| to remap |u| and |CTRL-R| keys
--- to add their new state to linear undo history.
MiniBracketed.register_undo_state = function()
local buf_id = vim.api.nvim_get_current_buf()
local tree = vim.fn.undotree()
-- Synchronize undo history and stop advancing
H.undo_sync(buf_id, tree, false)
0