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

Skip to content

Latest commit

 

History

History
2066 lines (1834 loc) · 74.9 KB

align.lua

File metadata and controls

2066 lines (1834 loc) · 74.9 KB
--- *mini.align* Align text interactively
---
--- MIT License Copyright (c) 2022 Evgeni Chasnovski
--- Rich and flexible customization of both alignment rules and user interaction.
--- Works with charwise, linewise, and blockwise selections in both Normal mode
--- (on textobject/motion; with dot-repeat) and Visual mode.
---
--- Features:
--- - Alignment is done in three main steps:
--- - <Split> lines into parts based on Lua pattern(s) or user-supplied rule.
--- - <Justify> parts for certain side(s) to be same width inside columns.
--- - <Merge> parts to be lines, with customizable delimiter(s).
---
--- Each main step can be preceded by other steps (pre-steps) to achieve
--- highly customizable outcome. See `steps` value in |MiniAlign.config|.
--- For more details, see |MiniAlign-glossary| and |MiniAlign-algorithm|.
---
--- - User can control alignment interactively by pressing customizable modifiers
--- (single keys representing how alignment steps and/or options should change).
--- Some of default modifiers:
--- - Press `s` to enter split Lua pattern.
--- - Press `j` to choose justification side from available ones ("left",
--- "center", "right", "none").
--- - Press `m` to enter merge delimiter.
--- - Press `f` to enter filter Lua expression to configure which parts
--- will be affected (like "align only first column").
--- - Press `i` to ignore some commonly unwanted split matches.
--- - Press `p` to pair neighboring parts so they be aligned together.
--- - Press `t` to trim whitespace from parts.
--- - Press `<BS>` (backspace) to delete some last pre-step.
---
--- For more details, see |MiniAlign-modifiers-builtin| and |MiniAlign-examples|.
---
--- - Alignment can be done with instant preview (result is updated after each
--- modifier) or without it (result is shown and accepted after non-default
--- split pattern is set).
---
--- - Every user interaction is accompanied with helper status message showing
--- relevant information about current alignment process.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.align').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniAlign`
--- which you can use for scripting or manually (with `:lua MiniAlign.*`).
---
--- See |MiniAlign.config| for available config settings.
---
--- You can override runtime config settings (like `config.modifiers`) locally
--- to buffer inside `vim.b.minialign_config` which should have same structure
--- as `MiniAlign.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Comparisons ~
---
--- - [junegunn/vim-easy-align](https://github.com/junegunn/vim-easy-align):
--- - 'mini.align' is mostly designed after 'junegunn/vim-easy-align', so
--- there are a lot of similarities.
--- - Both plugins allow users to change alignment options interactively by
--- pressing modifier keys (albeit completely different default ones).
--- 'junegunn/vim-easy-align' has those modifiers fixed, while 'mini.align'
--- allows their full customization. See |MiniAlign.config| for examples.
--- - 'junegunn/vim-easy-align' is designed to treat delimiters differently
--- than other parts of strings. 'mini.align' doesn't distinguish split
--- parts from one another by design: splitting is allowed to be done
--- based on some other logic than by splitting on delimiters.
--- - 'junegunn/vim-easy-align' initially aligns by only first delimiter.
--- 'mini.align' initially aligns by all delimiter.
--- - 'junegunn/vim-easy-align' implements special filtering by delimiter
--- row number. 'mini.align' has builtin filtering based on Lua code
--- supplied by user in modifier phase. See |MiniAlign.gen_step.filter()|
--- and 'f' builtin modifier.
--- - 'mini.align' treats any non-registered modifier as a plain delimiter
--- pattern, while 'junegunn/vim-easy-align' does not.
--- - 'mini.align' exports core Lua function used for aligning strings
--- (|MiniAlign.align_strings()|).
--- - [godlygeek/tabular](https://github.com/godlygeek/tabular):
--- - 'godlygeek/tabular' is mostly designed around single command which is
--- customized by printing its parameters. 'mini.align' implements
--- different concept of interactive alignment through pressing
--- customizable single character modifiers.
--- - 'godlygeek/tabular' can detect region upon which alignment can be
--- desirable. 'mini.align' does not by design: use Visual selection or
--- textobject/motion to explicitly define region to align.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minialign_disable` (globally) or `vim.b.minialign_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.
---@tag MiniAlign
--- PARTS ~
--- 2d array of strings (array of arrays of strings).
--- See more in |MiniAlign.as_parts()|.
---
--- ROW ~
--- First-level array of parts (like `parts[1]`).
---
--- COLUMN ~
--- Array of strings, constructed from parts elements with the same
--- second-level index (like `{ parts[1][1],` `parts[2][1], ... }`).
---
--- STEP ~
--- A named callable. See |MiniAlign.new_step()|. When used in terms of alignment
--- steps, callable takes two arguments: some object (parts or string array)
--- and option table.
---
--- SPLIT ~
--- Process of taking array of strings and converting it into parts.
---
--- JUSTIFY ~
--- Process of taking parts and converting them to aligned parts (all elements
--- have same widths inside columns).
---
--- MERGE ~
--- Process of taking parts and converting it back to array of strings. Usually
--- by concatenating rows into strings.
---
--- REGION ~
--- Table representing region in a buffer. Fields <from> / <to> have inclusive
--- start / end positions (<to> might be `nil` to describe empty region).
--- Positions are also tables with <line> and <col> fields (both start at 1).
---
--- MODE ~
--- Either charwise ("char", `v`, |charwise|), linewise ("line", `V`, |linewise|)
--- or blockwise ("block", `<C-v>`, |blockwise-visual|)
---@tag MiniAlign-glossary
--- There are two main processes implemented in 'mini.align': strings alignment
--- and interactive region alignment. See |MiniAlign-glossary| for more information
--- about used terms.
---
--- # Strings alignment ~
---
--- Main implementation is in |MiniAlign.align_strings()|. Its input is array of
--- strings and output - array of aligned strings. The process consists from three
--- main steps (split, justify, merge) which can be preceded by any number of
--- preliminary steps (pre-split, pre-justify, pre-merge).
---
--- Algorithm:
--- - <Pre-split>. Take input array of strings and consecutively apply all
--- pre-split steps (`steps.pre_split`). Each one has `(strings, opts)` signature
--- and should modify array in place.
--- - <Split>. Take array of strings and convert it to parts with `steps.split()`.
--- It has `(strings, opts)` signature and should return parts.
--- - <Pre-justify>. Take parts and consecutively apply all pre-justify
--- steps (`steps.pre_justify`). Each one has `(parts, opts)` signature and
--- should modify parts in place.
--- - <Justify>. Take parts and apply `steps.justify()`. It has `(parts, opts)`
--- signature and should modify parts in place.
--- - <Pre-merge>. Take parts and consecutively apply all pre-merge
--- steps (`steps.pre_merge`). Each one has `(parts, opts)` signature and
--- should modify parts in place.
--- - <Merge>. Take parts and convert it to array of strings with `steps.merge()`.
--- It has `(parts, opts)` signature and should return array of strings.
---
--- Notes:
--- - All table objects are initially copied so that modification in place doesn't
--- affect workflow.
--- - Default main steps are designed to be controlled via options. See
--- |MiniAlign.align_strings()| and default step entries in |MiniAlign.gen_step|.
--- - All steps are guaranteed to take same option table as second argument.
--- This allows steps to "talk" to each other, i.e. earlier steps can pass data
--- to later ones.
---
--- # Interactive region alignment ~
---
--- Interactive alignment is a main entry point for most users. It can be done
--- in two flavors:
--- - <Without-preview>. Initiated via mapping defined in `start` of
--- `MiniAlign.config.mappings`. Alignment is accepted once split pattern becomes
--- non-default.
--- - <With-preview>. Initiated via mapping defined in `start_with_preview` of
--- `MiniAlign.config.mappings`. Alignment result is shown after every modifier
--- and is accepted after `<CR>` (`Enter`) is hit. Note: each preview is done by
--- applying current alignment steps and options to the initial region lines,
--- not the ones currently displaying in preview.
---
--- Lifecycle (assuming default mappings):
--- - <Initiate-alignment>:
--- - In Normal mode type `ga` (or `gA` to show preview) followed by textobject
--- or motion defining region to be aligned.
--- - In Visual mode select region and type `ga` (or `gA` to show preview).
--- Strings contained in selected region will be used as input to
--- |MiniAlign.align_strings()|.
--- Beware of mode when selecting region: charwise (`v`), linewise (`V`), or
--- blockwise (`<C-v>`). They all behave differently.
--- - <Press-modifiers>. Press single keys one at a time:
--- - If pressed key is among table keys of `modifiers` table of
--- |MiniAlign.config|, its function value is executed. It usually modifies
--- some options(s) and/or affects some pre-step(s).
--- - If pressed key is not among defined modifiers, it is treated as plain
--- split pattern.
--- This process can either end by itself (usually in case of no preview and
--- non-default split pattern being set) or you can choose to end it manually.
--- - <Accept-or-discard>. In case of active preview, accept current result by
--- pressing `<CR>`. Discard any result and return to initial regions with
--- either `<Esc>` or `<C-c>`.
---
--- See more in |MiniAlign-modifiers-builtin| and |MiniAlign-examples|.
---
--- Notes:
--- - Visual blockwise selection works best with 'virtualedit' equal to "block"
--- or "all".
--- - Alignment with preview works best with 'showmode' disabled.
---@tag MiniAlign-algorithm
--- Overview of builtin modifiers
---
--- All examples assume interactive alignment with preview in linewise mode. With
--- default mappings, use `V` to select lines and `gA` to initiate alignment. It
--- might be helpful to copy lines into modifiable buffer and experiment yourself.
---
--- Notes:
--- - Any pressed key which doesn't have defined modifier will be treated as
--- plain split pattern.
--- - All modifiers can be customized inside |MiniAlign.setup()|. See "Modifiers"
--- section of |MiniAlign.config|.
---
--- # Main option modifiers ~
---
--- <s> Enter split pattern (confirm prompt by pressing `<CR>`). Input is treated
--- as plain delimiter.
---
--- Before: >
--- a-b-c
--- aa-bb-cc
--- <
--- After typing `s-<CR>`: >
--- a -b -c
--- aa-bb-cc
--- <
--- <j> Choose justify side. Prompts user (with helper message) to type single
--- character identifier of side: `l`eft, `c`enter, `r`ight, `n`one.
---
--- Before: >
--- a_b_c
--- aa_bb_cc
--- <
--- After typing `_jr` (first make split by `_`): >
--- a_ b_ c
--- aa_bb_cc
--- <
--- <m> Enter merge delimiter (confirm prompt by pressing `<CR>`).
---
--- Before: >
--- a_b_c
--- aa_bb_cc
--- <
--- After typing `_m--<CR>` (first make split by `_`): >
--- a --_--b --_--c
--- aa--_--bb--_--cc
--- <
--- # Modifiers adding pre-steps ~
---
--- <f> Enter filter expression. See more details in |MiniAlign.gen_step.filter()|.
---
--- Before: >
--- a_b_c
--- aa_bb_cc
--- <
--- After typing `_fn==1<CR>` (first make split by `_`): >
--- a _b_c
--- aa_bb_cc
--- <
--- <i> Ignore some split matches. It modifies `split_exclude_patterns` option by
--- adding commonly wanted patterns. See more details in
--- |MiniAlign.gen_step.ignore_split()|.
---
--- Before: >
--- /* This_is_assumed_to_be_comment */
--- a"_"_b
--- aa_bb
--- <
--- After typing `_i` (first make split by `_`): >
--- /* This_is_assumed_to_be_comment */
--- a"_"_b
--- aa _bb
--- <
--- <p> Pair neighboring parts.
---
--- Before: >
--- a_b_c
--- aaa_bbb_ccc
--- <
--- After typing `_p` (first make split by `_`): >
--- a_ b_ c
--- aaa_bbb_ccc
--- <
--- <t> Trim parts from whitespace on both sides (keeping indentation).
---
--- Before: >
--- a _ b _ c
--- aa _bb _cc
--- <
--- After typing `_t` (first make split by `_`): >
--- a _b _c
--- aa_bb_cc
--- <
--- # Delete some last pre-step ~
---
--- <BS> Delete one of the pre-steps. If there is only one kind of pre-steps,
--- remove its latest added one. If not, prompt user to choose pre-step kind
--- by entering single character: `s`plit, `j`ustify, `m`erge.
---
--- Examples:
--- - `tp<BS>` results in only "trim" step to be left.
--- - `it<BS>` prompts to choose which step to delete (pre-split or
--- pre-justify in this case).
---
--- # Special configurations for common splits ~
---
--- <=> Use special pattern to align by a group of consecutive "=". It can be
--- preceded by any number of punctuation marks and followed by some sommon
--- punctuation characters. Trim whitespace and merge with single space.
---
--- Before: >
--- a=b
--- aa<=bb
--- aaa===bbb
--- aaaa = cccc
--- <
--- After typing `=`: >
--- a = b
--- aa <= bb
--- aaa === bbb
--- aaaa = cccc
--- <
--- <,> Besides splitting by "," character, trim whitespace, pair neighboring
--- parts and merge with single space.
---
--- Before: >
--- a,b
--- aa,bb
--- aaa , bbb
--- <
--- After typing `,`: >
--- a, b
--- aa, bb
--- aaa, bbb
--- <
--- <|> Split by "|" character, trim whitespace, merge with single space.
---
--- Before: >
--- |a|b|
--- |aa|bb|
--- |aaa | bbb |
--- <
--- After typing `|`: >
--- | a | b |
--- | aa | bb |
--- | aaa | bbb |
--- <
--- <Space> (Space bar) Squash consecutive whitespace into single space (except
--- possible indentation) and split by `%s+` pattern (keeps indentation).
---
--- Before: >
--- a b c
--- aa bb cc
--- <
--- After typing `<Space>`: >
--- a b c
--- aa bb cc
--- <
---@tag MiniAlign-modifiers-builtin
--- Copy lines in modifiable buffer, initiate alignment with preview (`gAip`)
--- and try typing suggested key sequences.
--- These are modified examples taken from 'junegunn/vim-easy-align'.
---
--- # Equal sign ~
---
--- Lines: >
---
--- # This=is=assumed=to be a comment
--- "a ="
--- a =
--- a = 1
--- bbbb = 2
--- ccccccc = 3
--- ccccccccccccccc
--- ddd = 4
--- eeee === eee = eee = eee=f
--- fff = ggg += gg &&= gg
--- g != hhhhhhhh == 888
--- i := 5
--- i %= 5
--- i *= 5
--- j =~ 5
--- j >= 5
--- aa => 123
--- aa <<= 123
--- aa >>= 123
--- bbb => 123
--- c => 1233123
--- d => 123
--- dddddd &&= 123
--- dddddd ||= 123
--- dddddd /= 123
--- gg <=> ee
--- <
--- Key sequences:
--- - `=`
--- - `=jc`
--- - `=jr`
--- - `=m!<CR>`
--- - `=p`
--- - `=i` (execute `:lua vim.o.commentstring = '# %s'` for full experience)
--- - `=<BS>`
--- - `=<BS>p`
--- - `=fn==1<CR>`
--- - `=<BS>fn==1<CR>t`
--- - `=frow>7<CR>`
---@tag MiniAlign-examples
---@alias __align_with_preview boolean|nil Whether to align with live preview.
-- Module definition ==========================================================
local MiniAlign = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniAlign.config|.
---
---@usage >lua
--- require('mini.align').setup() -- use default config
--- -- OR
--- require('mini.align').setup({}) -- replace {} with your config table
--- <
MiniAlign.setup = function(config)
-- Export module
_G.MiniAlign = MiniAlign
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Modifiers ~
---
--- `MiniAlign.config.modifiers` is used to define interactive user experience
--- of managing alignment process. It is a table with single character keys and
--- modifier function values.
---
--- Each modifier function:
--- - Is called when corresponding modifier key is pressed.
--- - Has signature `(steps, opts)` and should modify any of its input in place.
---
--- Examples:
--- - Modifier function used for default 'i' modifier: >lua
---
--- function(steps, _)
--- table.insert(steps.pre_split, MiniAlign.gen_step.ignore_split())
--- end
--- <
--- - Tweak 't' modifier to use highest indentation instead of keeping it: >lua
---
--- require('mini.align').setup({
--- modifiers = {
--- t = function(steps, _)
--- local trim_high = MiniAlign.gen_step.trim('both', 'high')
--- table.insert(steps.pre_justify, trim_high)
--- end
--- }
--- })
--- <
--- - Tweak `j` modifier to cycle through available "justify_side" option
--- values (like in 'junegunn/vim-easy-align'): >lua
---
--- require('mini.align').setup({
--- modifiers = {
--- j = function(_, opts)
--- local next_option = ({
--- left = 'center', center = 'right', right = 'none', none = 'left',
--- })[opts.justify_side]
--- opts.justify_side = next_option or 'left'
--- end,
--- },
--- })
--- <
--- # Options ~
---
--- `MiniAlign.config.options` defines default values of options used to control
--- behavior of steps.
---
--- Examples:
--- - Set `justify_side = 'center'` to center align at initialization.
---
--- For more details about options see |MiniAlign.align_strings()| and entries of
--- |MiniAlign.gen_step| for default main steps.
---
--- # Steps ~
---
--- `MiniAlign.config.steps` defines default steps to be applied during
--- alignment process.
---
--- Examples:
--- - Align by default only first pair of columns: >lua
---
--- local align = require('mini.align')
--- align.setup({
--- steps = {
--- pre_justify = { align.gen_step.filter('n == 1') }
--- },
--- })
--- <
MiniAlign.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
start = 'ga',
start_with_preview = 'gA',
},
-- Modifiers changing alignment steps and/or options
modifiers = {
-- Main option modifiers
--minidoc_replace_start ['s'] = --<function: enter split pattern>,
['s'] = function(_, opts)
local input = H.user_input('Enter split Lua pattern')
if input == nil then return end
opts.split_pattern = input
end,
--minidoc_replace_end
--minidoc_replace_start ['j'] = --<function: choose justify side>,
['j'] = function(_, opts)
-- stylua: ignore
H.echo({
{ 'Select justify: ', 'ModeMsg' }, { 'l', 'Question' }, { 'eft, ' },
{ 'c', 'Question' }, { 'enter, ' }, { 'r', 'Question' }, { 'ight, ' },
{ 'n', 'Question' }, { 'one' }
})
local ok, char = pcall(vim.fn.getcharstr)
if not ok or char == '\27' then return end
local direction = ({ l = 'left', c = 'center', r = 'right', n = 'none' })[char]
if direction == nil then return end
opts.justify_side = direction
end,
--minidoc_replace_end
--minidoc_replace_start ['m'] = --<function: enter merge delimiter>,
['m'] = function(_, opts)
local input = H.user_input('Enter merge delimiter')
if input == nil then return end
opts.merge_delimiter = input
end,
--minidoc_replace_end
-- Modifiers adding pre-steps
--minidoc_replace_start ['f'] = --<function: filter parts by entering Lua expression>,
['f'] = function(steps, _)
local input = H.user_input('Enter filter expression')
local step = MiniAlign.gen_step.filter(input)
if step == nil then return end
table.insert(steps.pre_justify, step)
end,
--minidoc_replace_end
--minidoc_replace_start ['i'] = --<function: ignore some split matches>,
['i'] = function(steps, _) table.insert(steps.pre_split, MiniAlign.gen_step.ignore_split()) end,
--minidoc_replace_end
--minidoc_replace_start ['p'] = --<function: pair parts>,
['p'] = function(steps, _) table.insert(steps.pre_justify, MiniAlign.gen_step.pair()) end,
--minidoc_replace_end
--minidoc_replace_start ['t'] = --<function: trim parts>,
['t'] = function(steps, _) table.insert(steps.pre_justify, MiniAlign.gen_step.trim()) end,
--minidoc_replace_end
-- Delete some last pre-step
--minidoc_replace_start ['<BS>'] = --<function: delete some last pre-step>,
[vim.api.nvim_replace_termcodes('<BS>', true, true, true)] = function(steps, _)
local has_pre = {}
for _, pre in ipairs({ 'pre_split', 'pre_justify', 'pre_merge' }) do
if #steps[pre] > 0 then table.insert(has_pre, pre) end
end
if #has_pre == 0 then return end
if #has_pre == 1 then
local pre = steps[has_pre[1]]
table.remove(pre, #pre)
return
end
--stylua: ignore
H.echo({
{ 'Select pre-step to remove: ', 'ModeMsg' }, { 's', 'Question' }, { 'plit, ' },
{ 'j', 'Question' }, { 'ustify, ' }, { 'm', 'Question' }, { 'erge' },
})
local ok, char = pcall(vim.fn.getcharstr)
if not ok or char == '\27' then return end
if char == 's' then table.remove(steps.pre_split, #steps.pre_split) end
if char == 'j' then table.remove(steps.pre_justify, #steps.pre_justify) end
if char == 'm' then table.remove(steps.pre_merge, #steps.pre_merge) end
end,
--minidoc_replace_end
-- Special configurations for common splits
--minidoc_replace_start ['='] = --<function: enhanced setup for '='>,
['='] = function(steps, opts)
opts.split_pattern = '%p*=+[<>~]*'
table.insert(steps.pre_justify, MiniAlign.gen_step.trim())
opts.merge_delimiter = ' '
end,
--minidoc_replace_end
--minidoc_replace_start [','] = --<function: enhanced setup for ','>,
[','] = function(steps, opts)
opts.split_pattern = ','
table.insert(steps.pre_justify, MiniAlign.gen_step.trim())
table.insert(steps.pre_justify, MiniAlign.gen_step.pair())
opts.merge_delimiter = ' '
end,
--minidoc_replace_end
--minidoc_replace_start ['|'] = --<function: enhanced setup for '|'>,
['|'] = function(steps, opts)
opts.split_pattern = '|'
table.insert(steps.pre_justify, MiniAlign.gen_step.trim())
opts.merge_delimiter = ' '
end,
--minidoc_replace_end
--minidoc_replace_start [' '] = --<function: enhanced setup for ' '>,
[' '] = function(steps, opts)
table.insert(
steps.pre_split,
MiniAlign.new_step('squash', function(strings)
-- Replace all space sequences with single space (except indent)
for i, s in ipairs(strings) do
strings[i] = s:gsub('()(%s+)', function(n, space) return n == 1 and space or ' ' end)
end
end)
)
-- Don't use `' '` to respect indent
opts.split_pattern = '%s+'
end,
--minidoc_replace_end
},
-- Default options controlling alignment process
options = {
split_pattern = '',
justify_side = 'left',
merge_delimiter = '',
},
-- Default steps performing alignment (if `nil`, default is used)
steps = {
pre_split = {},
split = nil,
pre_justify = {},
justify = nil,
pre_merge = {},
merge = nil,
},
-- 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 =======================================================
--- Align strings
---
--- For details about alignment process see |MiniAlign-algorithm|.
---
---@param strings table Array of strings.
---@param opts table|nil Options. Its copy will be passed to steps as second
--- argument. Extended with `MiniAlign.config.options`.
--- This is a place to control default main steps:
--- - `opts.split_pattern` - Lua pattern(s) used to make split parts.
--- - `opts.split_exclude_patterns` - which split matches should be ignored.
--- - `opts.justify_side` - which direction(s) alignment should be done.
--- - `opts.justify_offsets` - offsets tweaking width of first column
--- - `opts.merge_delimiter` - which delimiter(s) to use when merging.
--- For more information see |MiniAlign.gen_step| entry for corresponding
--- default step.
---@param steps table|nil Steps. Extended with `MiniAlign.config.steps`.
--- Possible `nil` values are replaced with corresponding default steps:
--- - `split` - |MiniAlign.gen_step.default_split()|.
--- - `justify` - |MiniAlign.gen_step.default_justify()|.
--- - `merge` - |MiniAlign.gen_step.default_merge()|.
MiniAlign.align_strings = function(strings, opts, steps)
-- Validate arguments
if not H.is_array_of(strings, H.is_string) then
H.error('First argument of `MiniAlign.align_strings()` should be array of strings.')
end
opts = H.normalize_opts(opts)
steps = H.normalize_steps(steps, 'steps')
-- Make a copy so that modification in place doesn't affect input
strings = vim.deepcopy(strings)
-- Pre split
for _, step in ipairs(steps.pre_split) do
H.apply_step(step, strings, opts, 'pre_split')
end
-- Split
local parts = H.apply_step(steps.split, strings, opts, 'split')
if not H.is_parts(parts) then
if H.can_be_parts(parts) then
parts = MiniAlign.as_parts(parts)
else
H.error('Output of `split` step should be convertible to parts. See `:h MiniAlign.as_parts()`.')
end
end
-- Pre justify
for _, step in ipairs(steps.pre_justify) do
H.apply_step(step, parts, opts, 'pre_justify')
end
-- Justify
H.apply_step(steps.justify, parts, opts, 'justify')
-- Pre merge
for _, step in ipairs(steps.pre_merge) do
H.apply_step(step, parts, opts, 'pre_merge')
end
-- Merge
local new_strings = H.apply_step(steps.merge, parts, opts, 'merge')
if not H.is_array_of(new_strings, H.is_string) then H.error('Output of `merge` step should be array of strings.') end
return new_strings
end
--- Align current region with user-supplied steps
---
--- Mostly designed to be used inside mappings.
---
--- Will use |MiniAlign.align_strings()| and set the following options in `opts`:
--- - `justify_offsets` - array of offsets used to achieve actual alignment of
--- a region. It is non-trivial (not array of zeros) only for charwise
--- selection: offset of first string is computed as width of prefix to the
--- left of region start.
--- - `region` - current affected region (see |MiniAlign-glossary|). Can be
--- used to create more advanced steps.
--- - `mode` - mode of selection (see |MiniAlign-glossary|).
---
---@param mode string Selection mode. One of "char", "line", "block".
MiniAlign.align_user = function(mode)
local modifiers = H.get_config().modifiers
local with_preview = H.cache.with_preview
local opts = H.cache.opts or H.normalize_opts()
local steps = H.cache.steps or H.normalize_steps()
local steps_are_from_cache = H.cache.steps ~= nil
H.cache.region = nil
-- Track if lines were actually changed to properly undo during preview
local lines_were_changed = false
-- Make initial process
lines_were_changed = H.process_current_region(lines_were_changed, mode, opts, steps)
-- Make early return:
-- - If cache is present (enables dot-repeat).
-- - If `split` is not default with no preview (no more information needed).
if steps_are_from_cache or (not with_preview and opts.split_pattern ~= '') then return end
-- Ask user to input modifier id until no more is needed
local n_iter = 0
while true do
-- Get modifier from user
local id = H.user_modifier(with_preview, H.make_status_msg_chunks(opts, steps))
n_iter = n_iter + 1
-- Stop in case user supplied inappropriate modifier id (abort)
-- Also stop in case of too many iterations (guard from infinite cycle)
if id == nil or n_iter > 1000 then
if lines_were_changed then H.undo() end
if n_iter > 1000 then H.echo({ { 'Too many modifiers typed.', 'WarningMsg' } }, true) end
break
end
-- Stop preview after `<CR>` (confirmation)
if with_preview and id == '\r' then break end
-- Apply modifier
local mod = modifiers[id]
if mod == nil then
-- Use supplied identifier as split pattern
opts.split_pattern = vim.pesc(id)
else
-- Modifier should change input `steps` table in place
local ok, out = pcall(mod, steps, opts)
if not ok then
-- Force message to appear for 500ms because it might be overridden by
-- helper status message
local msg = string.format('Modifier %s should be properly callable. Reason: %s', vim.inspect(id), out)
H.echo({ { msg, 'WarningMsg' } }, true)
vim.cmd('redraw')
vim.loop.sleep(500)
end
end
-- Normalize steps and options while validating their correctness
opts = H.normalize_opts(opts)
steps = H.normalize_steps(steps, opts)
-- Process region while tracking if lines were set at least once
local lines_now_changed = H.process_current_region(lines_were_changed, mode, opts, steps)
lines_were_changed = lines_were_changed or lines_now_changed
-- Stop in "no preview" mode right after `split` is defined
if not with_preview and opts.split_pattern ~= '' then break end
end
-- Remove helper status message (if shown)
H.unecho()
end
--- Convert 2d array of strings to parts
---
--- This function verifies if input is a proper 2d array of strings and adds
--- methods to its copy.
---
---@class parts
---
---@field apply function Takes callable `f` and applies it to every part.
--- Callable should have signature `(s, data)`: `s` is a string part,
--- `data` - table with its data (<row> has row number, <col> has column number).
--- Returns new 2d array.
---
---@field apply_inplace function Takes callable `f` and applies it to every part.
--- Should have same signature as in `apply` method. Outputs (should all be
--- strings) are assigned in place to a corresponding parts element. Returns
--- parts itself to enable method chaining.
---
---@field get_dims function Return dimensions of parts array: a table with
--- <row> and <col> keys having number of rows and number of columns (maximum
--- number of elements across all rows).
---
---@field group function Concatenate neighboring strings based on supplied
--- boolean mask and direction (one of "left", default, or "right"). Has
--- signature `(mask, direction)` and modifies parts in place. Returns parts
--- itself to enable method chaining.
--- Example:
--- - Parts: `{ { "a", "b", "c" }, { "d", "e" }, { "f" } }`
--- - Mask: `{ { false, false, true }, { true, false }, { false } }`
--- - Result for direction "left": `{ { "abc" }, { "d", "e" }, { "f" } }`
--- - Result for direction "right": `{ { "ab","c" }, { "de" }, { "f" } }`
---
---@field pair function Concatenate neighboring element pairs. Takes
--- `direction` as input (one of "left", default, or "right") and applies
--- `group()` for an alternating mask.
--- Example:
--- - Parts: `{ { "a", "b", "c" }, { "d", "e" }, { "f" } }`
--- - Result for direction "left": `{ { "ab", "c" }, { "de" }, { "f" } }`
--- - Result for direction "right": `{ { "a", "bc" }, { "de" }, { "f" } }`
---
---@field slice_col function Return column with input index `j`. Note: it might
--- not be an array if rows have unequal number of columns.
---
---@field slice_row function Return row with input index `i`.
---
---@field trim function Trim elements whitespace. Has signature `(direction, indent)`
--- and modifies parts in place. Returns parts itself to enable method chaining.
--- - Possible values of `direction`: "both" (default), "left", "right",
--- "none". Defines from which side whitespaces should be removed.
--- - Possible values of `indent`: "keep" (default), "low", "high", "remove".
--- Defines what to do with possible indent (left whitespace of first string
--- in a row). Value "keep" keeps it; "low" makes all indent equal to the
--- lowest across rows; "high" - highest across rows; "remove" - removes indent.
---
---@usage >lua
--- parts = MiniAlign.as_parts({ { 'a', 'b' }, { 'c' } })
--- print(vim.inspect(parts.get_dims())) -- Should be { row = 2, col = 2 }
---
--- parts.apply_inplace(function(s, data)
--- return ' ' .. data.row .. s .. data.col .. ' '
--- end)
--- print(vim.inspect(parts)) -- Should be { { ' 1a1 ', ' 1b2 ' }, { ' 2c1 ' } }
---
--- parts.trim('both', 'remove').pair()
--- print(vim.inspect(parts)) -- Should be { { '1a11b2' }, { '2c1' } }
--- <
MiniAlign.as_parts = function(arr2d)
local ok, msg = H.can_be_parts(arr2d)
if not ok then H.error('Input of `as_parts()` ' .. msg) end
local parts = vim.deepcopy(arr2d)
local methods = {}
methods.apply = function(f)
local res = {}
for i, row in ipairs(parts) do
res[i] = {}
for j, s in ipairs(row) do
res[i][j] = f(s, { row = i, col = j })
end
end
return res
end
methods.apply_inplace = function(f)
for i, row in ipairs(parts) do
for j, s in ipairs(row) do
local new_val = f(s, { row = i, col = j })
if type(new_val) ~= 'string' then H.error('Input of `apply_inplace()` method should always return string.') end
parts[i][j] = new_val
end
end
return parts
end
methods.get_dims = function()
local n_cols = 0
for _, row in ipairs(parts) do
n_cols = math.max(n_cols, #row)
end
return { row = #parts, col = n_cols }
end
-- Group cells into single string based on boolean mask.
-- Can be used for filtering separators and sticking separator to its part.
methods.group = function(mask, direction)
direction = direction or 'left'
for i, row in ipairs(parts) do
local group_tables = H.group_by_mask(row, mask[i], direction)
parts[i] = vim.tbl_map(table.concat, group_tables)
end
return parts
end
methods.pair = function(direction)
direction = direction or 'left'
local mask = {}
for i, row in ipairs(parts) do
mask[i] = {}
for j, _ in ipairs(row) do
-- Count from corresponding end
local num = direction == 'left' and j or (#row - j + 1)
mask[i][j] = num % 2 == 0
end
end
parts.group(mask, direction)
return parts
end
-- NOTE: output might not be an array (some rows can not have input column)
-- Use `vim.tbl_keys()` and `vim.tbl_values()`
methods.slice_col = function(j)
return vim.tbl_map(function(row) return row[j] end, parts)
end
methods.slice_row = function(i) return parts[i] or {} end
methods.trim = function(direction, indent)
direction = direction or 'both'
indent = indent or 'keep'
-- Verify arguments
local trim_fun = H.trim_functions[direction]
if not vim.is_callable(trim_fun) then
local allowed = vim.tbl_map(vim.inspect, vim.tbl_keys(H.trim_functions))
table.sort(allowed)
H.error('`direction` should be one of ' .. table.concat(allowed, ', ') .. '.')
end
local indent_fun = H.indent_functions[indent]
if not vim.is_callable(indent_fun) then
local allowed = vim.tbl_map(vim.inspect, vim.tbl_keys(H.indent_functions))
table.sort(allowed)
H.error('`indent` should be one of ' .. table.concat(allowed, ', ') .. '.')
end
-- Compute indentation to restore later
local row_indent = vim.tbl_map(function(row) return row[1]:match('^(%s*)') end, parts)
row_indent = indent_fun(row_indent)
-- Trim
parts.apply_inplace(trim_fun)
-- Restore indentation if it was removed
if vim.tbl_contains({ 'both', 'left' }, direction) then
for i, row in ipairs(parts) do
row[1] = string.format('%s%s', row_indent[i], row[1])
end
end
return parts
end
return setmetatable(parts, { class = 'parts', __index = methods })
end
0