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

Skip to content

Latest commit

 

History

History
1170 lines (1022 loc) · 43 KB

jump2d.lua

File metadata and controls

1170 lines (1022 loc) · 43 KB
--- *mini.jump2d* Jump within visible lines
--- *MiniJump2d*
---
--- MIT License Copyright (c) 2022 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Jump within visible lines via iterative label filtering.
---
--- Features:
--- - Make jump by iterative filtering of possible, equally considered jump
--- spots until there is only one. Filtering is done by typing a label
--- character that is visualized at jump spot.
---
--- - Customizable (see |MiniJump2d.config|):
--- - Way of computing possible jump spots with opinionated default.
--- - Characters used to label jump spots during iterative filtering.
--- - Visual effects: how many steps ahead to show; dim lines with spots.
--- - Action hooks to be executed at certain events during jump.
--- - Allowed windows: current and/or not current.
--- - Allowed lines: whether to process blank or folded lines, lines
--- before/at/after cursor line, etc. Example: user can configure to look
--- for spots only inside current window at or after cursor line.
--- Example: user can configure to look for word starts only inside current
--- window at or after cursor line with 'j' and 'k' labels performing some
--- action after jump.
---
--- - Works in Visual and Operator-pending (with dot-repeat) modes.
---
--- - Preconfigured ways of computing jump spots (see |MiniJump2d.builtin_opts|):
--- - Starts of lines.
--- - Starts of words.
--- - Single character from user input.
--- - Variable length query from user input.
---
--- - Works with multibyte characters.
---
--- General overview of how jump is intended to be performed:
--- - Lock eyes on desired location ("spot") recognizable by future jump.
--- Should be within visible lines at place where cursor can be placed.
---
--- - Initiate jump. Either by custom keybinding or with a call to
--- |MiniJump2d.start()| (allows customization options). This will highlight
--- all possible jump spots with their labels (letters from "a" to "z" by
--- default). For more details, read |MiniJump2d.start()| and |MiniJump2d.config|.
---
--- - Type character that appeared over desired location. If its label was
--- unique, jump is performed. If it wasn't unique, possible jump spots are
--- filtered to those having the same label character.
---
--- - Repeat previous step until there is only one possible jump spot or type <CR>
--- to jump to first available jump spot. Typing anything else stops jumping
--- without moving cursor.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.jump2d').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table
--- `MiniJump2d` which you can use for scripting or manually (with
--- `:lua MiniJump2d.*`).
---
--- See |MiniJump2d.config| for available config settings.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minijump2d_config` which should have same structure as
--- `MiniJump2d.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Example usage ~
---
--- - Modify default jumping to use only current window at or after cursor line: >lua
---
--- require('mini.jump2d').setup({
--- allowed_lines = { cursor_before = false },
--- allowed_windows = { not_current = false },
--- })
--- <
--- - Jump to word start using combination of options supplied in
--- |MiniJump2d.config| and |MiniJump2d.builtin_opts.line_start|: >vim
---
--- :lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)
--- <
--- - Jump to a single character typed after executing this command: >vim
---
--- :lua MiniJump2d.start(MiniJump2d.builtin_opts.single_character)
--- <
--- - See more examples in |MiniJump2d.start| and |MiniJump2d.builtin_opts|.
---
--- # Comparisons ~
---
--- - 'phaazon/hop.nvim':
--- - Both are fast, customizable, and extensible (user can write their own
--- ways to define jump spots).
--- - 'hop.nvim' visualizes all steps at once. While this module can show
--- configurable number of steps ahead.
--- - Both have several builtin ways to specify type of jump (word start,
--- line start, one character or query based on user input). 'hop.nvim'
--- does that by exporting many targeted Neovim commands, while this
--- module has preconfigured basic options leaving others to
--- customization with Lua code (see |MiniJump2d.builtin_opts|).
--- - 'hop.nvim' computes labels (called "hints") differently. Contrary to
--- this module deliberately not having preference of one jump spot over
--- another, 'hop.nvim' uses specialized algorithm that produces sequence
--- of keys in a slightly biased manner: some sequences are intentionally
--- shorter than the others (leading to fewer average keystrokes). They
--- are put near cursor (by default) and highlighted differently. Final
--- order of sequences is based on distance to the cursor.
--- - 'mini.jump2d' has opinionated default algorithm of computing jump
--- spots. See |MiniJump2d.default_spotter|.
---
--- # Highlight groups ~
---
--- * `MiniJump2dSpot` - highlighting of jump spot's next step. By default it
--- uses label with highest contrast while not being too visually demanding:
--- white on black for dark 'background', black on white for light. If it
--- doesn't suit your liking, try couple of these alternatives (or choose
--- your own, of course): >lua
---
--- -- Reverse underlying colors (mostly *very* visible in any colorscheme)
--- vim.api.nvim_set_hl(0, 'MiniJump2dSpot', { reverse = true })
---
--- -- Bold italic
--- vim.api.nvim_set_hl(0, 'MiniJump2dSpot', { bold = true, italic = true })
---
--- -- Red undercurl
--- vim.api.nvim_set_hl(0, 'MiniJump2dSpot', { sp = 'Red', undercurl = true })
--- <
--- * `MiniJump2dSpotUnique` - highlighting of jump spot's next step if it has
--- unique label. By default links to `MiniJump2dSpot`.
---
--- * `MiniJump2dSpotAhead` - highlighting of jump spot's future steps. By default
--- similar to `MiniJump2dSpot` but with less contrast and visibility.
---
--- * `MiniJump2dDim` - highlighting of lines with at least one jump spot.
--- Make it non-bright in order for jump spot labels to be more visible.
--- By default linked to `Comment` highlight group.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minijump2d_disable` (globally) or
--- `vim.b.minijump2d_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.
-- Module definition ==========================================================
local MiniJump2d = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniJump2d.config|.
---
---@usage >lua
--- require('mini.jump2d').setup() -- use default config
--- -- OR
--- require('mini.jump2d').setup({}) -- replace {} with your config table
--- <
MiniJump2d.setup = function(config)
-- Export module
_G.MiniJump2d = MiniJump2d
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Options ~
---
--- ## Spotter function ~
---
--- Actual computation of possible jump spots is done through spotter function.
--- It should have the following arguments:
--- - `line_num` is a line number inside buffer.
--- - `args` - table with additional arguments:
--- - {win_id} - identifier of a window where input line number is from.
--- - {win_id_init} - identifier of a window which was current when
--- `MiniJump2d.start()` was called.
---
--- Its output is a list of byte-indexed positions that should be considered as
--- possible jump spots for this particular line in this particular window.
--- Note: for a more aligned visualization this list should be (but not
--- strictly necessary) sorted increasingly.
---
--- Note: spotter function is always called with `win_id` window being
--- "temporary current" (see |nvim_win_call|). This allows using builtin
--- Vimscript functions that operate only inside current window.
---
--- ## View ~
---
--- Option `view.n_steps_ahead` controls how many steps ahead to show along
--- with the currently required label. Those future steps are showed with
--- different (less visible) highlight group ("MiniJump2dSpotAhead"). Usually
--- it is a good idea to use this with a spotter which doesn't result into many
--- jump spots (like, for example, |MiniJump2d.builtin_opts.word_start|).
--- Default is 0 to not show anything ahead as it reduces visual noise.
---
--- Option `view.dim` controls whether to dim lines with at least one jump spot.
--- Dimming is done by applying "MiniJump2dDim" highlight group to the whole line.
---
--- ## Allowed lines ~
---
--- Option `allowed_lines` controls which lines will be used for computing
--- possible jump spots:
--- - If `blank` or `fold` is `true`, it is possible to jump to first column of blank
--- line (determined by |prevnonblank|) or first folded one (determined by
--- |foldclosed|) respectively. Otherwise they are skipped. These lines are
--- not processed by spotter function even if the option is `true`.
--- - If `cursor_before`, (`cursor_at`, `cursor_after`) is `true`, lines before
--- (at, after) cursor line of all processed windows are forwarded to spotter
--- function. Otherwise, they don't. This allows control of jump "direction".
---
--- ## Hooks ~
---
--- Following hook functions can be used to further tweak jumping experience:
--- - `before_start` - called without arguments first thing when jump starts.
--- One of the possible use cases is to ask for user input and update spotter
--- function with it.
--- - `after_jump` - called after jump was actually done. Useful to make
--- post-adjustments (like move cursor to first non-whitespace character).
MiniJump2d.config = {
-- Function producing jump spots (byte indexed) for a particular line.
-- For more information see |MiniJump2d.start|.
-- If `nil` (default) - use |MiniJump2d.default_spotter|
spotter = nil,
-- Characters used for labels of jump spots (in supplied order)
labels = 'abcdefghijklmnopqrstuvwxyz',
-- Options for visual effects
view = {
-- Whether to dim lines with at least one jump spot
dim = false,
-- How many steps ahead to show. Set to big number to show all steps.
n_steps_ahead = 0,
},
-- Which lines are used for computing spots
allowed_lines = {
blank = true, -- Blank line (not sent to spotter even if `true`)
cursor_before = true, -- Lines before cursor line
cursor_at = true, -- Cursor line
cursor_after = true, -- Lines after cursor line
fold = true, -- Start of fold (not sent to spotter even if `true`)
},
-- Which windows from current tabpage are used for visible lines
allowed_windows = {
current = true,
not_current = true,
},
-- Functions to be executed at certain events
hooks = {
before_start = nil, -- Before jump start
after_jump = nil, -- After jump was actually done
},
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
start_jumping = '<CR>',
},
-- 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 =======================================================
--- Start jumping
---
--- Compute possible jump spots, visualize them and wait for iterative filtering.
---
--- First computation of possible jump spots ~
---
--- - Process allowed windows (current and/or not current; controlled by
--- `allowed_windows` option) by visible lines from top to bottom. For each
--- one see if it is allowed (controlled by `allowed_lines` option). If not
--- allowed, then do nothing. If allowed and should be processed by
--- `spotter`, process it.
--- - Apply spotter function from `spotter` option for each appropriate line
--- and concatenate outputs. This means that eventual order of jump spots
--- aligns with lexicographical order within "window id" - "line number" -
--- "position in `spotter` output" tuples.
--- - For each possible jump compute its label: a single character from
--- `labels` option used to filter jump spots. Each possible label character
--- might be used more than once to label several "consecutive" jump spots.
--- It is done in an optimal way under assumption of no preference of one
--- spot over another. Basically, it means "use all labels at each step of
--- iterative filtering as equally as possible".
---
--- Visualization ~
---
--- Current label for each possible jump spot is shown at that position
--- overriding everything underneath it.
---
--- Iterative filtering ~
---
--- Labels of possible jump spots are computed in order to use them as equally
--- as possible.
---
--- Example:
--- - With `abc` as `labels` option, initial labels for 10 possible jumps
--- are "aaaabbbccc". As there are 10 spots which should be "coded" with 3
--- symbols, at least 2 symbols need 3 steps to filter them out. With current
--- implementation those are always the "first ones".
--- - After typing `a`, it filters first four jump spots and recomputes its
--- labels to be "aabc".
--- - After typing `a` again, it filters first two spots and recomputes its
--- labels to be "ab".
--- - After typing either `a` or `b` it filters single spot and makes jump.
---
--- With default 26 labels for most real-world cases 2 steps is enough for
--- default spotter function. Rarely 3 steps are needed with several windows.
---
---@param opts table|nil Configuration of jumping, overriding global and buffer
--- local values.config|. Has the same structure as |MiniJump2d.config|
--- without <mappings> field. Extra allowed fields:
--- - <hl_group> - which highlight group to use for first step.
--- Default: "MiniJump2dSpot".
--- - <hl_group_ahead> - which highlight group to use for second step and later.
--- Default: "MiniJump2dSpotAhead".
--- - <hl_group_dim> - which highlight group to use dimming used lines.
--- Default: "MiniJump2dSpotDim".
---
---@usage >lua
--- -- Start default jumping
--- MiniJump2d.start()
---
--- -- Jump to word start
--- MiniJump2d.start(MiniJump2d.builtin_opts.word_start)
---
--- -- Jump to single character from user input (follow by typing one character)
--- MiniJump2d.start(MiniJump2d.builtin_opts.single_character)
---
--- -- Jump to first character of punctuation group only inside current window
--- -- which is placed at cursor line; visualize with `Search`
--- MiniJump2d.start({
--- spotter = MiniJump2d.gen_pattern_spotter('%p+'),
--- allowed_lines = { cursor_before = false, cursor_after = false },
--- allowed_windows = { not_current = false },
--- hl_group = 'Search'
--- })
---<
---@seealso |MiniJump2d.config|
MiniJump2d.start = function(opts)
if H.is_disabled() then return end
opts = opts or {}
-- Apply `before_start` before `tbl_deep_extend` to allow it modify options
-- inside it (notably `spotter`). Example: `builtins.single_character`.
local before_start = (opts.hooks or {}).before_start
or ((vim.b.minijump2d_config or {}).hooks or {}).before_start
or MiniJump2d.config.hooks.before_start
if before_start ~= nil then before_start() end
opts = H.get_config(opts)
opts.spotter = opts.spotter or MiniJump2d.default_spotter
opts.hl_group = opts.hl_group or 'MiniJump2dSpot'
opts.hl_group_ahead = opts.hl_group_ahead or 'MiniJump2dSpotAhead'
opts.hl_group_unique = opts.hl_group_unique or 'MiniJump2dSpotUnique'
opts.hl_group_dim = opts.hl_group_dim or 'MiniJump2dDim'
local spots = H.spots_compute(opts)
if #spots == 0 then
H.message('No spots to show.')
return
end
if #spots == 1 then
H.perform_jump(spots[1], opts.hooks.after_jump)
return
end
local label_tbl = vim.split(opts.labels, '')
spots = H.spots_add_steps(spots, label_tbl, opts.view.n_steps_ahead)
H.spots_show(spots, opts)
H.cache.spots = spots
H.advance_jump(opts)
end
--- Stop jumping
MiniJump2d.stop = function()
H.spots_unshow()
H.cache.spots = nil
H.cache.msg_shown = false
vim.cmd('redraw')
if H.cache.is_in_getcharstr then vim.api.nvim_input('<C-c>') end
end
--- Generate spotter for Lua pattern
---
---@param pattern string|nil Lua pattern. Default: `'[^%s%p]+'` which matches group
--- of "non-whitespace non-punctuation characters" (basically a way of saying
--- "group of alphanumeric characters" that works with multibyte characters).
---@param side string|nil Which side of pattern match should be considered as
--- jumping spot. Should be one of 'start' (start of match, default), 'end'
--- (inclusive end of match), or 'none' (match for spot is done manually
--- inside pattern with plain `()` matching group).
---
---@return function Spotter function.
---
---@usage >lua
--- -- Match any punctuation
--- MiniJump2d.gen_pattern_spotter('%p')
---
--- -- Match first from line start non-whitespace character
--- MiniJump2d.gen_pattern_spotter('^%s*%S', 'end')
---
--- -- Match start of last word
--- MiniJump2d.gen_pattern_spotter('[^%s%p]+[%s%p]-$', 'start')
---
--- -- Match letter followed by another letter (example of manual matching
--- -- inside pattern)
--- MiniJump2d.gen_pattern_spotter('%a()%a', 'none')
--- <
MiniJump2d.gen_pattern_spotter = function(pattern, side)
-- Don't use `%w` to account for multibyte characters
pattern = pattern or '[^%s%p]+'
side = side or 'start'
-- Process anchored patterns separately because:
-- - `gmatch()` doesn't work if pattern start with `^`.
-- - Manual adding of `()` will conflict with anchors.
local is_anchored = pattern:sub(1, 1) == '^' or pattern:sub(-1, -1) == '$'
if is_anchored then
return function(line_num, args)
local line = vim.fn.getline(line_num)
local s, e, m = line:find(pattern)
return { ({ ['start'] = s, ['end'] = e, ['none'] = m })[side] }
end
end
-- Handle `side = 'end'` later by appending length of match to match start.
-- This, unlike appending `()` to end of pattern, makes output spot to be
-- inside matched pattern and on its exact right.
-- Having `(%s)` for `side = 'none'` is for compatibility with later `gmatch`
local pattern_template = side == 'none' and '(%s)' or '(()%s)'
pattern = pattern_template:format(pattern)
return function(line_num, args)
local line = vim.fn.getline(line_num)
local res = {}
-- NOTE: maybe a more straightforward approach would be a series of
-- `line:find(original_pattern, init)` with moving `init`, but it has some
-- weird behavior with quantifiers.
-- For example: `string.find(' --', '%s*', 4)` returns `4 3`.
for whole, spot in string.gmatch(line, pattern) do
-- Possibly correct spot to be index of last matched position
if side == 'end' then spot = spot + math.max(whole:len() - 1, 0) end
-- Ensure that index is strictly within line length (which can be not
-- true in case of weird pattern, like when using frontier `%f[%W]`)
spot = math.min(math.max(spot, 0), line:len())
-- Unify how spot is chosen in case of multibyte characters
-- Use `+-1` to make sure that result it at start of multibyte character
local utf_index = vim.str_utfindex(line, spot) - 1
spot = vim.str_byteindex(line, utf_index) + 1
-- Add spot only if it referces new actually visible column
if spot ~= res[#res] then table.insert(res, spot) end
end
return res
end
end
--- Generate union of spotters
---
---@param ... any Each argument should be a valid spotter.
--- See |MiniJump2d.config| for more details.
---
---@return function Spotter producing union of spots.
---
---@usage >lua
--- -- Match start and end of non-blank character groups:
--- local nonblank_start = MiniJump2d.gen_pattern_spotter('%S+', 'start')
--- local nonblank_end = MiniJump2d.gen_pattern_spotter('%S+', 'end')
--- local spotter = MiniJump2d.gen_union_spotter(nonblank_start, nonblank_end)
--- <
MiniJump2d.gen_union_spotter = function(...)
local spotters = { ... }
if #spotters == 0 then return function() return {} end end
local is_all_callable = true
for _, x in ipairs(spotters) do
if not vim.is_callable(x) then is_all_callable = false end
end
if not is_all_callable then H.error('All `gen_union_spotter()` arguments should be callable elements.') end
return function(line_num, args)
local res = spotters[1](line_num, args)
for i = 2, #spotters do
res = H.merge_unique(res, spotters[i](line_num, args))
end
return res
end
end
--- Default spotter function
---
--- Spot is possible for jump if it is one of the following:
--- - Start or end of non-whitespace character group.
--- - Alphanumeric character followed or preceded by punctuation (useful for
--- snake case names).
--- - Start of uppercase character group (useful for camel case names). Usually
--- only Latin alphabet is recognized due to Lua patterns shortcomings.
---
--- These rules are derived in an attempt to balance between two intentions:
--- - Allow as much useful jumping spots as possible.
--- - Make labeled jump spots easily distinguishable.
---
--- Usually takes from 2 to 3 keystrokes to get to destination.
MiniJump2d.default_spotter = (function()
-- NOTE: not using `MiniJump2d.gen_union_spotter()` due to slightly better
-- algorithmic complexity merging small arrays first.
local nonblank_start = MiniJump2d.gen_pattern_spotter('%S+', 'start')
local nonblank_end = MiniJump2d.gen_pattern_spotter('%S+', 'end')
-- Use `[^%s%p]` as "alphanumeric" to allow working with multibyte characters
local alphanum_before_punct = MiniJump2d.gen_pattern_spotter('[^%s%p]%p', 'start')
local alphanum_after_punct = MiniJump2d.gen_pattern_spotter('%p[^%s%p]', 'end')
-- NOTE: works only with Latin alphabet
local upper_start = MiniJump2d.gen_pattern_spotter('%u+', 'start')
return function(line_num, args)
local res_1 = H.merge_unique(nonblank_start(line_num, args), nonblank_end(line_num, args))
local res_2 = H.merge_unique(alphanum_before_punct(line_num, args), alphanum_after_punct(line_num, args))
local res = H.merge_unique(res_1, res_2)
return H.merge_unique(res, upper_start(line_num, args))
end
end)()
--- Table with builtin `opts` values for |MiniJump2d.start()|
---
--- Each element of table is itself a table defining one or several options for
--- `MiniJump2d.start()`. Read help description to see which options it defines
--- (like in |MiniJump2d.builtin_opts.line_start|).
---
---@usage >lua
--- -- Using `MiniJump2d.builtin_opts.line_start` as example:
--- -- Command
--- :lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)
---
--- -- Custom mapping
--- vim.keymap.set(
--- 'n', '<CR>',
--- '<Cmd>lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)<CR>'
--- )
---
--- -- Inside `MiniJump2d.setup()` (make sure to use all defined options)
--- local jump2d = require('mini.jump2d')
--- local jump_line_start = jump2d.builtin_opts.line_start
--- jump2d.setup({
--- spotter = jump_line_start.spotter,
--- hooks = { after_jump = jump_line_start.hooks.after_jump }
--- })
--- <
MiniJump2d.builtin_opts = {}
--- Jump with |MiniJump2d.default_spotter()|
---
--- Defines `spotter`.
MiniJump2d.builtin_opts.default = { spotter = MiniJump2d.default_spotter }
--- Jump to line start
---
--- Defines `spotter` and `hooks.after_jump`.
MiniJump2d.builtin_opts.line_start = {
spotter = function(line_num, args) return { 1 } end,
hooks = {
after_jump = function()
-- Move to first non-blank character
vim.cmd('normal! ^')
end,
},
}
--- Jump to word start
---
--- Defines `spotter`.
MiniJump2d.builtin_opts.word_start = { spotter = MiniJump2d.gen_pattern_spotter('[^%s%p]+') }
-- Produce `opts` which modifies spotter based on user input
local function user_input_opts(input_fun)
local res = {
spotter = function() return {} end,
allowed_lines = { blank = false, fold = false },
}
res.hooks = {
before_start = function()
local input = input_fun()
if input == nil then
res.spotter = function() return {} end
else
local pattern = vim.pesc(input)
res.spotter = MiniJump2d.gen_pattern_spotter(pattern)
end
end,
}
return res
end
--- Jump to single character taken from user input
---
--- Defines `spotter`, `allowed_lines.blank`, `allowed_lines.fold`, and
--- `hooks.before_start`.
MiniJump2d.builtin_opts.single_character = user_input_opts(
function() return H.getcharstr('Enter single character to search') end
)
--- Jump to query taken from user input
---
--- Defines `spotter`, `allowed_lines.blank`, `allowed_lines.fold`, and
--- `hooks.before_start`.
MiniJump2d.builtin_opts.query = user_input_opts(function() return H.input('Enter query to search') end)
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniJump2d.config)
-- Namespaces to be used within module
H.ns_id = {
dim = vim.api.nvim_create_namespace('MiniJump2dDim'),
spots = vim.api.nvim_create_namespace('MiniJump2dSpots'),
input = vim.api.nvim_create_namespace('MiniJump2dInput'),
}
-- Table with current relevant data:
H.cache = {
-- Array of shown spots
spots = nil,
-- Indicator of whether Neovim is currently in "getcharstr" mode
is_in_getcharstr = false,
-- Whether helper message was shown
msg_shown = false,
}
-- Table with special keys
H.keys = {
esc = vim.api.nvim_replace_termcodes('<Esc>', true, true, true),
cr = vim.api.nvim_replace_termcodes('<CR>', true, true, true),
block_operator_pending = vim.api.nvim_replace_termcodes('no<C-V>', true, true, true),
}
-- 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 {})
vim.validate({
spotter = { config.spotter, 'function', true },
labels = { config.labels, 'string' },
view = { config.view, 'table' },
allowed_lines = { config.allowed_lines, 'table' },
allowed_windows = { config.allowed_windows, 'table' },
hooks = { config.hooks, 'table' },
mappings = { config.mappings, 'table' },
silent = { config.silent, 'boolean' },
})
vim.validate({
['view.dim'] = { config.view.dim, 'boolean' },
['view.n_steps_ahead'] = { config.view.n_steps_ahead, 'number' },
['allowed_lines.blank'] = { config.allowed_lines.blank, 'boolean' },
['allowed_lines.cursor_before'] = { config.allowed_lines.cursor_before, 'boolean' },
['allowed_lines.cursor_at'] = { config.allowed_lines.cursor_at, 'boolean' },
['allowed_lines.cursor_after'] = { config.allowed_lines.cursor_after, 'boolean' },
['allowed_lines.fold'] = { config.allowed_lines.fold, 'boolean' },
['allowed_windows.current'] = { config.allowed_windows.current, 'boolean' },
['allowed_windows.not_current'] = { config.allowed_windows.not_current, 'boolean' },
['hooks.before_start'] = { config.hooks.before_start, 'function', true },
['hooks.after_jump'] = { config.hooks.after_jump, 'function', true },
['mappings.start_jumping'] = { config.mappings.start_jumping, 'string' },
})
return config
end
H.apply_config = function(config)
MiniJump2d.config = config
-- Apply mappings
local keymap = config.mappings.start_jumping
H.map('n', keymap, MiniJump2d.start, { desc = 'Start 2d jumping' })
H.map('x', keymap, MiniJump2d.start, { desc = 'Start 2d jumping' })
-- Use `<Cmd>...<CR>` to have proper dot-repeat
-- See https://github.com/neovim/neovim/issues/23406
-- TODO: use local functions if/when that issue is resolved
H.map('o', keymap, '<Cmd>lua MiniJump2d.start()<CR>', { desc = 'Start 2d jumping' })
end
H.create_autocommands = function(config)
local gr = vim.api.nvim_create_augroup('MiniJump2d', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { pattern = pattern, group = gr, callback = callback, desc = desc })
end
-- Corrections for default `<CR>` mapping to not interfere with popular usages
if config.mappings.start_jumping == '<CR>' then
local revert_cr = function() vim.keymap.set('n', '<CR>', '<CR>', { buffer = true }) end
au('FileType', 'qf', revert_cr, 'Revert <CR>')
au('CmdwinEnter', '*', revert_cr, 'Revert <CR>')
end
au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
end
--stylua: ignore
H.create_default_hl = function()
local set_default_hl = function(name, data)
data.default = true
vim.api.nvim_set_hl(0, name, data)
end
local is_light_bg = vim.o.background == 'light'
local bg_color = is_light_bg and 'white' or 'black'
local fg_color = is_light_bg and 'black' or 'white'
set_default_hl('MiniJump2dSpot', { fg = fg_color, bg = bg_color, bold = true, nocombine = true })
set_default_hl('MiniJump2dSpotAhead', { fg = 'grey', bg = bg_color, nocombine = true })
set_default_hl('MiniJump2dSpotUnique', { link = 'MiniJump2dSpot' })
set_default_hl('MiniJump2dDim', { link = 'Comment' })
end
H.is_disabled = function() return vim.g.minijump2d_disable == true or vim.b.minijump2d_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniJump2d.config, vim.b.minijump2d_config or {}, config or {})
end
-- Jump spots -----------------------------------------------------------------
H.spots_compute = function(opts)
local win_id_init = vim.api.nvim_get_current_win()
local win_id_arr = vim.tbl_filter(function(win_id)
if win_id == win_id_init then return opts.allowed_windows.current end
return opts.allowed_windows.not_current
end, H.tabpage_list_wins(0))
local res = {}
for _, win_id in ipairs(win_id_arr) do
vim.api.nvim_win_call(win_id, function()
local cursor_pos = vim.api.nvim_win_get_cursor(win_id)
local spotter_args = { win_id = win_id, win_id_init = win_id_init }
local buf_id = vim.api.nvim_win_get_buf(win_id)
-- Use all currently visible lines
for i = vim.fn.line('w0'), vim.fn.line('w$') do
local columns = H.spot_find_in_line(i, spotter_args, opts, cursor_pos)
-- Use all returned columns for particular line
for _, col in ipairs(columns) do
table.insert(res, { line = i, column = col, buf_id = buf_id, win_id = win_id })
end
end
end)
end
return res
end
H.spots_add_steps = function(spots, label_tbl, n_steps_ahead)
-- Compute all required steps
local steps = {}
for _ = 1, #spots do
table.insert(steps, {})
end
H.populate_spot_steps(steps, label_tbl, 1, n_steps_ahead + 1)
for i, spot in ipairs(spots) do
spot.steps = steps[i]
end
return spots
end
---@param spot_steps_arr table Array of step arrays. Single step array consists
--- from labels user needs to press in order to filter out the spot. Example:
--- { { 'a', 'a' }, { 'a', 'b' }, { 'b' } }
---
---@return nil Modifies `spot_steps_arr` in place.
---@private
H.populate_spot_steps = function(spot_steps_arr, label_tbl, cur_step, max_step)
local n_spots, n_label_chars = #spot_steps_arr, #label_tbl
if n_spots <= 1 or max_step < cur_step then return end
-- Adding labels for specific step is done by distributing all available
-- labels as equally as possible by repeating labels in their order.
-- Example: with 3 label characters labels should evolve with progressing
-- number of spots like this: 'a', 'ab', 'abc', 'aabc', 'aabbc', 'aabbcc',
-- 'aaabbcc', 'aaabbbcc', 'aaabbbccc', etc.
local base, extra = math.floor(n_spots / n_label_chars), n_spots % n_label_chars
-- `cur_label_spot_steps` is an array of spot steps which are expanded with
-- the same label. It is used to initiate computing all steps needed.
local label_id, cur_label_spot_steps = 1, {}
local label_max_count = base + (label_id <= extra and 1 or 0)
for _, spot_steps in ipairs(spot_steps_arr) do
table.insert(spot_steps, label_tbl[label_id])
table.insert(cur_label_spot_steps, spot_steps)
if #cur_label_spot_steps >= label_max_count then
H.populate_spot_steps(cur_label_spot_steps, label_tbl, cur_step + 1, max_step)
label_id, cur_label_spot_steps = label_id + 1, {}
label_max_count = base + (label_id <= extra and 1 or 0)
end
end
end
H.spots_show = function(spots, opts)
spots = spots or H.cache.spots or {}
local set_extmark = vim.api.nvim_buf_set_extmark
-- Add extmark with proper virtual text to jump spots
local dim_buf_lines = {}
for _, extmark in ipairs(H.spots_to_extmarks(spots, opts)) do
local extmark_opts = {
hl_mode = 'combine',
-- Use very high priority
priority = 1000,
virt_text = extmark.virt_text,
virt_text_pos = 'overlay',
}
local buf_id, line = extmark.buf_id, extmark.line
pcall(set_extmark, buf_id, H.ns_id.spots, line, extmark.col, extmark_opts)
-- Register lines to dim
local lines = dim_buf_lines[buf_id] or {}
lines[line] = true
dim_buf_lines[buf_id] = lines
end
-- Possibly dim used lines
if opts.view.dim then
local extmark_opts = { end_col = 0, hl_eol = true, hl_group = opts.hl_group_dim, priority = 999 }
for buf_id, lines in pairs(dim_buf_lines) do
for _, l_num in ipairs(vim.tbl_keys(lines)) do
extmark_opts.end_line = l_num + 1
pcall(set_extmark, buf_id, H.ns_id.dim, l_num, 0, extmark_opts)
end
end
end
-- Redraw to force showing marks
vim.cmd('redraw')
end
H.spots_unshow = function(spots)
spots = spots or H.cache.spots or {}
-- Remove spot extmarks from all buffers they are present
local buf_ids = {}
for _, s in ipairs(spots) do
buf_ids[s.buf_id] = true
end
for _, buf_id in ipairs(vim.tbl_keys(buf_ids)) do
pcall(vim.api.nvim_buf_clear_namespace, buf_id, H.ns_id.spots, 0, -1)
pcall(vim.api.nvim_buf_clear_namespace, buf_id, H.ns_id.dim, 0, -1)
end
end
--- Convert consecutive spot labels into single extmark
---
--- This considerably increases performance in case of many spots.
---@private
H.spots_to_extmarks = function(spots, opts)
if #spots == 0 then return {} end
local hl_group, hl_group_ahead, hl_group_unique = opts.hl_group, opts.hl_group_ahead, opts.hl_group_unique
-- Compute counts for first step in order to distinguish which highlight
-- group to use: `hl_group` or `hl_group_unique`
local first_step_counts = {}
for _, s in ipairs(spots) do
local cur_first_step = s.steps[1]
local cur_count = first_step_counts[cur_first_step] or 0
first_step_counts[cur_first_step] = cur_count + 1
end
-- Define how steps for single spot are added to virtual text
local append_to_virt_text = function(virt_text_arr, steps, n_steps_to_show)
-- Use special group if current first step is unique
local first_hl_group = first_step_counts[steps[1]] == 1 and hl_group_unique or hl_group
table.insert(virt_text_arr, { steps[1], first_hl_group })
-- Add ahead steps only if they are present
local ahead_label = table.concat(steps):sub(2, n_steps_to_show)
if ahead_label ~= '' then table.insert(virt_text_arr, { ahead_label, hl_group_ahead }) end
end
-- Convert all spots to array of extmarks
local res = {}
local buf_id, line, col, virt_text = spots[1].buf_id, spots[1].line - 1, spots[1].column - 1, {}
for i = 1, #spots - 1 do
local cur_spot, next_spot = spots[i], spots[i + 1]
local n_steps = #cur_spot.steps
-- Find which spot steps can be shown
local is_in_same_line = cur_spot.buf_id == next_spot.buf_id and cur_spot.line == next_spot.line
local max_allowed_steps = is_in_same_line and (next_spot.column - cur_spot.column) or math.huge
local n_steps_to_show = math.min(n_steps, max_allowed_steps)
-- Add text for shown steps
append_to_virt_text(virt_text, cur_spot.steps, n_steps_to_show)
-- Finish creating extmark if next spot is far enough
local next_is_close = is_in_same_line and n_steps == max_allowed_steps
if not next_is_close then
table.insert(res, { buf_id = buf_id, line = line, col = col, virt_text = virt_text })
buf_id, line, col, virt_text = next_spot.buf_id, next_spot.line - 1, next_spot.column - 1, {}
end
end
local last_steps = spots[#spots].steps
append_to_virt_text(virt_text, last_steps, #last_steps)
table.insert(res, { buf_id = buf_id, line = line, col = col, virt_text = virt_text })
return res
end
H.spot_find_in_line = function(line_num, spotter_args, opts, cursor_pos)
local allowed = opts.allowed_lines
-- Adjust for cursor line
local cur_line = cursor_pos[1]
if
(not allowed.cursor_before and line_num < cur_line)
or (not allowed.cursor_at and line_num == cur_line)
or (not allowed.cursor_after and line_num > cur_line)
then
return {}
end
-- Process folds
local fold_indicator = vim.fn.foldclosed(line_num)
if fold_indicator ~= -1 then return (allowed.fold and fold_indicator == line_num) and { 1 } or {} end
-- Process blank lines
if vim.fn.prevnonblank(line_num) ~= line_num then return allowed.blank and { 1 } or {} end
-- Finally apply spotter
return opts.spotter(line_num, spotter_args)
end
-- Jump state -----------------------------------------------------------------
H.advance_jump = function(opts)
local label_tbl = vim.split(opts.labels, '')
local spots = H.cache.spots
local n_steps_ahead = opts.view.n_steps_ahead
if type(spots) ~= 'table' or #spots < 1 then
H.spots_unshow(spots)
H.cache.spots = nil
return
end
local key = H.getcharstr('Enter encoding symbol to advance jump')
if vim.tbl_contains(label_tbl, key) then
H.spots_unshow(spots)
spots = vim.tbl_filter(function(x) return x.steps[1] == key end, spots)
if #spots > 1 then
spots = H.spots_add_steps(spots, label_tbl, n_steps_ahead)
0