1105 lines
44 KiB
Lua
1105 lines
44 KiB
Lua
--- *mini.indentscope* Visualize and work with indent scope
|
|
--- *MiniIndentscope*
|
|
---
|
|
--- MIT License Copyright (c) 2022 Evgeni Chasnovski
|
|
---
|
|
--- ==============================================================================
|
|
---
|
|
--- Indent scope (or just "scope") is a maximum set of consecutive lines which
|
|
--- contains certain reference line (cursor line by default) and every member
|
|
--- has indent not less than certain reference indent ("indent at cursor" by
|
|
--- default: minimum between cursor column and indent of cursor line).
|
|
---
|
|
--- Features:
|
|
--- - Visualize scope with animated vertical line. It is very fast and done
|
|
--- automatically in a non-blocking way (other operations can be performed,
|
|
--- like moving cursor). You can customize debounce delay and animation rule.
|
|
---
|
|
--- - Customization of scope computation options can be done on global level
|
|
--- (in |MiniIndentscope.config|), for a certain buffer (using
|
|
--- `vim.b.miniindentscope_config` buffer variable), or within a call (using
|
|
--- `opts` variable in |MiniIndentscope.get_scope|).
|
|
---
|
|
--- - Customizable notion of a border: which adjacent lines with strictly lower
|
|
--- indent are recognized as such. This is useful for a certain filetypes
|
|
--- (for example, Python or plain text).
|
|
---
|
|
--- - Customizable way of line to be considered "border first". This is useful
|
|
--- if you want to place cursor on function header and get scope of its body.
|
|
---
|
|
--- - There are textobjects and motions to operate on scope. Support |count|
|
|
--- and dot-repeat (in operator pending mode).
|
|
---
|
|
--- # Setup~
|
|
---
|
|
--- This module needs a setup with `require('mini.indentscope').setup({})`
|
|
--- (replace `{}` with your `config` table). It will create global Lua table
|
|
--- `MiniIndentscope` which you can use for scripting or manually (with `:lua
|
|
--- MiniIndentscope.*`).
|
|
---
|
|
--- See |MiniIndentscope.config| for available config settings.
|
|
---
|
|
--- You can override runtime config settings locally to buffer inside
|
|
--- `vim.b.miniindentscope_config` which should have same structure as
|
|
--- `MiniIndentscope.config`. See |mini.nvim-buffer-local-config| for more details.
|
|
---
|
|
--- # Comparisons~
|
|
---
|
|
--- - 'lukas-reineke/indent-blankline.nvim':
|
|
--- - Its main functionality is about showing static guides of indent levels.
|
|
--- - Implementation of 'mini.indentscope' is similar to
|
|
--- 'indent-blankline.nvim' (using |extmarks| on first column to be shown
|
|
--- even on blank lines). They can be used simultaneously, but it will
|
|
--- lead to one of the visualizations being on top (hiding) of another.
|
|
---
|
|
--- # Highlight groups~
|
|
---
|
|
--- * `MiniIndentscopeSymbol` - symbol showing on every line of scope if its
|
|
--- indent is multiple of 'shiftwidth'.
|
|
--- * `MiniIndentscopeSymbolOff` - symbol showing on every line of scope if its
|
|
--- indent is not multiple of 'shiftwidth'.
|
|
--- Default: links to `MiniIndentscopeSymbol`.
|
|
---
|
|
--- To change any highlight group, modify it directly with |:highlight|.
|
|
---
|
|
--- # Disabling~
|
|
---
|
|
--- To disable autodrawing, set `vim.g.miniindentscope_disable` (globally) or
|
|
--- `vim.b.miniindentscope_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.
|
|
|
|
--- Drawing of scope indicator
|
|
---
|
|
--- Draw of scope indicator is done as iterative animation. It has the
|
|
--- following design:
|
|
--- - Draw indicator on origin line (where cursor is at) immediately. Indicator
|
|
--- is visualized as `MiniIndentscope.config.symbol` placed to the right of
|
|
--- scope's border indent. This creates a line from top to bottom scope edges.
|
|
--- - Draw upward and downward concurrently per one line. Progression by one
|
|
--- line in both direction is considered to be one step of animation.
|
|
--- - Before each step wait certain amount of time, which is decided by
|
|
--- "animation function". It takes next and total step numbers (both are one
|
|
--- or bigger) and returns number of milliseconds to wait before drawing next
|
|
--- step. Comparing to a more popular "easing functions" in animation (input:
|
|
--- duration since animation start; output: percent of animation done), it is
|
|
--- a discrete inverse version of its derivative. Such interface proved to be
|
|
--- more appropriate for kind of task at hand.
|
|
---
|
|
--- Special cases~
|
|
---
|
|
--- - When scope to be drawn intersects (same indent, ranges overlap) currently
|
|
--- visible one (at process or finished drawing), drawing is done immediately
|
|
--- without animation. With most common example being typing new text, this
|
|
--- feels more natural.
|
|
--- - Scope for the whole buffer is not drawn as it is isually redundant.
|
|
--- Technically, it can be thought as drawn at column 0 (because border
|
|
--- indent is -1) which is not visible.
|
|
---@tag MiniIndentscope-drawing
|
|
|
|
-- Module definition ==========================================================
|
|
local MiniIndentscope = {}
|
|
local H = {}
|
|
|
|
--- Module setup
|
|
---
|
|
---@param config table|nil Module config table. See |MiniIndentscope.config|.
|
|
---
|
|
---@usage `require('mini.indentscope').setup({})` (replace `{}` with your `config` table)
|
|
MiniIndentscope.setup = function(config)
|
|
-- Export module
|
|
_G.MiniIndentscope = MiniIndentscope
|
|
|
|
-- Setup config
|
|
config = H.setup_config(config)
|
|
|
|
-- Apply config
|
|
H.apply_config(config)
|
|
|
|
-- Define behavior
|
|
H.create_autocommands()
|
|
|
|
-- Create default highlighting
|
|
H.create_default_hl()
|
|
end
|
|
|
|
--- Module config
|
|
---
|
|
--- Default values:
|
|
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
|
|
---@text # Options ~
|
|
---
|
|
--- - Options can be supplied globally (from this `config`), locally to buffer
|
|
--- (via `options` field of `vim.b.miniindentscope_config` buffer variable),
|
|
--- or locally to call (as argument to |MiniIndentscope.get_scope()|).
|
|
---
|
|
--- - Option `border` controls which line(s) with smaller indent to categorize
|
|
--- as border. This matters for textobjects and motions.
|
|
--- It also controls how empty lines are treated: they are included in scope
|
|
--- only if followed by a border. Another way of looking at it is that indent
|
|
--- of blank line is computed based on value of `border` option.
|
|
--- Here is an illustration of how `border` works in presence of empty lines:
|
|
--- >
|
|
--- |both|bottom|top|none|
|
|
--- 1|function foo() | 0 | 0 | 0 | 0 |
|
|
--- 2| | 4 | 0 | 4 | 0 |
|
|
--- 3| print('Hello world') | 4 | 4 | 4 | 4 |
|
|
--- 4| | 4 | 4 | 2 | 2 |
|
|
--- 5| end | 2 | 2 | 2 | 2 |
|
|
--- <
|
|
--- Numbers inside a table are indent values of a line computed with certain
|
|
--- value of `border`. So, for example, a scope with reference line 3 and
|
|
--- right-most column has body range depending on value of `border` option:
|
|
--- - `border` is "both": range is 2-4, border is 1 and 5 with indent 2.
|
|
--- - `border` is "top": range is 2-3, border is 1 with indent 0.
|
|
--- - `border` is "bottom": range is 3-4, border is 5 with indent 0.
|
|
--- - `border` is "none": range is 3-3, border is empty with indent `nil`.
|
|
---
|
|
--- - Option `indent_at_cursor` controls if cursor position should affect
|
|
--- computation of scope. If `true`, reference indent is a minimum of
|
|
--- reference line's indent and cursor column. In main example, here how
|
|
--- scope's body range differs depending on cursor column and `indent_at_cursor`
|
|
--- value (assuming cursor is on line 3 and it is whole buffer):
|
|
--- >
|
|
--- Column\Option true|false
|
|
--- 1 and 2 2-5 | 2-4
|
|
--- 3 and more 2-4 | 2-4
|
|
--- <
|
|
--- - Option `try_as_border` controls how to act when input line can be
|
|
--- recognized as a border of some neighbor indent scope. In main example,
|
|
--- when input line is 1 and can be recognized as border for inner scope,
|
|
--- value `try_as_border = true` means that inner scope will be returned.
|
|
--- Similar, for input line 5 inner scope will be returned if it is
|
|
--- recognized as border.
|
|
MiniIndentscope.config = {
|
|
-- Draw options
|
|
draw = {
|
|
-- Delay (in ms) between event and start of drawing scope indicator
|
|
delay = 100,
|
|
|
|
-- Animation rule for scope's first drawing. A function which, given
|
|
-- next and total step numbers, returns wait time (in ms). See
|
|
-- |MiniIndentscope.gen_animation| for builtin options. To disable
|
|
-- animation, use `require('mini.indentscope').gen_animation.none()`.
|
|
--minidoc_replace_start animation = --<function: implements constant 20ms between steps>,
|
|
animation = function(s, n) return 20 end,
|
|
--minidoc_replace_end
|
|
|
|
-- Symbol priority. Increase to display on top of more symbols.
|
|
priority = 2,
|
|
},
|
|
|
|
-- Module mappings. Use `''` (empty string) to disable one.
|
|
mappings = {
|
|
-- Textobjects
|
|
object_scope = 'ii',
|
|
object_scope_with_border = 'ai',
|
|
|
|
-- Motions (jump to respective border line; if not present - body line)
|
|
goto_top = '[i',
|
|
goto_bottom = ']i',
|
|
},
|
|
|
|
-- Options which control scope computation
|
|
options = {
|
|
-- Type of scope's border: which line(s) with smaller indent to
|
|
-- categorize as border. Can be one of: 'both', 'top', 'bottom', 'none'.
|
|
border = 'both',
|
|
|
|
-- Whether to use cursor column when computing reference indent.
|
|
-- Useful to see incremental scopes with horizontal cursor movements.
|
|
indent_at_cursor = true,
|
|
|
|
-- Whether to first check input line to be a border of adjacent scope.
|
|
-- Use it if you want to place cursor on function header to get scope of
|
|
-- its body.
|
|
try_as_border = false,
|
|
},
|
|
|
|
-- Which character to use for drawing scope indicator
|
|
symbol = '╎',
|
|
}
|
|
--minidoc_afterlines_end
|
|
|
|
-- Module functionality =======================================================
|
|
--- Compute indent scope
|
|
---
|
|
--- Indent scope (or just "scope") is a maximum set of consecutive lines which
|
|
--- contains certain reference line (cursor line by default) and every member
|
|
--- has indent not less than certain reference indent ("indent at column" by
|
|
--- default). Here "indent at column" means minimum between input column value
|
|
--- and indent of reference line. When using cursor column, this allows for a
|
|
--- useful interactive view of nested indent scopes by making horizontal
|
|
--- movements within line.
|
|
---
|
|
--- Options controlling actual computation is taken from these places in order:
|
|
--- - Argument `opts`. Use it to ensure independence from other sources.
|
|
--- - Buffer local variable `vim.b.miniindentscope_config` (`options` field).
|
|
--- Useful to define local behavior (for example, for a certain filetype).
|
|
--- - Global options from |MiniIndentscope.config|.
|
|
---
|
|
--- Algorithm overview~
|
|
---
|
|
--- - Compute reference "indent at column". Reference line is an input `line`
|
|
--- which might be modified to one of its neighbors if `try_as_border` option
|
|
--- is `true`: if it can be viewed as border of some neighbor scope, it will.
|
|
--- - Process upwards and downwards from reference line to search for line with
|
|
--- indent strictly less than reference one. This is like casting rays up and
|
|
--- down from reference line and reference indent until meeting "a wall"
|
|
--- (character to the right of indent or buffer edge). Latest line before
|
|
--- meeting is a respective end of scope body. It always exists because
|
|
--- reference line is a such one.
|
|
--- - Based on top and bottom lines with strictly lower indent, construct
|
|
--- scopes's border. The way it is computed is decided based on `border`
|
|
--- option (see |MiniIndentscope.config| for more information).
|
|
--- - Compute border indent as maximum indent of border lines (or reference
|
|
--- indent minus one in case of no border). This is used during drawing
|
|
--- visual indicator.
|
|
---
|
|
--- Indent computation~
|
|
---
|
|
--- For every line indent is intended to be computed unambiguously:
|
|
--- - For "normal" lines indent is an output of |indent()|.
|
|
--- - Indent is `-1` for imaginary lines 0 and past last line.
|
|
--- - For blank and empty lines indent is computed based on previous
|
|
--- (|prevnonblank()|) and next (|nextnonblank()|) non-blank lines. The way
|
|
--- it is computed is decided based on `border` in order to not include blank
|
|
--- lines at edge of scope's body if there is no border there. See
|
|
--- |MiniIndentscope.config| for a details example.
|
|
---
|
|
---@param line number|nil Input line number (starts from 1). Can be modified to a
|
|
--- neighbor if `try_as_border` is `true`. Default: cursor line.
|
|
---@param col number|nil Column number (starts from 1). Default: if
|
|
--- `indent_at_cursor` option is `true` - cursor column from `curswant` of
|
|
--- |getcurpos()| (allows for more natural behavior on empty lines);
|
|
--- `math.huge` otherwise in order to not incorporate cursor in computation.
|
|
---@param opts table|nil Options to override global or buffer local ones (see
|
|
--- |MiniIndentscope.config|).
|
|
---
|
|
---@return table Table with scope information:
|
|
--- - <body> - table with <top> (top line of scope, inclusive), <bottom>
|
|
--- (bottom line of scope, inclusive), and <indent> (minimum indent within
|
|
--- scope) keys. Line numbers start at 1.
|
|
--- - <border> - table with <top> (line of top border, might be `nil`),
|
|
--- <bottom> (line of bottom border, might be `nil`), and <indent> (indent
|
|
--- of border) keys. Line numbers start at 1.
|
|
--- - <buf_id> - identifier of current buffer.
|
|
--- - <reference> - table with <line> (reference line), <column> (reference
|
|
--- column), and <indent> ("indent at column") keys.
|
|
MiniIndentscope.get_scope = function(line, col, opts)
|
|
opts = H.get_config({ options = opts }).options
|
|
|
|
-- Compute default `line` and\or `col`
|
|
if not (line and col) then
|
|
local curpos = vim.fn.getcurpos()
|
|
|
|
line = line or curpos[2]
|
|
line = opts.try_as_border and H.border_correctors[opts.border](line, opts) or line
|
|
|
|
-- Use `curpos[5]` (`curswant`, see `:h getcurpos()`) to account for blank
|
|
-- and empty lines.
|
|
col = col or (opts.indent_at_cursor and curpos[5] or math.huge)
|
|
end
|
|
|
|
-- Compute "indent at column"
|
|
local line_indent = H.get_line_indent(line, opts)
|
|
local indent = math.min(col, line_indent)
|
|
|
|
-- Make early return
|
|
local body = { indent = indent }
|
|
if indent <= 0 then
|
|
body.top, body.bottom, body.indent = 1, vim.fn.line('$'), line_indent
|
|
else
|
|
local up_min_indent, down_min_indent
|
|
body.top, up_min_indent = H.cast_ray(line, indent, 'up', opts)
|
|
body.bottom, down_min_indent = H.cast_ray(line, indent, 'down', opts)
|
|
body.indent = math.min(line_indent, up_min_indent, down_min_indent)
|
|
end
|
|
|
|
return {
|
|
body = body,
|
|
border = H.border_from_body[opts.border](body, opts),
|
|
buf_id = vim.api.nvim_get_current_buf(),
|
|
reference = { line = line, column = col, indent = indent },
|
|
}
|
|
end
|
|
|
|
--- Draw scope manually
|
|
---
|
|
--- Scope is visualized as a vertical line within scope's body range at column
|
|
--- equal to border indent plus one (or body indent if border is absent).
|
|
--- Numbering starts from one.
|
|
---
|
|
---@param scope table|nil Scope. Default: output of |MiniIndentscope.get_scope|
|
|
--- with default arguments.
|
|
---@param opts table|nil Options. Currently supported:
|
|
--- - <animation_fun> - animation function for drawing. See
|
|
--- |MiniIndentscope-drawing| and |MiniIndentscope.gen_animation|.
|
|
--- - <priority> - priority number for visualization. See `priority` option
|
|
--- for |nvim_buf_set_extmark()|.
|
|
MiniIndentscope.draw = function(scope, opts)
|
|
scope = scope or MiniIndentscope.get_scope()
|
|
local config = H.get_config()
|
|
local draw_opts =
|
|
vim.tbl_deep_extend('force', { animation_fun = config.draw.animation, priority = config.draw.priority }, opts or {})
|
|
|
|
H.undraw_scope()
|
|
|
|
H.current.scope = scope
|
|
H.draw_scope(scope, draw_opts)
|
|
end
|
|
|
|
--- Undraw currently visible scope manually
|
|
MiniIndentscope.undraw = function() H.undraw_scope() end
|
|
|
|
--- Generate builtin animation function
|
|
---
|
|
--- This is a builtin source to generate animation function for usage in
|
|
--- `MiniIndentscope.config.draw.animation`. Most of them are variations of
|
|
--- common easing functions, which provide certain type of progression for
|
|
--- revealing scope visual indicator.
|
|
---
|
|
--- Each field corresponds to one family of progression which can be customized
|
|
--- further by supplying appropriate arguments.
|
|
---
|
|
--- Examples ~
|
|
--- - Don't use animation: `MiniIndentscope.gen_animation.none()`
|
|
--- - Use quadratic "out" easing with total duration of 1000 ms:
|
|
--- `gen_animation.quadratic({ easing = 'out', duration = 1000, unit = 'total' })`
|
|
---
|
|
---@seealso |MiniIndentscope-drawing| for more information about how drawing is done.
|
|
MiniIndentscope.gen_animation = {}
|
|
|
|
---@alias __indentscope_animation_opts table|nil Options that control progression. Possible keys:
|
|
--- - <easing> `(string)` - a subtype of progression. One of "in"
|
|
--- (accelerating from zero speed), "out" (decelerating to zero speed),
|
|
--- "in-out" (default; accelerating halfway, decelerating after).
|
|
--- - <duration> `(number)` - duration (in ms) of a unit. Default: 20.
|
|
--- - <unit> `(string)` - which unit's duration `opts.duration` controls. One
|
|
--- of "step" (default; ensures average duration of step to be `opts.duration`)
|
|
--- or "total" (ensures fixed total duration regardless of scope's range).
|
|
---@alias __indentscope_animation_return function Animation function (see |MiniIndentscope-drawing|).
|
|
|
|
--- Generate no animation
|
|
---
|
|
--- Show indicator immediately. Same as animation function always returning 0.
|
|
MiniIndentscope.gen_animation.none = function()
|
|
return function() return 0 end
|
|
end
|
|
|
|
--- Generate linear progression
|
|
---
|
|
---@param opts __indentscope_animation_opts
|
|
---
|
|
---@return __indentscope_animation_return
|
|
MiniIndentscope.gen_animation.linear =
|
|
function(opts) return H.animation_arithmetic_powers(0, H.normalize_animation_opts(opts)) end
|
|
|
|
--- Generate quadratic progression
|
|
---
|
|
---@param opts __indentscope_animation_opts
|
|
---
|
|
---@return __indentscope_animation_return
|
|
MiniIndentscope.gen_animation.quadratic =
|
|
function(opts) return H.animation_arithmetic_powers(1, H.normalize_animation_opts(opts)) end
|
|
|
|
--- Generate cubic progression
|
|
---
|
|
---@param opts __indentscope_animation_opts
|
|
---
|
|
---@return __indentscope_animation_return
|
|
MiniIndentscope.gen_animation.cubic =
|
|
function(opts) return H.animation_arithmetic_powers(2, H.normalize_animation_opts(opts)) end
|
|
|
|
--- Generate quartic progression
|
|
---
|
|
---@param opts __indentscope_animation_opts
|
|
---
|
|
---@return __indentscope_animation_return
|
|
MiniIndentscope.gen_animation.quartic =
|
|
function(opts) return H.animation_arithmetic_powers(3, H.normalize_animation_opts(opts)) end
|
|
|
|
--- Generate exponential progression
|
|
---
|
|
---@param opts __indentscope_animation_opts
|
|
---
|
|
---@return __indentscope_animation_return
|
|
MiniIndentscope.gen_animation.exponential =
|
|
function(opts) return H.animation_geometrical_powers(H.normalize_animation_opts(opts)) end
|
|
|
|
--- Move cursor within scope
|
|
---
|
|
--- Cursor is placed on a first non-blank character of target line.
|
|
---
|
|
---@param side string One of "top" or "bottom".
|
|
---@param use_border boolean|nil Whether to move to border or within scope's body.
|
|
--- If particular border is absent, body is used.
|
|
---@param scope table|nil Scope to use. Default: output of |MiniIndentscope.get_scope()|.
|
|
MiniIndentscope.move_cursor = function(side, use_border, scope)
|
|
scope = scope or MiniIndentscope.get_scope()
|
|
|
|
-- This defaults to body's side if it is not present in border
|
|
local target_line = use_border and scope.border[side] or scope.body[side]
|
|
target_line = math.min(math.max(target_line, 1), vim.fn.line('$'))
|
|
|
|
vim.api.nvim_win_set_cursor(0, { target_line, 0 })
|
|
-- Move to first non-blank character to allow chaining scopes
|
|
vim.cmd('normal! ^')
|
|
end
|
|
|
|
--- Function for motion mappings
|
|
---
|
|
--- Move to a certain side of border. Respects |count| and dot-repeat (in
|
|
--- operator-pending mode). Doesn't move cursor for scope that is not shown
|
|
--- (drawing indent less that zero).
|
|
---
|
|
---@param side string One of "top" or "bottom".
|
|
---@param add_to_jumplist boolean|nil Whether to add movement to jump list. It is
|
|
--- `true` only for Normal mode mappings.
|
|
MiniIndentscope.operator = function(side, add_to_jumplist)
|
|
local scope = MiniIndentscope.get_scope()
|
|
|
|
-- Don't support scope that can't be shown
|
|
if H.scope_get_draw_indent(scope) < 0 then return end
|
|
|
|
-- Add movement to jump list. Needs remembering `count1` before that because
|
|
-- it seems to reset it to 1.
|
|
local count = vim.v.count1
|
|
if add_to_jumplist then vim.cmd('normal! m`') end
|
|
|
|
-- Make sequence of jumps
|
|
for _ = 1, count do
|
|
MiniIndentscope.move_cursor(side, true, scope)
|
|
-- Use `try_as_border = false` to enable chaining
|
|
scope = MiniIndentscope.get_scope(nil, nil, { try_as_border = false })
|
|
|
|
-- Don't support scope that can't be shown
|
|
if H.scope_get_draw_indent(scope) < 0 then return end
|
|
end
|
|
end
|
|
|
|
--- Function for textobject mappings
|
|
---
|
|
--- Respects |count| and dot-repeat (in operator-pending mode). Doesn't work
|
|
--- for scope that is not shown (drawing indent less that zero).
|
|
---
|
|
---@param use_border boolean|nil Whether to include border in textobject. When
|
|
--- `true` and `try_as_border` option is `false`, allows "chaining" calls for
|
|
--- incremental selection.
|
|
MiniIndentscope.textobject = function(use_border)
|
|
local scope = MiniIndentscope.get_scope()
|
|
|
|
-- Don't support scope that can't be shown
|
|
if H.scope_get_draw_indent(scope) < 0 then return end
|
|
|
|
-- Allow chaining only if using border
|
|
local count = use_border and vim.v.count1 or 1
|
|
|
|
-- Make sequence of incremental selections
|
|
for _ = 1, count do
|
|
-- Try finish cursor on border
|
|
local start, finish = 'top', 'bottom'
|
|
if use_border and scope.border.bottom == nil then
|
|
start, finish = 'bottom', 'top'
|
|
end
|
|
|
|
H.exit_visual_mode()
|
|
MiniIndentscope.move_cursor(start, use_border, scope)
|
|
vim.cmd('normal! V')
|
|
MiniIndentscope.move_cursor(finish, use_border, scope)
|
|
|
|
-- Use `try_as_border = false` to enable chaining
|
|
scope = MiniIndentscope.get_scope(nil, nil, { try_as_border = false })
|
|
|
|
-- Don't support scope that can't be shown
|
|
if H.scope_get_draw_indent(scope) < 0 then return end
|
|
end
|
|
end
|
|
|
|
-- Helper data ================================================================
|
|
-- Module default config
|
|
H.default_config = vim.deepcopy(MiniIndentscope.config)
|
|
|
|
-- Namespace for drawing vertical line
|
|
H.ns_id = vim.api.nvim_create_namespace('MiniIndentscope')
|
|
|
|
-- Timer for doing animation
|
|
H.timer = vim.loop.new_timer()
|
|
|
|
-- Table with current relevalnt data:
|
|
-- - `event_id` - counter for events.
|
|
-- - `scope` - latest drawn scope.
|
|
-- - `draw_status` - status of current drawing.
|
|
H.current = { event_id = 0, scope = {}, draw_status = 'none' }
|
|
|
|
-- Functions to compute indent in ambiguous cases
|
|
H.indent_funs = {
|
|
['min'] = function(top_indent, bottom_indent) return math.min(top_indent, bottom_indent) end,
|
|
['max'] = function(top_indent, bottom_indent) return math.max(top_indent, bottom_indent) end,
|
|
['top'] = function(top_indent, bottom_indent) return top_indent end,
|
|
['bottom'] = function(top_indent, bottom_indent) return bottom_indent end,
|
|
}
|
|
|
|
-- Functions to compute indent of blank line to satisfy `config.options.border`
|
|
H.blank_indent_funs = {
|
|
['none'] = H.indent_funs.min,
|
|
['top'] = H.indent_funs.bottom,
|
|
['bottom'] = H.indent_funs.top,
|
|
['both'] = H.indent_funs.max,
|
|
}
|
|
|
|
-- Functions to compute border from body
|
|
H.border_from_body = {
|
|
['none'] = function(body, opts) return {} end,
|
|
['top'] = function(body, opts) return { top = body.top - 1, indent = H.get_line_indent(body.top - 1, opts) } end,
|
|
['bottom'] = function(body, opts) return { bottom = body.bottom + 1, indent = H.get_line_indent(body.bottom + 1, opts) } end,
|
|
['both'] = function(body, opts)
|
|
return {
|
|
top = body.top - 1,
|
|
bottom = body.bottom + 1,
|
|
indent = math.max(H.get_line_indent(body.top - 1, opts), H.get_line_indent(body.bottom + 1, opts)),
|
|
}
|
|
end,
|
|
}
|
|
|
|
-- Functions to correct line in case it is a border
|
|
H.border_correctors = {
|
|
['none'] = function(line, opts) return line end,
|
|
['top'] = function(line, opts)
|
|
local cur_indent, next_indent = H.get_line_indent(line, opts), H.get_line_indent(line + 1, opts)
|
|
return (cur_indent < next_indent) and (line + 1) or line
|
|
end,
|
|
['bottom'] = function(line, opts)
|
|
local prev_indent, cur_indent = H.get_line_indent(line - 1, opts), H.get_line_indent(line, opts)
|
|
return (cur_indent < prev_indent) and (line - 1) or line
|
|
end,
|
|
['both'] = function(line, opts)
|
|
local prev_indent, cur_indent, next_indent =
|
|
H.get_line_indent(line - 1, opts), H.get_line_indent(line, opts), H.get_line_indent(line + 1, opts)
|
|
|
|
if prev_indent <= cur_indent and next_indent <= cur_indent then return line end
|
|
|
|
-- If prev and next indents are equal and bigger than current, prefer next
|
|
if prev_indent <= next_indent then return line + 1 end
|
|
|
|
return line - 1
|
|
end,
|
|
}
|
|
|
|
-- Helper functionality =======================================================
|
|
-- Settings -------------------------------------------------------------------
|
|
H.setup_config = function(config)
|
|
-- General idea: if some table elements are not present in user-supplied
|
|
-- `config`, take them from default config
|
|
vim.validate({ config = { config, 'table', true } })
|
|
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
|
|
|
|
-- Validate per nesting level to produce correct error message
|
|
vim.validate({
|
|
draw = { config.draw, 'table' },
|
|
mappings = { config.mappings, 'table' },
|
|
options = { config.options, 'table' },
|
|
symbol = { config.symbol, 'string' },
|
|
})
|
|
|
|
vim.validate({
|
|
['draw.delay'] = { config.draw.delay, 'number' },
|
|
['draw.animation'] = { config.draw.animation, 'function' },
|
|
['draw.priority'] = { config.draw.priority, 'number' },
|
|
|
|
['mappings.object_scope'] = { config.mappings.object_scope, 'string' },
|
|
['mappings.object_scope_with_border'] = { config.mappings.object_scope_with_border, 'string' },
|
|
['mappings.goto_top'] = { config.mappings.goto_top, 'string' },
|
|
['mappings.goto_bottom'] = { config.mappings.goto_bottom, 'string' },
|
|
|
|
['options.border'] = { config.options.border, 'string' },
|
|
['options.indent_at_cursor'] = { config.options.indent_at_cursor, 'boolean' },
|
|
['options.try_as_border'] = { config.options.try_as_border, 'boolean' },
|
|
})
|
|
return config
|
|
end
|
|
|
|
H.apply_config = function(config)
|
|
MiniIndentscope.config = config
|
|
local maps = config.mappings
|
|
|
|
--stylua: ignore start
|
|
H.map('n', maps.goto_top, [[<Cmd>lua MiniIndentscope.operator('top', true)<CR>]], { desc = 'Go to indent scope top' })
|
|
H.map('n', maps.goto_bottom, [[<Cmd>lua MiniIndentscope.operator('bottom', true)<CR>]], { desc = 'Go to indent scope bottom' })
|
|
|
|
H.map('x', maps.goto_top, [[<Cmd>lua MiniIndentscope.operator('top')<CR>]], { desc = 'Go to indent scope top' })
|
|
H.map('x', maps.goto_bottom, [[<Cmd>lua MiniIndentscope.operator('bottom')<CR>]], { desc = 'Go to indent scope bottom' })
|
|
H.map('x', maps.object_scope, '<Cmd>lua MiniIndentscope.textobject(false)<CR>', { desc = 'Object scope' })
|
|
H.map('x', maps.object_scope_with_border, '<Cmd>lua MiniIndentscope.textobject(true)<CR>', { desc = 'Object scope with border' })
|
|
|
|
-- 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', maps.goto_top, [[<Cmd>lua MiniIndentscope.operator('top')<CR>]], { desc = 'Go to indent scope top' })
|
|
H.map('o', maps.goto_bottom, [[<Cmd>lua MiniIndentscope.operator('bottom')<CR>]], { desc = 'Go to indent scope bottom' })
|
|
H.map('o', maps.object_scope, '<Cmd>lua MiniIndentscope.textobject(false)<CR>', { desc = 'Object scope' })
|
|
H.map('o', maps.object_scope_with_border, '<Cmd>lua MiniIndentscope.textobject(true)<CR>', { desc = 'Object scope with border' })
|
|
--stylua: ignore start
|
|
end
|
|
|
|
H.create_autocommands = function()
|
|
local augroup = vim.api.nvim_create_augroup('MiniIndentscope', {})
|
|
|
|
local au = function(event, pattern, callback, desc)
|
|
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
|
|
end
|
|
|
|
au(
|
|
{ 'CursorMoved', 'CursorMovedI', 'ModeChanged' },
|
|
'*',
|
|
function() H.auto_draw({ lazy = true }) end,
|
|
'Auto draw indentscope lazily'
|
|
)
|
|
au(
|
|
{ 'TextChanged', 'TextChangedI', 'TextChangedP', 'WinScrolled' },
|
|
'*',
|
|
function() H.auto_draw() end,
|
|
'Auto draw indentscope'
|
|
)
|
|
end
|
|
|
|
--stylua: ignore
|
|
H.create_default_hl = function()
|
|
vim.api.nvim_set_hl(0, 'MiniIndentscopeSymbol', { default = true, link = 'Delimiter' })
|
|
vim.api.nvim_set_hl(0, 'MiniIndentscopeSymbolOff', { default = true, link = 'MiniIndentscopeSymbol' })
|
|
end
|
|
|
|
H.is_disabled = function() return vim.g.miniindentscope_disable == true or vim.b.miniindentscope_disable == true end
|
|
|
|
H.get_config = function(config)
|
|
return vim.tbl_deep_extend('force', MiniIndentscope.config, vim.b.miniindentscope_config or {}, config or {})
|
|
end
|
|
|
|
-- Autocommands ---------------------------------------------------------------
|
|
H.auto_draw = function(opts)
|
|
if H.is_disabled() then
|
|
H.undraw_scope()
|
|
return
|
|
end
|
|
|
|
opts = opts or {}
|
|
local scope = MiniIndentscope.get_scope()
|
|
|
|
-- Make early return if nothing has to be done. Doing this before updating
|
|
-- event id allows to not interrupt ongoing animation.
|
|
if opts.lazy and H.current.draw_status ~= 'none' and H.scope_is_equal(scope, H.current.scope) then return end
|
|
|
|
-- Account for current event
|
|
local local_event_id = H.current.event_id + 1
|
|
H.current.event_id = local_event_id
|
|
|
|
-- Compute drawing options for current event
|
|
local draw_opts = H.make_autodraw_opts(scope)
|
|
|
|
-- Allow delay
|
|
if draw_opts.delay > 0 then H.undraw_scope(draw_opts) end
|
|
|
|
-- Use `defer_fn()` even if `delay` is 0 to draw indicator only after all
|
|
-- events are processed (stops flickering)
|
|
vim.defer_fn(function()
|
|
if H.current.event_id ~= local_event_id then return end
|
|
|
|
H.undraw_scope(draw_opts)
|
|
|
|
H.current.scope = scope
|
|
H.draw_scope(scope, draw_opts)
|
|
end, draw_opts.delay)
|
|
end
|
|
|
|
-- Scope ----------------------------------------------------------------------
|
|
-- Line indent:
|
|
-- - Equals output of `vim.fn.indent()` in case of non-blank line.
|
|
-- - Depends on `MiniIndentscope.config.options.border` in such way so as to
|
|
-- ignore blank lines before line not recognized as border.
|
|
H.get_line_indent = function(line, opts)
|
|
local prev_nonblank = vim.fn.prevnonblank(line)
|
|
local res = vim.fn.indent(prev_nonblank)
|
|
|
|
-- Compute indent of blank line depending on `options.border` values
|
|
if line ~= prev_nonblank then
|
|
local next_indent = vim.fn.indent(vim.fn.nextnonblank(line))
|
|
local blank_rule = H.blank_indent_funs[opts.border]
|
|
res = blank_rule(res, next_indent)
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
H.cast_ray = function(line, indent, direction, opts)
|
|
local final_line, increment = 1, -1
|
|
if direction == 'down' then
|
|
final_line, increment = vim.fn.line('$'), 1
|
|
end
|
|
|
|
local min_indent = math.huge
|
|
for l = line, final_line, increment do
|
|
local new_indent = H.get_line_indent(l + increment, opts)
|
|
if new_indent < indent then return l, min_indent end
|
|
if new_indent < min_indent then min_indent = new_indent end
|
|
end
|
|
|
|
return final_line, min_indent
|
|
end
|
|
|
|
H.scope_get_draw_indent = function(scope) return scope.border.indent or (scope.body.indent - 1) end
|
|
|
|
H.scope_is_equal = function(scope_1, scope_2)
|
|
if type(scope_1) ~= 'table' or type(scope_2) ~= 'table' then return false end
|
|
|
|
return scope_1.buf_id == scope_2.buf_id
|
|
and H.scope_get_draw_indent(scope_1) == H.scope_get_draw_indent(scope_2)
|
|
and scope_1.body.top == scope_2.body.top
|
|
and scope_1.body.bottom == scope_2.body.bottom
|
|
end
|
|
|
|
H.scope_has_intersect = function(scope_1, scope_2)
|
|
if type(scope_1) ~= 'table' or type(scope_2) ~= 'table' then return false end
|
|
if (scope_1.buf_id ~= scope_2.buf_id) or (H.scope_get_draw_indent(scope_1) ~= H.scope_get_draw_indent(scope_2)) then
|
|
return false
|
|
end
|
|
|
|
local body_1, body_2 = scope_1.body, scope_2.body
|
|
return (body_2.top <= body_1.top and body_1.top <= body_2.bottom)
|
|
or (body_1.top <= body_2.top and body_2.top <= body_1.bottom)
|
|
end
|
|
|
|
-- Indicator ------------------------------------------------------------------
|
|
--- Compute indicator of scope to be displayed
|
|
---
|
|
--- Indicator is visual representation of scope in current window view using
|
|
--- extmarks. Currently only needed because Neovim can't correctly process
|
|
--- horizontal window scroll (Neovim issue:
|
|
--- https://github.com/neovim/neovim/issues/14050)
|
|
---
|
|
---@return table|nil Table with indicator info or empty one in case indicator
|
|
--- shouldn't be drawn.
|
|
---@private
|
|
H.indicator_compute = function(scope)
|
|
scope = scope or H.current.scope
|
|
local indent = H.scope_get_draw_indent(scope)
|
|
|
|
-- Don't draw indicator that should be outside of screen. This condition is
|
|
-- (perpusfully) "responsible" for not drawing indicator spanning whole file.
|
|
if indent < 0 then return {} end
|
|
|
|
-- Text indentation should depend on current window view because it will use
|
|
-- `virt_text_win_col` attribute of extmark options (the only way to reliably
|
|
-- put it anywhere on screen; important to show properly on empty lines).
|
|
local col = indent - vim.fn.winsaveview().leftcol
|
|
if col < 0 then return {} end
|
|
|
|
-- Pick highlight group based on if indent is a multiple of shiftwidth.
|
|
-- This adds visual indicator of whether indent is "correct".
|
|
local hl_group = (indent % vim.fn.shiftwidth() == 0) and 'MiniIndentscopeSymbol' or 'MiniIndentscopeSymbolOff'
|
|
local virt_text = { { H.get_config().symbol, hl_group } }
|
|
|
|
return {
|
|
buf_id = vim.api.nvim_get_current_buf(),
|
|
virt_text = virt_text,
|
|
virt_text_win_col = col,
|
|
top = scope.body.top,
|
|
bottom = scope.body.bottom,
|
|
}
|
|
end
|
|
|
|
-- Drawing --------------------------------------------------------------------
|
|
H.draw_scope = function(scope, opts)
|
|
scope = scope or {}
|
|
opts = opts or {}
|
|
|
|
local indicator = H.indicator_compute(scope)
|
|
|
|
-- Don't draw anything if nothing to be displayed
|
|
if indicator.virt_text == nil or #indicator.virt_text == 0 then
|
|
H.current.draw_status = 'finished'
|
|
return
|
|
end
|
|
|
|
-- Make drawing function
|
|
local draw_fun = H.make_draw_function(indicator, opts)
|
|
|
|
-- Perform drawing
|
|
H.current.draw_status = 'drawing'
|
|
H.draw_indicator_animation(indicator, draw_fun, opts.animation_fun)
|
|
end
|
|
|
|
H.draw_indicator_animation = function(indicator, draw_fun, animation_fun)
|
|
-- Draw from origin (cursor line but within indicator range)
|
|
local top, bottom = indicator.top, indicator.bottom
|
|
local origin = math.min(math.max(vim.fn.line('.'), top), bottom)
|
|
|
|
local step = 0
|
|
local n_steps = math.max(origin - top, bottom - origin)
|
|
local wait_time = 0
|
|
|
|
local draw_step
|
|
draw_step = vim.schedule_wrap(function()
|
|
-- Check for not drawing outside of interval is done inside `draw_fun`
|
|
local success = draw_fun(origin - step)
|
|
if step > 0 then success = success and draw_fun(origin + step) end
|
|
|
|
if not success or step == n_steps then
|
|
H.current.draw_status = step == n_steps and 'finished' or H.current.draw_status
|
|
H.timer:stop()
|
|
return
|
|
end
|
|
|
|
step = step + 1
|
|
wait_time = wait_time + animation_fun(step, n_steps)
|
|
|
|
-- Repeat value of `timer` seems to be rounded down to milliseconds. This
|
|
-- means that values less than 1 will lead to timer stop repeating. Instead
|
|
-- call next step function directly.
|
|
if wait_time < 1 then
|
|
H.timer:set_repeat(0)
|
|
-- Use `return` to make this proper "tail call"
|
|
return draw_step()
|
|
else
|
|
H.timer:set_repeat(wait_time)
|
|
|
|
-- Restart `wait_time` only if it is actually used. Do this accounting
|
|
-- actually set repeat time.
|
|
wait_time = wait_time - H.timer:get_repeat()
|
|
|
|
-- Usage of `again()` is needed to overcome the fact that it is called
|
|
-- inside callback and to restart initial timer. Mainly this is needed
|
|
-- only in case of transition from 'non-repeating' timer to 'repeating'
|
|
-- one in case of complex animation functions. See
|
|
-- https://docs.libuv.org/en/v1.x/timer.html#api
|
|
H.timer:again()
|
|
end
|
|
end)
|
|
|
|
-- Start non-repeating timer without callback execution. This shouldn't be
|
|
-- `timer:start(0, 0, draw_step)` because it will execute `draw_step` on the
|
|
-- next redraw (flickers on window scroll).
|
|
H.timer:start(10000000, 0, draw_step)
|
|
|
|
-- Draw step zero (at origin) immediately
|
|
draw_step()
|
|
end
|
|
|
|
H.undraw_scope = function(opts)
|
|
opts = opts or {}
|
|
|
|
-- Don't operate outside of current event if able to verify
|
|
if opts.event_id and opts.event_id ~= H.current.event_id then return end
|
|
|
|
pcall(vim.api.nvim_buf_clear_namespace, H.current.scope.buf_id or 0, H.ns_id, 0, -1)
|
|
|
|
H.current.draw_status = 'none'
|
|
H.current.scope = {}
|
|
end
|
|
|
|
H.make_autodraw_opts = function(scope)
|
|
local config = H.get_config()
|
|
local res = {
|
|
event_id = H.current.event_id,
|
|
type = 'animation',
|
|
delay = config.draw.delay,
|
|
animation_fun = config.draw.animation,
|
|
priority = config.draw.priority,
|
|
}
|
|
|
|
if H.current.draw_status == 'none' then return res end
|
|
|
|
-- Draw immediately scope which intersects (same indent, overlapping ranges)
|
|
-- currently drawn or finished. This is more natural when typing text.
|
|
if H.scope_has_intersect(scope, H.current.scope) then
|
|
res.type = 'immediate'
|
|
res.delay = 0
|
|
res.animation_fun = MiniIndentscope.gen_animation.none()
|
|
return res
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
H.make_draw_function = function(indicator, opts)
|
|
local extmark_opts = {
|
|
hl_mode = 'combine',
|
|
priority = opts.priority,
|
|
right_gravity = false,
|
|
virt_text = indicator.virt_text,
|
|
virt_text_win_col = indicator.virt_text_win_col,
|
|
virt_text_pos = 'overlay',
|
|
}
|
|
|
|
local current_event_id = opts.event_id
|
|
|
|
return function(l)
|
|
-- Don't draw if outdated
|
|
if H.current.event_id ~= current_event_id and current_event_id ~= nil then return false end
|
|
|
|
-- Don't draw if disabled
|
|
if H.is_disabled() then return false end
|
|
|
|
-- Don't put extmark outside of indicator range
|
|
if not (indicator.top <= l and l <= indicator.bottom) then return true end
|
|
|
|
return pcall(vim.api.nvim_buf_set_extmark, indicator.buf_id, H.ns_id, l - 1, 0, extmark_opts)
|
|
end
|
|
end
|
|
|
|
-- Animations -----------------------------------------------------------------
|
|
--- Imitate common power easing function
|
|
---
|
|
--- Every step is preceded by waiting time decreasing/increasing in power
|
|
--- series fashion (`d` is "delta", ensures total duration time):
|
|
--- - "in": d*n^p; d*(n-1)^p; ... ; d*2^p; d*1^p
|
|
--- - "out": d*1^p; d*2^p; ... ; d*(n-1)^p; d*n^p
|
|
--- - "in-out": "in" until 0.5*n, "out" afterwards
|
|
---
|
|
--- This way it imitates `power + 1` common easing function because animation
|
|
--- progression behaves as sum of `power` elements.
|
|
---
|
|
---@param power number Power of series.
|
|
---@param opts table Options from `MiniIndentscope.gen_animation` entry.
|
|
---@private
|
|
H.animation_arithmetic_powers = function(power, opts)
|
|
-- Sum of first `n_steps` natural numbers raised to `power`
|
|
local arith_power_sum = ({
|
|
[0] = function(n_steps) return n_steps end,
|
|
[1] = function(n_steps) return n_steps * (n_steps + 1) / 2 end,
|
|
[2] = function(n_steps) return n_steps * (n_steps + 1) * (2 * n_steps + 1) / 6 end,
|
|
[3] = function(n_steps) return n_steps ^ 2 * (n_steps + 1) ^ 2 / 4 end,
|
|
})[power]
|
|
|
|
-- Function which computes common delta so that overall duration will have
|
|
-- desired value (based on supplied `opts`)
|
|
local duration_unit, duration_value = opts.unit, opts.duration
|
|
local make_delta = function(n_steps, is_in_out)
|
|
local total_time = duration_unit == 'total' and duration_value or (duration_value * n_steps)
|
|
local total_parts
|
|
if is_in_out then
|
|
-- Examples:
|
|
-- - n_steps=5: 3^d, 2^d, 1^d, 2^d, 3^d
|
|
-- - n_steps=6: 3^d, 2^d, 1^d, 1^d, 2^d, 3^d
|
|
total_parts = 2 * arith_power_sum(math.ceil(0.5 * n_steps)) - (n_steps % 2 == 1 and 1 or 0)
|
|
else
|
|
total_parts = arith_power_sum(n_steps)
|
|
end
|
|
return total_time / total_parts
|
|
end
|
|
|
|
return ({
|
|
['in'] = function(s, n) return make_delta(n) * (n - s + 1) ^ power end,
|
|
['out'] = function(s, n) return make_delta(n) * s ^ power end,
|
|
['in-out'] = function(s, n)
|
|
local n_half = math.ceil(0.5 * n)
|
|
local s_halved
|
|
if n % 2 == 0 then
|
|
s_halved = s <= n_half and (n_half - s + 1) or (s - n_half)
|
|
else
|
|
s_halved = s < n_half and (n_half - s + 1) or (s - n_half + 1)
|
|
end
|
|
return make_delta(n, true) * s_halved ^ power
|
|
end,
|
|
})[opts.easing]
|
|
end
|
|
|
|
--- Imitate common exponential easing function
|
|
---
|
|
--- Every step is preceded by waiting time decreasing/increasing in geometric
|
|
--- progression fashion (`d` is 'delta', ensures total duration time):
|
|
--- - 'in': (d-1)*d^(n-1); (d-1)*d^(n-2); ...; (d-1)*d^1; (d-1)*d^0
|
|
--- - 'out': (d-1)*d^0; (d-1)*d^1; ...; (d-1)*d^(n-2); (d-1)*d^(n-1)
|
|
--- - 'in-out': 'in' until 0.5*n, 'out' afterwards
|
|
---
|
|
---@param opts table Options from `MiniIndentscope.gen_animation` entry.
|
|
---@private
|
|
H.animation_geometrical_powers = function(opts)
|
|
-- Function which computes common delta so that overall duration will have
|
|
-- desired value (based on supplied `opts`)
|
|
local duration_unit, duration_value = opts.unit, opts.duration
|
|
local make_delta = function(n_steps, is_in_out)
|
|
local total_time = duration_unit == 'step' and (duration_value * n_steps) or duration_value
|
|
-- Exact solution to avoid possible (bad) approximation
|
|
if n_steps == 1 then return total_time + 1 end
|
|
if is_in_out then
|
|
local n_half = math.ceil(0.5 * n_steps)
|
|
-- Example for n_steps=6:
|
|
-- Steps: (d-1)*d^2, (d-1)*d^1, (d-1)*d^0, (d-1)*d^0, (d-1)*d^1, (d-1)*d^2
|
|
-- Sum: 2 * (d - 1) * (d^0 + d^1 + d^2) = 2 * (d^3 - 1)
|
|
-- Solution: 2 * (d^3 - 1) = total_time =>
|
|
-- d = math.pow(0.5 * total_time + 1, 1 / 3)
|
|
--
|
|
-- Example for n_steps=5:
|
|
-- Steps: (d-1)*d^2, (d-1)*d^1, (d-1)*d^0, (d-1)*d^1, (d-1)*d^2
|
|
-- Sum: 2 * (d - 1) * (d^0 + d^1 + d^2) - (d - 1) = 2 * (d^3 - 1) - (d - 1)
|
|
-- Solution: 2 * (d^3 - 1) - (d - 1) = total_time =>
|
|
-- As there is no general explicit solution, use approximation =>
|
|
-- (Exact solution without `- (d-1)`):
|
|
-- d_0 = math.pow(0.5 * total_time + 1, 1 / 3);
|
|
-- (Correction by solving exactly withtou `- (d-1)` for
|
|
-- `total_time_corr = total_time + (d_0 - 1)`):
|
|
-- d_1 = math.pow(0.5 * total_time_corr + 1, 1 / 3)
|
|
if n_steps % 2 == 1 then total_time = total_time + math.pow(0.5 * total_time + 1, 1 / n_half) - 1 end
|
|
return math.pow(0.5 * total_time + 1, 1 / n_half)
|
|
end
|
|
return math.pow(total_time + 1, 1 / n_steps)
|
|
end
|
|
|
|
return ({
|
|
['in'] = function(s, n)
|
|
local delta = make_delta(n)
|
|
return (delta - 1) * delta ^ (n - s)
|
|
end,
|
|
['out'] = function(s, n)
|
|
local delta = make_delta(n)
|
|
return (delta - 1) * delta ^ (s - 1)
|
|
end,
|
|
['in-out'] = function(s, n)
|
|
local n_half, delta = math.ceil(0.5 * n), make_delta(n, true)
|
|
local s_halved
|
|
if n % 2 == 0 then
|
|
s_halved = s <= n_half and (n_half - s) or (s - n_half - 1)
|
|
else
|
|
s_halved = s < n_half and (n_half - s) or (s - n_half)
|
|
end
|
|
return (delta - 1) * delta ^ s_halved
|
|
end,
|
|
})[opts.easing]
|
|
end
|
|
|
|
H.normalize_animation_opts = function(x)
|
|
x = vim.tbl_deep_extend('force', { easing = 'in-out', duration = 20, unit = 'step' }, x or {})
|
|
|
|
if not vim.tbl_contains({ 'in', 'out', 'in-out' }, x.easing) then
|
|
H.error([[In `gen_animation` option `easing` should be one of 'in', 'out', or 'in-out'.]])
|
|
end
|
|
|
|
if type(x.duration) ~= 'number' or x.duration < 0 then
|
|
H.error([[In `gen_animation` option `duration` should be a positive number.]])
|
|
end
|
|
|
|
if not vim.tbl_contains({ 'total', 'step' }, x.unit) then
|
|
H.error([[In `gen_animation` option `unit` should be one of 'step' or 'total'.]])
|
|
end
|
|
|
|
return x
|
|
end
|
|
|
|
-- Utilities ------------------------------------------------------------------
|
|
H.error = function(msg) error(('(mini.indentscope) %s'):format(msg)) end
|
|
|
|
H.map = function(mode, lhs, rhs, opts)
|
|
if lhs == '' then return end
|
|
opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
|
|
vim.keymap.set(mode, lhs, rhs, opts)
|
|
end
|
|
|
|
H.exit_visual_mode = function()
|
|
local ctrl_v = vim.api.nvim_replace_termcodes('<C-v>', true, true, true)
|
|
local cur_mode = vim.fn.mode()
|
|
if cur_mode == 'v' or cur_mode == 'V' or cur_mode == ctrl_v then vim.cmd('normal! ' .. cur_mode) end
|
|
end
|
|
|
|
return MiniIndentscope
|