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

Skip to content

Latest commit

 

History

History
2017 lines (1704 loc) · 79.7 KB

bracketed.lua

File metadata and controls

2017 lines (1704 loc) · 79.7 KB
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]
vim.cmd('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 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
vim.cmd('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 |get_node_at_pos()| from |lua-treesitter| (present in Neovim=0.8)
--- or |vim.treesitter.get_node()| (present in Neovim>=0.9) along with loaded
--- tree-sitter parser in 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 |<C-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 |<C-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
0