1

Update generated neovim config

This commit is contained in:
2024-08-15 14:28:54 +02:00
parent 07409c223d
commit 25cfcf2941
3809 changed files with 351157 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,766 @@
--- *mini.basics* Common configuration presets
--- *MiniBasics*
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Install, create 'init.lua', add `require('mini.basics').setup()` and you
--- are good to go.
---
--- Features:
--- - Presets for common options. It will only change option if it wasn't
--- manually set before. See more in |MiniBasics.config.options|.
---
--- - Presets for common mappings. It will only add a mapping if it wasn't
--- manually created before. See more in |MiniBasics.config.mappings|.
---
--- - Presets for common autocommands. See more in |MiniBasics.config.autocommands|.
---
--- - Reverse compatibility is a high priority. Any decision to change already
--- present behavior will be made with great care.
---
--- Notes:
--- - Main goal of this module is to provide a relatively easier way for
--- new-ish Neovim users to have better "works out of the box" experience
--- while having documented relevant options/mappings/autocommands to study.
--- It is based partially on survey among Neovim users and partially is
--- coming from personal preferences.
---
--- However, more seasoned users almost surely will find something useful.
---
--- Still, it is recommended to read about used options/mappings/autocommands
--- and decide if they are needed. The main way to do that is by reading
--- Neovim's help pages (linked in help file) and this module's source code
--- (thoroughly documented for easier comprehension).
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.basics').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniBasics`
--- which you can use for scripting or manually (with `:lua MiniBasics.*`).
---
--- See |MiniBasics.config| for available config settings.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Comparisons ~
---
--- - 'tpope/vim-sensible':
--- - Most of 'tpope/vim-sensible' is already incorporated as default
--- options in Neovim (see |nvim-default|). This module has a much
--- broader effect.
--- - 'tpope/vim-unimpaired':
--- - The 'tpope/vim-unimpaired' has mapping for toggling options with `yo`
--- prefix. This module implements similar functionality with `\` prefix
--- (see |MiniBasics.config.mappings|).
---@diagnostic disable:undefined-field
-- To study source behind presets, search for:
-- - `-- Options ---` for `config.options`.
-- - `-- Mappings ---` for `config.mappings`.
-- - `-- Autocommands ---` for `config.autocommands`.
-- Module definition ==========================================================
local MiniBasics = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniBasics.config|.
---
---@usage >lua
--- require('mini.basics').setup() -- use default config
--- -- OR
--- require('mini.basics').setup({}) -- replace {} with your config table
--- <
MiniBasics.setup = function(config)
-- Export module
_G.MiniBasics = MiniBasics
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text *MiniBasics.config.options*
--- # Options ~
---
--- Usage example: >lua
---
--- require('mini.basics').setup({
--- options = {
--- basic = true,
--- extra_ui = true,
--- win_borders = 'double',
--- }
--- })
--- <
--- ## options.basic ~
---
--- The `config.options.basic` sets certain options to values which are quite
--- commonly used (judging by study of available Neovim pre-configurations,
--- public dotfiles, and surveys).
--- Any option is changed only if it was not set manually beforehand.
--- For exact changes, please see source code ('lua/mini/basics.lua').
---
--- Here is the list of affected options (put cursor on it and press |CTRL-]|):
--- - General:
--- - Sets |<Leader>| key to |<Space>|. Be sure to make all Leader mappings
--- after this (otherwise they are made with default <Leader>).
--- - Runs `:filetype plugin indent on` (see |:filetype-overview|)
--- - |backup|
--- - |mouse|
--- - |undofile|
--- - |writebackup|
--- - Appearance
--- - |breakindent|
--- - |cursorline|
--- - |fillchars|
--- - |linebreak|
--- - |number|
--- - |ruler|
--- - |showmode|
--- - |signcolumn|
--- - |shortmess|
--- - |splitbelow|
--- - |splitkeep| (on Neovim>=0.9)
--- - |splitright|
--- - |termguicolors| (on Neovim<0.10; later versions have it smartly enabled)
--- - |wrap|
--- - Editing
--- - |completeopt|
--- - |formatoptions|
--- - |ignorecase|
--- - |incsearch|
--- - |infercase|
--- - |smartcase|
--- - |smartindent|
--- - |virtualedit|
---
--- ## options.extra_ui ~
---
--- The `config.options.extra_ui` sets certain options for visual appearance
--- which might not be aligned with common preferences, but still worth trying.
--- Any option is changed only if it was not set manually beforehand.
--- For exact changes, please see source code ('lua/mini/basics.lua').
---
--- List of affected options:
--- - |list|
--- - |listchars|
--- - |pumblend|
--- - |pumheight|
--- - |winblend|
--- - Runs `:syntax on` (see |:syntax-on|)
---
--- ## options.win_borders
---
--- The `config.options.win_borders` updates |fillchars| to have a consistent set of
--- characters for window border (`vert`, `horiz`, etc.).
---
--- Available values:
--- - `'bold'` - bold lines.
--- - `'dot'` - dot in every cell.
--- - `'double'` - double line.
--- - `'single'` - single line.
--- - `'solid'` - no symbol, only background.
---
--- *MiniBasics.config.mappings*
--- # Mappings ~
---
--- Usage example: >lua
---
--- require('mini.basics').setup({
--- mappings = {
--- basic = true,
--- option_toggle_prefix = [[\]],
--- windows = true,
--- move_with_alt = true,
--- }
--- })
--- <
--- If you don't want only some mappings to be made at all, use |vim.keymap.del()|
--- after calling |MiniBasics.setup()|.
---
--- ## mappings.basic ~
---
--- The `config.mappings.basic` creates mappings for certain commonly mapped actions
--- (judging by study of available Neovim pre-configurations and public dotfiles).
---
--- Some of the mappings override built-in ones to either improve their
--- behavior or override its default not very useful action.
--- It will only add a mapping if it wasn't manually created before.
---
--- Here is a table with created mappings : >
---
--- |Keys | Modes | Description |
--- |-------|-----------------|-----------------------------------------------|
--- | j | Normal, Visual | Move down by visible lines with no [count] |
--- | k | Normal, Visual | Move up by visible lines with no [count] |
--- | go | Normal | Add [count] empty lines after cursor |
--- | gO | Normal | Add [count] empty lines before cursor |
--- | gy | Normal, Visual | Copy to system clipboard |
--- | gp | Normal, Visual | Paste from system clipboard |
--- | gV | Normal | Visually select latest changed or yanked text |
--- | g/ | Visual | Search inside current visual selection |
--- | * | Visual | Search forward for current visual selection |
--- | # | Visual | Search backward for current visual selection |
--- | <C-s> | Normal, Visual, | Save and go to Normal mode |
--- | | Insert | |
--- <
--- Notes:
--- - See |[count]| for its meaning.
--- - On Neovim>=0.10 mappings for `#` and `*` are not created as their
--- enhanced variants are made built-in. See |v_star-default| and |v_#-default|.
---
--- ## mappings.option_toggle_prefix ~
---
--- The `config.mappings.option_toggle_prefix` defines a prefix used for
--- creating mappings that toggle common options. The result mappings will be
--- `<prefix> + <suffix>`. For example, with default value, `\w` will toggle |wrap|.
---
--- Other viable choices for prefix are
--- - `,` (as a mnemonic for several values to toggle).
--- - `|` (as a same mnemonic).
--- - `yo` (used in 'tpope/vim-unimpaired')
--- - Something with |<Leader>| key, like `<Leader>t` (`t` for "toggle"). Note:
--- if your prefix contains `<Leader>` key, make sure to set it before
--- calling |MiniBasics.setup()| (as is done with default `basic` field of
--- |MiniBasics.config.options|).
---
--- After toggling, there will be a feedback about the current option value if
--- prior to `require('mini.basics').setup()` module wasn't silenced (see
--- "Silencing" section in |mini.basics|).
---
--- It will only add a mapping if it wasn't manually created before.
---
--- Here is a list of suffixes for created toggling mappings (all in Normal mode):
---
--- - `b` - |'background'|.
--- - `c` - |'cursorline'|.
--- - `C` - |'cursorcolumn'|.
--- - `d` - diagnostic (via |vim.diagnostic| functions).
--- - `h` - |'hlsearch'| (or |v:hlsearch| to be precise).
--- - `i` - |'ignorecase'|.
--- - `l` - |'list'|.
--- - `n` - |'number'|.
--- - `r` - |'relativenumber'|.
--- - `s` - |'spell'|.
--- - `w` - |'wrap'|.
---
--- ## mappings.windows ~
---
--- The `config.mappings.windows` creates mappings for easiere window manipulation.
---
--- It will only add a mapping if it wasn't manually created before.
---
--- Here is a list with created Normal mode mappings (all mappings respect |[count]|):
--- - Window navigation:
--- - `<C-h>` - focus on left window (see |CTRL-W_H|).
--- - `<C-j>` - focus on below window (see |CTRL-W_J|).
--- - `<C-k>` - focus on above window (see |CTRL-W_K|).
--- - `<C-l>` - focus on right window (see |CTRL-W_L|).
--- - Window resize (all use arrow keys; variants of |resize|; all respect |[count]|):
--- - `<C-left>` - decrease window width.
--- - `<C-down>` - decrease window height.
--- - `<C-up>` - increase window height.
--- - `<C-right>` - increase window width.
---
--- ## mappings.move_with_alt
---
--- The `config.mappings.move_with_alt` creates mappings for a more consistent
--- cursor move in Insert, Command, and Terminal modes. For example, it proves
--- useful in combination of autopair plugin (like |MiniPairs|) to move right
--- outside of inserted pairs (no matter what the pair is).
---
--- It will only add a mapping if it wasn't manually created before.
---
--- Here is a list of created mappings (`<M-x>` means `Alt`/`Meta` plus `x`):
--- - `<M-h>` - move cursor left. Modes: Insert, Terminal, Command.
--- - `<M-j>` - move cursor down. Modes: Insert, Terminal.
--- - `<M-k>` - move cursor up. Modes: Insert, Terminal.
--- - `<M-l>` - move cursor right. Modes: Insert, Terminal, Command.
---
--- *MiniBasics.config.autocommands*
--- # Autocommands ~
---
--- Usage example: >lua
---
--- require('mini.basics').setup({
--- autocommands = {
--- basic = true,
--- relnum_in_visual_mode = true,
--- }
--- })
--- <
--- ## autocommands.basic ~
---
--- The `config.autocommands.basic` creates some common autocommands:
---
--- - Starts insert mode when opening terminal (see |startinsert| and |TermOpen|).
--- - Highlights yanked text for a brief period of time (see
--- |vim.highlight.on_yank()| and |TextYankPost|).
---
--- ## autocommands.relnum_in_visual_mode ~
---
--- The `config.autocommands.relnum_in_visual_mode` creates autocommands that
--- enable |relativenumber| in linewise and blockwise Visual modes and disable
--- otherwise. See |ModeChanged|.
MiniBasics.config = {
-- Options. Set to `false` to disable.
options = {
-- Basic options ('number', 'ignorecase', and many more)
basic = true,
-- Extra UI features ('winblend', 'cmdheight=0', ...)
extra_ui = false,
-- Presets for window borders ('single', 'double', ...)
win_borders = 'default',
},
-- Mappings. Set to `false` to disable.
mappings = {
-- Basic mappings (better 'jk', save with Ctrl+S, ...)
basic = true,
-- Prefix for mappings that toggle common options ('wrap', 'spell', ...).
-- Supply empty string to not create these mappings.
option_toggle_prefix = [[\]],
-- Window navigation with <C-hjkl>, resize with <C-arrow>
windows = false,
-- Move cursor in Insert, Command, and Terminal mode with <M-hjkl>
move_with_alt = false,
},
-- Autocommands. Set to `false` to disable
autocommands = {
-- Basic autocommands (highlight on yank, start Insert in terminal, ...)
basic = true,
-- Set 'relativenumber' only in linewise and blockwise Visual mode
relnum_in_visual_mode = false,
},
-- Whether to disable showing non-error feedback
silent = false,
}
--minidoc_afterlines_end
--- Toggle diagnostic for current buffer
---
--- This uses |vim.diagnostic| functions per buffer.
---
---@return string String indicator for new state. Similar to what |:set| `{option}?` shows.
MiniBasics.toggle_diagnostic = function()
local buf_id = vim.api.nvim_get_current_buf()
local is_enabled = H.diagnostic_is_enabled(buf_id)
local f
if vim.fn.has('nvim-0.10') == 1 then
f = function(bufnr) vim.diagnostic.enable(not is_enabled, { bufnr = bufnr }) end
else
f = is_enabled and vim.diagnostic.disable or vim.diagnostic.enable
end
f(buf_id)
local new_buf_state = not is_enabled
H.buffer_diagnostic_state[buf_id] = new_buf_state
return new_buf_state and ' diagnostic' or 'nodiagnostic'
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniBasics.config)
-- Diagnostic state per buffer
H.buffer_diagnostic_state = {}
-- 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({
options = { config.options, 'table' },
mappings = { config.mappings, 'table' },
autocommands = { config.autocommands, 'table' },
})
vim.validate({
['options.basic'] = { config.options.basic, 'boolean' },
['options.extra_ui'] = { config.options.extra_ui, 'boolean' },
['options.win_borders'] = { config.options.win_borders, 'string' },
['mappings.basic'] = { config.mappings.basic, 'boolean' },
['mappings.option_toggle_prefix'] = { config.mappings.option_toggle_prefix, 'string' },
['mappings.windows'] = { config.mappings.windows, 'boolean' },
['mappings.move_with_alt'] = { config.mappings.move_with_alt, 'boolean' },
['autocommands.basic'] = { config.autocommands.basic, 'boolean' },
['autocommands.relnum_in_visual_mode'] = { config.autocommands.relnum_in_visual_mode, 'boolean' },
['silent'] = { config.silent, 'boolean' },
})
return config
end
H.apply_config = function(config)
MiniBasics.config = config
H.apply_options(config)
H.apply_mappings(config)
H.apply_autocommands(config)
end
-- Options --------------------------------------------------------------------
--stylua: ignore
H.apply_options = function(config)
-- Use `local o, opt = vim.o, vim.opt` to copy lines as is.
-- Or use `vim.o` and `vim.opt` directly.
local o, opt = H.vim_o, H.vim_opt
-- Basic options
if config.options.basic then
-- Leader key
if vim.g.mapleader == nil then
vim.g.mapleader = ' ' -- Use space as the one and only true Leader key
end
-- General
o.undofile = true -- Enable persistent undo (see also `:h undodir`)
o.backup = false -- Don't store backup while overwriting the file
o.writebackup = false -- Don't store backup while overwriting the file
o.mouse = 'a' -- Enable mouse for all available modes
vim.cmd('filetype plugin indent on') -- Enable all filetype plugins
-- Appearance
o.breakindent = true -- Indent wrapped lines to match line start
o.cursorline = true -- Highlight current line
o.linebreak = true -- Wrap long lines at 'breakat' (if 'wrap' is set)
o.number = true -- Show line numbers
o.splitbelow = true -- Horizontal splits will be below
o.splitright = true -- Vertical splits will be to the right
o.ruler = false -- Don't show cursor position in command line
o.showmode = false -- Don't show mode in command line
o.wrap = false -- Display long lines as just one line
o.signcolumn = 'yes' -- Always show sign column (otherwise it will shift text)
o.fillchars = 'eob: ' -- Don't show `~` outside of buffer
-- Editing
o.ignorecase = true -- Ignore case when searching (use `\C` to force not doing that)
o.incsearch = true -- Show search results while typing
o.infercase = true -- Infer letter cases for a richer built-in keyword completion
o.smartcase = true -- Don't ignore case when searching if pattern has upper case
o.smartindent = true -- Make indenting smart
o.completeopt = 'menuone,noinsert,noselect' -- Customize completions
o.virtualedit = 'block' -- Allow going past the end of line in visual block mode
o.formatoptions = 'qjl1' -- Don't autoformat comments
-- Neovim version dependent
if vim.fn.has('nvim-0.9') == 1 then
opt.shortmess:append('WcC') -- Reduce command line messages
o.splitkeep = 'screen' -- Reduce scroll during window split
else
opt.shortmess:append('Wc') -- Reduce command line messages
end
if vim.fn.has('nvim-0.10') == 0 then
o.termguicolors = true -- Enable gui colors
end
end
-- Some opinioneted extra UI options
if config.options.extra_ui then
o.pumblend = 10 -- Make builtin completion menus slightly transparent
o.pumheight = 10 -- Make popup menu smaller
o.winblend = 10 -- Make floating windows slightly transparent
-- NOTE: Having `tab` present is needed because `^I` will be shown if
-- omitted (documented in `:h listchars`).
-- Having it equal to a default value should be less intrusive.
o.listchars = 'tab:> ,extends:…,precedes:…,nbsp:␣' -- Define which helper symbols to show
o.list = true -- Show some helper symbols
-- Enable syntax highlighting if it wasn't already (as it is time consuming)
if vim.fn.exists("syntax_on") ~= 1 then vim.cmd([[syntax enable]]) end
end
-- Use some common window borders presets
local border_chars = H.win_borders_fillchars[config.options.win_borders]
if border_chars ~= nil then
vim.opt.fillchars:append(border_chars)
end
end
H.vim_o = setmetatable({}, {
__newindex = function(_, name, value)
local was_set = vim.api.nvim_get_option_info(name).was_set
if was_set then return end
vim.o[name] = value
end,
})
H.vim_opt = setmetatable({}, {
__index = function(_, name)
local was_set = vim.api.nvim_get_option_info(name).was_set
if was_set then return { append = function() end, remove = function() end } end
return vim.opt[name]
end,
})
--stylua: ignore
H.win_borders_fillchars = {
bold = 'vert:┃,horiz:━,horizdown:┳,horizup:┻,verthoriz:╋,vertleft:┫,vertright:┣',
dot = 'vert:·,horiz:·,horizdown:·,horizup:·,verthoriz:·,vertleft:·,vertright:·',
double = 'vert:║,horiz:═,horizdown:╦,horizup:╩,verthoriz:╬,vertleft:╣,vertright:╠',
single = 'vert:│,horiz:─,horizdown:┬,horizup:┴,verthoriz:┼,vertleft:┤,vertright:├',
solid = 'vert: ,horiz: ,horizdown: ,horizup: ,verthoriz: ,vertleft: ,vertright: ',
}
-- Mappings -------------------------------------------------------------------
--stylua: ignore
H.apply_mappings = function(config)
-- Use `local map = vim.keymap.set` to copy lines as is. Or use it directly.
local map = H.keymap_set
if config.mappings.basic then
-- Move by visible lines. Notes:
-- - Don't map in Operator-pending mode because it severely changes behavior:
-- like `dj` on non-wrapped line will not delete it.
-- - Condition on `v:count == 0` to allow easier use of relative line numbers.
map({ 'n', 'x' }, 'j', [[v:count == 0 ? 'gj' : 'j']], { expr = true })
map({ 'n', 'x' }, 'k', [[v:count == 0 ? 'gk' : 'k']], { expr = true })
-- Add empty lines before and after cursor line supporting dot-repeat
MiniBasics.put_empty_line = function(put_above)
-- This has a typical workflow for enabling dot-repeat:
-- - On first call it sets `operatorfunc`, caches data, and calls
-- `operatorfunc` on current cursor position.
-- - On second call it performs task: puts `v:count1` empty lines
-- above/below current line.
if type(put_above) == 'boolean' then
vim.o.operatorfunc = 'v:lua.MiniBasics.put_empty_line'
MiniBasics.cache_empty_line = { put_above = put_above }
return 'g@l'
end
local target_line = vim.fn.line('.') - (MiniBasics.cache_empty_line.put_above and 1 or 0)
vim.fn.append(target_line, vim.fn['repeat']({ '' }, vim.v.count1))
end
-- NOTE: if you don't want to support dot-repeat, use this snippet:
-- ```
-- map('n', 'gO', "<Cmd>call append(line('.') - 1, repeat([''], v:count1))<CR>")
-- map('n', 'go', "<Cmd>call append(line('.'), repeat([''], v:count1))<CR>")
-- ```
map('n', 'gO', 'v:lua.MiniBasics.put_empty_line(v:true)', { expr = true, desc = 'Put empty line above' })
map('n', 'go', 'v:lua.MiniBasics.put_empty_line(v:false)', { expr = true, desc = 'Put empty line below' })
-- Copy/paste with system clipboard
map({ 'n', 'x' }, 'gy', '"+y', { desc = 'Copy to system clipboard' })
map( 'n', 'gp', '"+p', { desc = 'Paste from system clipboard' })
-- - Paste in Visual with `P` to not copy selected text (`:h v_P`)
map( 'x', 'gp', '"+P', { desc = 'Paste from system clipboard' })
-- Reselect latest changed, put, or yanked text
map('n', 'gV', '"`[" . strpart(getregtype(), 0, 1) . "`]"', { expr = true, replace_keycodes = false, desc = 'Visually select changed text' })
-- Search inside visually highlighted text. Use `silent = false` for it to
-- make effect immediately.
map('x', 'g/', '<esc>/\\%V', { silent = false, desc = 'Search inside visual selection' })
-- Search visually selected text (slightly better than builtins in
-- Neovim>=0.8 but slightly worse than builtins in Neovim>=0.10)
-- TODO: Remove this after compatibility with Neovim=0.9 is dropped
if vim.fn.has('nvim-0.10') == 0 then
map('x', '*', [[y/\V<C-R>=escape(@", '/\')<CR><CR>]], { desc = 'Search forward' })
map('x', '#', [[y?\V<C-R>=escape(@", '?\')<CR><CR>]], { desc = 'Search backward' })
end
-- Alternative way to save and exit in Normal mode.
-- NOTE: Adding `redraw` helps with `cmdheight=0` if buffer is not modified
map( 'n', '<C-S>', '<Cmd>silent! update | redraw<CR>', { desc = 'Save' })
map({ 'i', 'x' }, '<C-S>', '<Esc><Cmd>silent! update | redraw<CR>', { desc = 'Save and go to Normal mode' })
end
local toggle_prefix = config.mappings.option_toggle_prefix
if type(toggle_prefix) == 'string' and toggle_prefix ~= '' then
local map_toggle = function(lhs, rhs, desc) map('n', toggle_prefix .. lhs, rhs, { desc = desc }) end
if config.silent then
-- Toggle without feedback
map_toggle('b', '<Cmd>lua vim.o.bg = vim.o.bg == "dark" and "light" or "dark"<CR>', "Toggle 'background'")
map_toggle('c', '<Cmd>setlocal cursorline!<CR>', "Toggle 'cursorline'")
map_toggle('C', '<Cmd>setlocal cursorcolumn!<CR>', "Toggle 'cursorcolumn'")
map_toggle('d', '<Cmd>lua MiniBasics.toggle_diagnostic()<CR>', 'Toggle diagnostic')
map_toggle('h', '<Cmd>let v:hlsearch = 1 - v:hlsearch<CR>', 'Toggle search highlight')
map_toggle('i', '<Cmd>setlocal ignorecase!<CR>', "Toggle 'ignorecase'")
map_toggle('l', '<Cmd>setlocal list!<CR>', "Toggle 'list'")
map_toggle('n', '<Cmd>setlocal number!<CR>', "Toggle 'number'")
map_toggle('r', '<Cmd>setlocal relativenumber!<CR>', "Toggle 'relativenumber'")
map_toggle('s', '<Cmd>setlocal spell!<CR>', "Toggle 'spell'")
map_toggle('w', '<Cmd>setlocal wrap!<CR>', "Toggle 'wrap'")
else
map_toggle('b', '<Cmd>lua vim.o.bg = vim.o.bg == "dark" and "light" or "dark"; print(vim.o.bg)<CR>', "Toggle 'background'")
map_toggle('c', '<Cmd>setlocal cursorline! cursorline?<CR>', "Toggle 'cursorline'")
map_toggle('C', '<Cmd>setlocal cursorcolumn! cursorcolumn?<CR>', "Toggle 'cursorcolumn'")
map_toggle('d', '<Cmd>lua print(MiniBasics.toggle_diagnostic())<CR>', 'Toggle diagnostic')
map_toggle('h', '<Cmd>let v:hlsearch = 1 - v:hlsearch | echo (v:hlsearch ? " " : "no") . "hlsearch"<CR>', 'Toggle search highlight')
map_toggle('i', '<Cmd>setlocal ignorecase! ignorecase?<CR>', "Toggle 'ignorecase'")
map_toggle('l', '<Cmd>setlocal list! list?<CR>', "Toggle 'list'")
map_toggle('n', '<Cmd>setlocal number! number?<CR>', "Toggle 'number'")
map_toggle('r', '<Cmd>setlocal relativenumber! relativenumber?<CR>', "Toggle 'relativenumber'")
map_toggle('s', '<Cmd>setlocal spell! spell?<CR>', "Toggle 'spell'")
map_toggle('w', '<Cmd>setlocal wrap! wrap?<CR>', "Toggle 'wrap'")
end
end
if config.mappings.windows then
-- Window navigation
map('n', '<C-H>', '<C-w>h', { desc = 'Focus on left window' })
map('n', '<C-J>', '<C-w>j', { desc = 'Focus on below window' })
map('n', '<C-K>', '<C-w>k', { desc = 'Focus on above window' })
map('n', '<C-L>', '<C-w>l', { desc = 'Focus on right window' })
-- Window resize (respecting `v:count`)
map('n', '<C-Left>', '"<Cmd>vertical resize -" . v:count1 . "<CR>"', { expr = true, replace_keycodes = false, desc = 'Decrease window width' })
map('n', '<C-Down>', '"<Cmd>resize -" . v:count1 . "<CR>"', { expr = true, replace_keycodes = false, desc = 'Decrease window height' })
map('n', '<C-Up>', '"<Cmd>resize +" . v:count1 . "<CR>"', { expr = true, replace_keycodes = false, desc = 'Increase window height' })
map('n', '<C-Right>', '"<Cmd>vertical resize +" . v:count1 . "<CR>"', { expr = true, replace_keycodes = false, desc = 'Increase window width' })
end
if config.mappings.move_with_alt then
-- Move only sideways in command mode. Using `silent = false` makes movements
-- to be immediately shown.
map('c', '<M-h>', '<Left>', { silent = false, desc = 'Left' })
map('c', '<M-l>', '<Right>', { silent = false, desc = 'Right' })
-- Don't `noremap` in insert mode to have these keybindings behave exactly
-- like arrows (crucial inside TelescopePrompt)
map('i', '<M-h>', '<Left>', { noremap = false, desc = 'Left' })
map('i', '<M-j>', '<Down>', { noremap = false, desc = 'Down' })
map('i', '<M-k>', '<Up>', { noremap = false, desc = 'Up' })
map('i', '<M-l>', '<Right>', { noremap = false, desc = 'Right' })
map('t', '<M-h>', '<Left>', { desc = 'Left' })
map('t', '<M-j>', '<Down>', { desc = 'Down' })
map('t', '<M-k>', '<Up>', { desc = 'Up' })
map('t', '<M-l>', '<Right>', { desc = 'Right' })
end
end
H.keymap_set = function(modes, lhs, rhs, opts)
-- NOTE: Use `<C-H>`, `<C-Up>`, `<M-h>` casing (instead of `<C-h>`, `<C-up>`,
-- `<M-H>`) to match the `lhs` of keymap info. Otherwise it will say that
-- mapping doesn't exist when in fact it does.
if type(modes) == 'string' then modes = { modes } end
for _, mode in ipairs(modes) do
-- Don't map if mapping is already set **globally**
local map_info = H.get_map_info(mode, lhs)
if not H.is_default_keymap(mode, lhs, map_info) then return end
-- Map
H.map(mode, lhs, rhs, opts)
end
end
H.is_default_keymap = function(mode, lhs, map_info)
if map_info == nil then return true end
local rhs, desc = map_info.rhs or '', map_info.desc or ''
-- Some mappings are set by default in Neovim
if mode == 'n' and lhs == '<C-L>' then return rhs:find('nohl') ~= nil end
if mode == 'i' and lhs == '<C-S>' then return desc:find('signature') ~= nil end
if mode == 'x' and lhs == '*' then return rhs == [[y/\V<C-R>"<CR>]] end
if mode == 'x' and lhs == '#' then return rhs == [[y?\V<C-R>"<CR>]] end
end
H.get_map_info = function(mode, lhs)
local keymaps = vim.api.nvim_get_keymap(mode)
for _, info in ipairs(keymaps) do
if info.lhs == lhs then return info end
end
end
-- Autocommands ---------------------------------------------------------------
H.apply_autocommands = function(config)
local augroup = vim.api.nvim_create_augroup('MiniBasicsAutocommands', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
if config.autocommands.basic then
au('TextYankPost', '*', function() vim.highlight.on_yank() end, 'Highlight yanked text')
local start_terminal_insert = vim.schedule_wrap(function(data)
-- Try to start terminal mode only if target terminal is current
if not (vim.api.nvim_get_current_buf() == data.buf and vim.bo.buftype == 'terminal') then return end
vim.cmd('startinsert')
end)
au('TermOpen', 'term://*', start_terminal_insert, 'Start builtin terminal in Insert mode')
end
if config.autocommands.relnum_in_visual_mode then
au(
'ModeChanged',
-- Show relative numbers only when they matter (linewise and blockwise
-- selection) and 'number' is set (avoids horizontal flickering)
'*:[V\x16]*',
function() vim.wo.relativenumber = vim.wo.number end,
'Show relative line numbers'
)
au(
'ModeChanged',
'[V\x16]*:*',
-- Hide relative numbers when neither linewise/blockwise mode is on
function() vim.wo.relativenumber = string.find(vim.fn.mode(), '^[V\22]') ~= nil end,
'Hide relative line numbers'
)
end
end
-- Utilities ------------------------------------------------------------------
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
if vim.fn.has('nvim-0.10') == 1 then
H.diagnostic_is_enabled = function(buf_id) return vim.diagnostic.is_enabled({ bufnr = buf_id }) end
elseif vim.fn.has('nvim-0.9') == 1 then
H.diagnostic_is_enabled = function(buf_id) return not vim.diagnostic.is_disabled(buf_id) end
else
H.diagnostic_is_enabled = function(buf_id)
local res = H.buffer_diagnostic_state[buf_id]
if res == nil then res = true end
return res
end
end
return MiniBasics

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,284 @@
--- *mini.bufremove* Remove buffers
--- *MiniBufremove*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Unshow, delete, and wipeout buffer while saving window layout
--- (opposite to builtin Neovim's commands).
---
--- # Setup ~
---
--- This module doesn't need setup, but it can be done to improve usability.
--- Setup with `require('mini.bufremove').setup({})` (replace `{}` with your
--- `config` table). It will create global Lua table `MiniBufremove` which you
--- can use for scripting or manually (with `:lua MiniBufremove.*`).
---
--- See |MiniBufremove.config| for `config` structure and default values.
---
--- This module doesn't have runtime options, so using `vim.b.minibufremove_config`
--- will have no effect here.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Notes ~
---
--- 1. Which buffer to show in window(s) after its current buffer is removed is
--- decided by the algorithm:
--- - If alternate buffer (see |CTRL-^|) is listed (see |buflisted()|), use it.
--- - If previous listed buffer (see |bprevious|) is different, use it.
--- - Otherwise create a new one with `nvim_create_buf(true, false)` and use it.
---
--- # Disabling ~
---
--- To disable core functionality, set `vim.g.minibufremove_disable` (globally) or
--- `vim.b.minibufremove_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.
---@alias __bufremove_return boolean|nil Whether operation was successful. If `nil`, no operation was done.
---@alias __bufremove_buf_id number|nil Buffer identifier (see |bufnr()|) to use.
--- Default: 0 for current.
---@alias __bufremove_force boolean|nil Whether to ignore unsaved changes (using `!` version of
--- command). If `false`, calling with unsaved changes will prompt confirm dialog.
--- Default: `false`.
-- Module definition ==========================================================
local MiniBufremove = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniBufremove.config|.
---
---@usage >lua
--- require('mini.bufremove').setup() -- use default config
--- -- OR
--- require('mini.bufremove').setup({}) -- replace {} with your config table
--- <
MiniBufremove.setup = function(config)
-- Export module
_G.MiniBufremove = MiniBufremove
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniBufremove.config = {
-- Whether to set Vim's settings for buffers (allow hidden buffers)
set_vim_settings = true,
-- Whether to disable showing non-error feedback
silent = false,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Delete buffer `buf_id` with |:bdelete| after unshowing it
---
---@param buf_id __bufremove_buf_id
---@param force __bufremove_force
---
---@return __bufremove_return
MiniBufremove.delete = function(buf_id, force)
if H.is_disabled() then return end
return H.unshow_and_cmd(buf_id, force, 'bdelete')
end
--- Wipeout buffer `buf_id` with |:bwipeout| after unshowing it
---
---@param buf_id __bufremove_buf_id
---@param force __bufremove_force
---
---@return __bufremove_return
MiniBufremove.wipeout = function(buf_id, force)
if H.is_disabled() then return end
return H.unshow_and_cmd(buf_id, force, 'bwipeout')
end
--- Stop showing buffer `buf_id` in all windows
---
---@param buf_id __bufremove_buf_id
---
---@return __bufremove_return
MiniBufremove.unshow = function(buf_id)
if H.is_disabled() then return end
buf_id = H.normalize_buf_id(buf_id)
if not H.is_valid_id(buf_id, 'buffer') then return false end
vim.tbl_map(MiniBufremove.unshow_in_window, vim.fn.win_findbuf(buf_id))
return true
end
--- Stop showing current buffer of window `win_id`
---
--- Notes:
--- - If `win_id` represents |cmdline-window|, this function will close it.
---
---@param win_id number|nil Window identifier (see |win_getid()|) to use.
--- Default: 0 for current.
---
---@return __bufremove_return
MiniBufremove.unshow_in_window = function(win_id)
if H.is_disabled() then return nil end
win_id = (win_id == nil) and 0 or win_id
if not H.is_valid_id(win_id, 'window') then return false end
local cur_buf = vim.api.nvim_win_get_buf(win_id)
-- Temporary use window `win_id` as current to have Vim's functions working
vim.api.nvim_win_call(win_id, function()
if vim.fn.getcmdwintype() ~= '' then
vim.cmd('close!')
return
end
-- Try using alternate buffer
local alt_buf = vim.fn.bufnr('#')
if alt_buf ~= cur_buf and vim.fn.buflisted(alt_buf) == 1 then
vim.api.nvim_win_set_buf(win_id, alt_buf)
return
end
-- Try using previous buffer
local has_previous = pcall(vim.cmd, 'bprevious')
if has_previous and cur_buf ~= vim.api.nvim_win_get_buf(win_id) then return end
-- Create new listed buffer
local new_buf = vim.api.nvim_create_buf(true, false)
vim.api.nvim_win_set_buf(win_id, new_buf)
end)
return true
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniBufremove.config)
-- 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({
set_vim_settings = { config.set_vim_settings, 'boolean' },
silent = { config.silent, 'boolean' },
})
return config
end
H.apply_config = function(config)
MiniBufremove.config = config
if config.set_vim_settings then
vim.o.hidden = true -- Allow hidden buffers
end
end
H.is_disabled = function() return vim.g.minibufremove_disable == true or vim.b.minibufremove_disable == true end
-- Removing implementation ----------------------------------------------------
H.unshow_and_cmd = function(buf_id, force, cmd)
buf_id = H.normalize_buf_id(buf_id)
if not H.is_valid_id(buf_id, 'buffer') then
H.message(buf_id .. ' is not a valid buffer id.')
return false
end
if force == nil then force = false end
if type(force) ~= 'boolean' then
H.message('`force` should be boolean.')
return false
end
local fun_name = ({ ['bdelete'] = 'delete', ['bwipeout'] = 'wipeout' })[cmd]
if not H.can_remove(buf_id, force, fun_name) then return false end
-- Unshow buffer from all windows
MiniBufremove.unshow(buf_id)
-- Execute command
local command = string.format('%s! %d', cmd, buf_id)
-- Use `pcall` here to take care of case where `unshow()` was enough. This
-- can happen with 'bufhidden' option values:
-- - If `delete` then `unshow()` already `bdelete`d buffer. Without `pcall`
-- it gives E516 for `MiniBufremove.delete()` (`wipeout` works).
-- - If `wipe` then `unshow()` already `bwipeout`ed buffer. Without `pcall`
-- it gives E517 for module's `wipeout()` (still E516 for `delete()`).
--
-- Also account for executing command in command-line window.
-- It gives E11 if trying to execute command. The `unshow()` call should
-- close such window but somehow it doesn't seem to happen immediately.
local ok, result = pcall(vim.cmd, command)
if not (ok or result:find('E516%D') or result:find('E517%D') or result:find('E11%D')) then
H.message(result)
return false
end
return true
end
-- Utilities ------------------------------------------------------------------
H.echo = function(msg, is_important)
if MiniBufremove.config.silent then return end
-- Construct message chunks
msg = type(msg) == 'string' and { { msg } } or msg
table.insert(msg, 1, { '(mini.bufremove) ', 'WarningMsg' })
-- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
vim.cmd([[echo '' | redraw]])
vim.api.nvim_echo(msg, is_important, {})
end
H.message = function(msg) H.echo(msg, true) end
H.is_valid_id = function(x, type)
local is_valid = false
if type == 'buffer' then
is_valid = vim.api.nvim_buf_is_valid(x)
elseif type == 'window' then
is_valid = vim.api.nvim_win_is_valid(x)
end
if not is_valid then H.message(string.format('%s is not a valid %s id.', tostring(x), type)) end
return is_valid
end
-- Check if buffer can be removed with `MiniBufremove.fun_name` function
H.can_remove = function(buf_id, force, fun_name)
if force or not vim.bo[buf_id].modified then return true end
local msg = string.format('Buffer %d has unsaved changes. Do you want to force %s?', buf_id, fun_name)
return vim.fn.confirm(msg, '&No\n&Yes', 1, 'Question') == 2
end
-- Compute 'true' buffer id (strictly positive integer). Treat `nil` and 0 as
-- current buffer.
H.normalize_buf_id = function(buf_id)
if buf_id == nil or buf_id == 0 then return vim.api.nvim_get_current_buf() end
return buf_id
end
return MiniBufremove

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,566 @@
--- *mini.comment* Comment lines
--- *MiniComment*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Commenting in Normal mode respects |count| and is dot-repeatable.
---
--- - Comment structure by default is inferred from 'commentstring': either
--- from current buffer or from locally active tree-sitter language (only on
--- Neovim>=0.9). It can be customized via `options.custom_commentstring`
--- (see |MiniComment.config| for details).
---
--- - Allows custom hooks before and after successful commenting.
---
--- - Configurable options for some nuanced behavior.
---
--- What it doesn't do:
--- - Block and sub-line comments. This will only support per-line commenting.
---
--- - Handle indentation with mixed tab and space.
---
--- - Preserve trailing whitespace in empty lines.
---
--- Notes:
--- - To use tree-sitter aware commenting, global value of 'commentstring'
--- should be `''` (empty string). This is the default value in Neovim>=0.9,
--- so make sure to not set it manually.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.comment').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table
--- `MiniComment` which you can use for scripting or manually (with
--- `:lua MiniComment.*`).
---
--- See |MiniComment.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minicomment_config` which should have same structure as
--- `MiniComment.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Disabling ~
---
--- To disable core functionality, set `vim.g.minicomment_disable` (globally) or
--- `vim.b.minicomment_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 MiniComment = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniComment.config|.
---
---@usage >lua
--- require('mini.comment').setup() -- use default config
--- -- OR
--- require('mini.comment').setup({}) -- replace {} with your config table
--- <
MiniComment.setup = function(config)
-- Export module
_G.MiniComment = MiniComment
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Options ~
---
--- ## Custom commentstring ~
---
--- `options.custom_commentstring` can be a function customizing 'commentstring'
--- option used to infer comment structure. It is called once before every
--- commenting action with the following arguments:
--- - `ref_position` - position at which to compute 'commentstring' (might be
--- relevant for a text with locally different commenting rules). Its structure
--- is the same as `opts.ref_position` in |MiniComment.toggle_lines()|.
---
--- Its output should be a valid 'commentstring' (string containing `%s`).
---
--- If not set or the output is `nil`, |MiniComment.get_commentstring()| is used.
---
--- For example, this option can be used to always use buffer 'commentstring'
--- even in case of present active tree-sitter parser: >lua
---
--- require('mini.comment').setup({
--- options = {
--- custom_commentstring = function() return vim.bo.commentstring end,
--- }
--- })
--- <
--- # Hooks ~
---
--- `hooks.pre` and `hooks.post` functions are executed before and after successful
--- commenting action (toggle or computing textobject). They will be called
--- with a single table argument which has the following fields:
--- - <action> `(string)` - action name. One of "toggle" (when actual toggle
--- direction is yet unknown), "comment", "uncomment", "textobject".
--- - <line_start> `(number|nil)` - action start line. Can be absent if yet unknown.
--- - <line_end> `(number|nil)` - action end line. Can be absent if yet unknown.
--- - <ref_position> `(table|nil)` - reference position.
---
--- Notes:
--- - Changing 'commentstring' in `hooks.pre` is allowed and will take effect.
--- - If hook returns `false`, any further action is terminated.
MiniComment.config = {
-- Options which control module behavior
options = {
-- Function to compute custom 'commentstring' (optional)
custom_commentstring = nil,
-- Whether to ignore blank lines when commenting
ignore_blank_line = false,
-- Whether to recognize as comment only lines without indent
start_of_line = false,
-- Whether to force single space inner padding for comment parts
pad_comment_parts = true,
},
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Toggle comment (like `gcip` - comment inner paragraph) for both
-- Normal and Visual modes
comment = 'gc',
-- Toggle comment on current line
comment_line = 'gcc',
-- Toggle comment on visual selection
comment_visual = 'gc',
-- Define 'comment' textobject (like `dgc` - delete whole comment block)
-- Works also in Visual mode if mapping differs from `comment_visual`
textobject = 'gc',
},
-- Hook functions to be executed at certain stage of commenting
hooks = {
-- Before successful commenting. Does nothing by default.
pre = function() end,
-- After successful commenting. Does nothing by default.
post = function() end,
},
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Main function to be mapped
---
--- It is meant to be used in expression mappings (see |map-<expr>|) to enable
--- dot-repeatability and commenting on range. There is no need to do this
--- manually, everything is done inside |MiniComment.setup()|.
---
--- It has a somewhat unintuitive logic (because of how expression mapping with
--- dot-repeatability works): it should be called without arguments inside
--- expression mapping and with argument when action should be performed.
---
---@param mode string|nil Optional string with 'operatorfunc' mode (see |g@|).
---
---@return string|nil 'g@' if called without argument, '' otherwise (but after
--- performing action).
MiniComment.operator = function(mode)
if H.is_disabled() then return '' end
-- If used without arguments inside expression mapping:
-- - Set itself as `operatorfunc` to be called later to perform action.
-- - Return 'g@' which will then be executed resulting into waiting for a
-- motion or text object. This textobject will then be recorded using `'[`
-- and `']` marks. After that, `operatorfunc` is called with `mode` equal
-- to one of "line", "char", or "block".
-- NOTE: setting `operatorfunc` inside this function enables usage of 'count'
-- like `10gc_` toggles comments of 10 lines below (starting with current).
if mode == nil then
vim.o.operatorfunc = 'v:lua.MiniComment.operator'
return 'g@'
end
-- If called with non-nil `mode`, get target region and act on it
-- This also works in expression mapping in Visual mode, as `g@` seems to
-- place these marks on start and end of visual selection
local mark_left, mark_right = '[', ']'
local lnum_from, col_from = unpack(vim.api.nvim_buf_get_mark(0, mark_left))
local lnum_to, col_to = unpack(vim.api.nvim_buf_get_mark(0, mark_right))
-- Do nothing if "from" mark is after "to" (like in empty textobject)
if (lnum_from > lnum_to) or (lnum_from == lnum_to and col_from > col_to) then return end
-- NOTE: use cursor position as reference for possibly computing local
-- tree-sitter-based 'commentstring'. Recompute every time for a proper
-- dot-repeat. In Visual and sometimes Normal mode it uses left position.
local cursor = vim.api.nvim_win_get_cursor(0)
MiniComment.toggle_lines(lnum_from, lnum_to, { ref_position = { cursor[1], cursor[2] + 1 } })
return ''
end
--- Toggle comments between two line numbers
---
--- It uncomments if lines are comment (every line is a comment) and comments
--- otherwise. It respects indentation and doesn't insert trailing
--- whitespace. Toggle commenting not in visual mode is also dot-repeatable
--- and respects |count|.
---
--- # Notes ~
---
--- - Comment structure is inferred from buffer's 'commentstring' option or
--- local language of tree-sitter parser (if active; only on Neovim>=0.9).
---
--- - Call to this function will remove all |extmarks| from target range.
---
---@param line_start number Start line number (inclusive from 1 to number of lines).
---@param line_end number End line number (inclusive from 1 to number of lines).
---@param opts table|nil Options. Possible fields:
--- - <ref_position> `(table)` - A two-value array with `{ row, col }` (both
--- starting at 1) of reference position at which 'commentstring' value
--- will be computed. Default: `{ line_start, 1 }`.
MiniComment.toggle_lines = function(line_start, line_end, opts)
if H.is_disabled() then return end
opts = opts or {}
local ref_position = vim.deepcopy(opts.ref_position) or { line_start, 1 }
local n_lines = vim.api.nvim_buf_line_count(0)
if not (1 <= line_start and line_start <= n_lines and 1 <= line_end and line_end <= n_lines) then
error('(mini.comment) `line_start` and `line_end` should be within range [1; ' .. n_lines .. '].')
end
if not (line_start <= line_end) then
error('(mini.comment) `line_start` should be less than or equal to `line_end`.')
end
local config = H.get_config()
local hook_arg = { action = 'toggle', line_start = line_start, line_end = line_end, ref_position = ref_position }
if config.hooks.pre(hook_arg) == false then return end
local parts = H.get_comment_parts(ref_position, config.options)
local lines = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false)
local indent, is_comment = H.get_lines_info(lines, parts, config.options)
local f = is_comment and H.make_uncomment_function(parts) or H.make_comment_function(parts, indent, config.options)
-- NOTE: Direct of `nvim_buf_set_lines()` essentially removes (squashes to
-- empty range at either side of the region) both regular and extended marks
-- inside region. It can be resolved at least in the following ways:
-- 1. Use `lockmarks`. Preserves regular but does nothing for extmarks.
-- 2. Use `vim.fn.setline(line_start, new_lines)`. Preserves regular marks,
-- but squashes extmarks within a single line.
-- 3. Refactor to use precise editing of lines with `nvim_buf_set_text()`.
-- Preserves both regular and extended marks.
--
-- But:
-- - Options 2 and 3 are **significantly** slower for a large-ish regions.
-- Toggle of ~4000 lines takes 20 ms for 1, 200 ms for 2, 400 ms for 3.
--
-- - Preserving extmarks is not a universally good thing to do. It looks like
-- a good idea for extmarks which are not used for directly highlighting
-- text (like for 'mini.diff' signs or smartly tracking buffer position).
-- However, preserving extmarks is not 100% desirable when they highlight
-- text area, as every comment toggle at least results in a flickering
-- due to those extmarks still highlighting a (un)commented region.
-- Main example is LSP semantic token highlighting. Although it can have
-- special treatment (precisely clear those extmarks in the target region),
-- it is not 100% effective (they are restored after undo, again resulting
-- into flicker) and there might be more unnoticed issues.
--
-- So all in all, computing and replacing whole lines with `lockmarks` is the
-- best compromise so far. It also aligns with treating "toggle comment" in
-- a semantic way (those lines lines now have completely different meaning)
-- rather than in a text edit way (add comment parts to those lines).
_G._from, _G._to, _G._lines = line_start - 1, line_end, vim.tbl_map(f, lines)
vim.cmd('lockmarks lua pcall(vim.api.nvim_buf_set_lines, 0, _G._from, _G._to, false, _G._lines)')
_G._from, _G._to, _G._lines = nil, nil, nil
hook_arg.action = is_comment and 'uncomment' or 'comment'
if config.hooks.post(hook_arg) == false then return end
end
--- Select comment textobject
---
--- This selects all commented lines adjacent to cursor line (if it itself is
--- commented). Designed to be used with operator mode mappings (see |mapmode-o|).
MiniComment.textobject = function()
if H.is_disabled() then return end
local config = H.get_config()
local hook_args = { action = 'textobject' }
if config.hooks.pre(hook_args) == false then return end
local lnum_cur = vim.fn.line('.')
local parts = H.get_comment_parts({ lnum_cur, vim.fn.col('.') }, config.options)
local comment_check = H.make_comment_check(parts, config.options)
local lnum_from, lnum_to
if comment_check(vim.fn.getline(lnum_cur)) then
lnum_from = lnum_cur
while (lnum_from >= 2) and comment_check(vim.fn.getline(lnum_from - 1)) do
lnum_from = lnum_from - 1
end
lnum_to = lnum_cur
local n_lines = vim.api.nvim_buf_line_count(0)
while (lnum_to <= n_lines - 1) and comment_check(vim.fn.getline(lnum_to + 1)) do
lnum_to = lnum_to + 1
end
local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode())
if is_visual then vim.cmd('normal! \27') end
-- This visual selection doesn't seem to change `'<` and `'>` marks when
-- executed as `onoremap` mapping
vim.cmd('normal! ' .. lnum_from .. 'GV' .. lnum_to .. 'G')
end
hook_args.line_start, hook_args.line_end = lnum_from, lnum_to
if config.hooks.post(hook_args) == false then return end
end
--- Get 'commentstring'
---
--- This function represents default approach of computing relevant
--- 'commentstring' option in current buffer. Used to infer comment structure.
---
--- It has the following logic:
--- - (Only on Neovim>=0.9) If there is an active tree-sitter parser, try to get
--- 'commentstring' from the local language at `ref_position`.
---
--- - If first step is not successful, use buffer's 'commentstring' directly.
---
---@param ref_position table Reference position inside current buffer at which
--- to compute 'commentstring'. Same structure as `opts.ref_position`
--- in |MiniComment.toggle_lines()|.
---
---@return string Relevant value of 'commentstring'.
MiniComment.get_commentstring = function(ref_position)
local buf_cs = vim.bo.commentstring
-- Neovim<0.9 can only have buffer 'commentstring'
if vim.fn.has('nvim-0.9') == 0 then return buf_cs end
local has_ts_parser, ts_parser = pcall(vim.treesitter.get_parser)
if not has_ts_parser then return buf_cs end
-- Try to get 'commentstring' associated with local tree-sitter language.
-- This is useful for injected languages (like markdown with code blocks).
-- Sources:
-- - https://github.com/neovim/neovim/pull/22634#issue-1620078948
-- - https://github.com/neovim/neovim/pull/22643
local row, col = ref_position[1] - 1, ref_position[2] - 1
local ref_range = { row, col, row, col + 1 }
-- - Get 'commentstring' from the deepest LanguageTree which both contains
-- reference range and has valid 'commentstring' (meaning it has at least
-- one associated 'filetype' with valid 'commentstring').
-- In simple cases using `parser:language_for_range()` would be enough, but
-- it fails for languages without valid 'commentstring' (like 'comment').
local ts_cs, res_level = nil, 0
local traverse
traverse = function(lang_tree, level)
if not lang_tree:contains(ref_range) then return end
local lang = lang_tree:lang()
local filetypes = vim.treesitter.language.get_filetypes(lang)
for _, ft in ipairs(filetypes) do
-- Using `vim.filetype.get_option()` for performance as it has caching
local cur_cs = vim.filetype.get_option(ft, 'commentstring')
if type(cur_cs) == 'string' and cur_cs ~= '' and level > res_level then ts_cs = cur_cs end
end
for _, child_lang_tree in pairs(lang_tree:children()) do
traverse(child_lang_tree, level + 1)
end
end
traverse(ts_parser, 1)
return ts_cs or buf_cs
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniComment.config)
-- 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({
options = { config.options, 'table' },
mappings = { config.mappings, 'table' },
hooks = { config.hooks, 'table' },
})
vim.validate({
['options.custom_commentstring'] = { config.options.custom_commentstring, 'function', true },
['options.ignore_blank_line'] = { config.options.ignore_blank_line, 'boolean' },
['options.start_of_line'] = { config.options.start_of_line, 'boolean' },
['options.pad_comment_parts'] = { config.options.pad_comment_parts, 'boolean' },
['mappings.comment'] = { config.mappings.comment, 'string' },
['mappings.comment_line'] = { config.mappings.comment_line, 'string' },
['mappings.comment_visual'] = { config.mappings.comment_visual, 'string' },
['mappings.textobject'] = { config.mappings.textobject, 'string' },
['hooks.pre'] = { config.hooks.pre, 'function' },
['hooks.post'] = { config.hooks.post, 'function' },
})
return config
end
H.apply_config = function(config)
MiniComment.config = config
-- Make mappings
local operator_rhs = function() return MiniComment.operator() end
H.map('n', config.mappings.comment, operator_rhs, { expr = true, desc = 'Comment' })
H.map('x', config.mappings.comment_visual, operator_rhs, { expr = true, desc = 'Comment selection' })
H.map(
'n',
config.mappings.comment_line,
function() return MiniComment.operator() .. '_' end,
{ expr = true, desc = 'Comment line' }
)
-- Use `<Cmd>...<CR>` to have proper dot-repeat
-- See https://github.com/neovim/neovim/issues/23406
local modes = config.mappings.textobject == config.mappings.comment_visual and { 'o' } or { 'x', 'o' }
H.map(modes, config.mappings.textobject, '<Cmd>lua MiniComment.textobject()<CR>', { desc = 'Comment textobject' })
end
H.is_disabled = function() return vim.g.minicomment_disable == true or vim.b.minicomment_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniComment.config, vim.b.minicomment_config or {}, config or {})
end
-- Core implementations -------------------------------------------------------
H.get_comment_parts = function(ref_position, options)
local cs
if vim.is_callable(options.custom_commentstring) then cs = options.custom_commentstring(ref_position) end
cs = cs or MiniComment.get_commentstring(ref_position)
if cs == nil or cs == '' then
vim.api.nvim_echo({ { '(mini.comment) ', 'WarningMsg' }, { [[Option 'commentstring' is empty.]] } }, true, {})
return { left = '', right = '' }
end
if not (type(cs) == 'string' and string.find(cs, '%%s') ~= nil) then
H.error(vim.inspect(cs) .. " is not a valid 'commentstring'.")
end
-- Structure of 'commentstring': <left part> <%s> <right part>
local left, right = string.match(cs, '^(.-)%%s(.-)$')
-- Force single space padding if requested
if options.pad_comment_parts then
left, right = vim.trim(left), vim.trim(right)
left, right = left == '' and '' or (left .. ' '), right == '' and '' or (' ' .. right)
end
return { left = left, right = right }
end
H.make_comment_check = function(parts, options)
local l_esc, r_esc = vim.pesc(parts.left), vim.pesc(parts.right)
local prefix = options.start_of_line and '' or '%s-'
-- Commented line has the following structure:
-- <whitespace> <trimmed left> <anything> <trimmed right> <whitespace>
local regex = '^' .. prefix .. vim.trim(l_esc) .. '.*' .. vim.trim(r_esc) .. '%s-$'
return function(line) return string.find(line, regex) ~= nil end
end
H.get_lines_info = function(lines, parts, options)
local comment_check = H.make_comment_check(parts, options)
local is_commented = true
local indent, indent_width = nil, math.huge
for _, l in ipairs(lines) do
-- Update lines indent: minimum of all indents except blank lines
local _, indent_width_cur, indent_cur = string.find(l, '^(%s*)')
-- Ignore blank lines completely when making a decision
if indent_width_cur < l:len() then
-- NOTE: Copying actual indent instead of recreating it with `indent_width`
-- allows to handle both tabs and spaces
if indent_width_cur < indent_width then
indent_width, indent = indent_width_cur, indent_cur
end
-- Update comment info: commented if every non-blank line is commented
if is_commented then is_commented = comment_check(l) end
end
end
-- `indent` can still be `nil` in case all `lines` are empty
return indent or '', is_commented
end
H.make_comment_function = function(parts, indent, options)
local prefix = options.start_of_line and (parts.left .. indent) or (indent .. parts.left)
local nonindent_start = string.len(indent) + 1
local suffix = parts.right
local blank_comment = indent .. vim.trim(parts.left) .. vim.trim(parts.right)
local ignore_blank_line = options.ignore_blank_line
return function(line)
if H.is_blank(line) then return ignore_blank_line and line or blank_comment end
return prefix .. string.sub(line, nonindent_start) .. suffix
end
end
H.make_uncomment_function = function(parts)
local l_esc, r_esc = vim.pesc(parts.left), vim.pesc(parts.right)
local regex = '^(%s*)' .. l_esc .. '(.*)' .. r_esc .. '(%s-)$'
local regex_trimmed = '^(%s*)' .. vim.trim(l_esc) .. '(.*)' .. vim.trim(r_esc) .. '(%s-)$'
return function(line)
-- Try regex with exact comment parts first, fall back to trimmed parts
local indent, new_line, trail = line:match(regex)
if new_line == nil then
indent, new_line, trail = line:match(regex_trimmed)
end
-- Return original if line is not commented
if new_line == nil then return line end
-- Prevent trailing whitespace
if H.is_blank(new_line) then
indent, trail = '', ''
end
return indent .. new_line .. trail
end
end
-- Utilities ------------------------------------------------------------------
H.error = function(msg) error('(mini.comment) ' .. msg, 0) 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.is_blank = function(x) return string.find(x, '^%s*$') ~= nil end
return MiniComment

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,299 @@
--- *mini.cursorword* Autohighlight word under cursor
--- *MiniCursorword*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Autohighlight word under cursor with customizable delay.
---
--- - Current word under cursor can be highlighted differently.
---
--- - Highlighting is triggered only if current cursor character is a |[:keyword:]|.
---
--- - Highlighting stops in insert and terminal modes.
---
--- - "Word under cursor" is meant as in Vim's |<cword>|: something user would
--- get as 'iw' text object.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.cursorword').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniCursorword` which you can use for scripting or manually (with
--- `:lua MiniCursorword.*`).
---
--- See |MiniCursorword.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minicursorword_config` which should have same structure as
--- `MiniCursorword.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Highlight groups ~
---
--- * `MiniCursorword` - highlight group of a non-current cursor word.
--- Default: plain underline.
---
--- * `MiniCursorwordCurrent` - highlight group of a current word under cursor.
--- Default: links to `MiniCursorword` (so `:hi clear MiniCursorwordCurrent`
--- will lead to showing `MiniCursorword` highlight group).
--- Note: To not highlight it, use the following Lua code: >lua
---
--- vim.api.nvim_set_hl(0, 'MiniCursorwordCurrent', {})
--- <
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable core functionality, set `vim.g.minicursorword_disable` (globally) or
--- `vim.b.minicursorword_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. Note: after disabling
--- there might be highlighting left; it will be removed after next
--- highlighting update.
---
--- Module-specific disabling:
--- - Don't show highlighting if cursor is on the word that is in a blocklist
--- of current filetype. In this example, blocklist for "lua" is "local" and
--- "require" words, for "javascript" - "import": >lua
---
--- _G.cursorword_blocklist = function()
--- local curword = vim.fn.expand('<cword>')
--- local filetype = vim.bo.filetype
---
--- -- Add any disabling global or filetype-specific logic here
--- local blocklist = {}
--- if filetype == 'lua' then
--- blocklist = { 'local', 'require' }
--- elseif filetype == 'javascript' then
--- blocklist = { 'import' }
--- end
---
--- vim.b.minicursorword_disable = vim.tbl_contains(blocklist, curword)
--- end
---
--- -- Make sure to add this autocommand *before* calling module's `setup()`.
--- vim.cmd('au CursorMoved * lua _G.cursorword_blocklist()')
--- <
-- Module definition ==========================================================
local MiniCursorword = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniCursorword.config|.
---
---@usage >lua
--- require('mini.cursorword').setup() -- use default config
--- -- OR
--- require('mini.cursorword').setup({}) -- replace {} with your config table
--- <
MiniCursorword.setup = function(config)
-- Export module
_G.MiniCursorword = MiniCursorword
-- 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)
MiniCursorword.config = {
-- Delay (in ms) between when cursor moved and when highlighting appeared
delay = 100,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniCursorword.config)
-- Delay timer
H.timer = vim.loop.new_timer()
-- Information about last match highlighting (stored *per window*):
-- - Key: windows' unique buffer identifiers.
-- - Value: table with:
-- - `id` field for match id (from `vim.fn.matchadd()`).
-- - `word` field for matched word.
H.window_matches = {}
-- 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({ delay = { config.delay, 'number' } })
return config
end
H.apply_config = function(config)
MiniCursorword.config = config
-- Make `setup()` to proper reset module
for _, m in ipairs(vim.fn.getmatches()) do
if vim.startswith(m.group, 'MiniCursorword') then vim.fn.matchdelete(m.id) end
end
end
H.create_autocommands = function()
local augroup = vim.api.nvim_create_augroup('MiniCursorword', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
au('CursorMoved', '*', H.auto_highlight, 'Auto highlight cursorword')
au({ 'InsertEnter', 'TermEnter', 'QuitPre' }, '*', H.auto_unhighlight, 'Auto unhighlight cursorword')
au('ModeChanged', '*:[^i]', H.auto_highlight, 'Auto highlight cursorword')
au('ColorScheme', '*', H.create_default_hl, 'Ensure proper colors')
au('FileType', 'TelescopePrompt', function() vim.b.minicursorword_disable = true end, 'Disable locally')
end
--stylua: ignore
H.create_default_hl = function()
vim.api.nvim_set_hl(0, 'MiniCursorword', { default = true, underline = true })
vim.api.nvim_set_hl(0, 'MiniCursorwordCurrent', { default = true, link = 'MiniCursorword' })
end
H.is_disabled = function() return vim.g.minicursorword_disable == true or vim.b.minicursorword_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniCursorword.config, vim.b.minicursorword_config or {}, config or {})
end
-- Autocommands ---------------------------------------------------------------
H.auto_highlight = function()
-- Stop any possible previous delayed highlighting
H.timer:stop()
-- Stop highlighting immediately if module is disabled when cursor is not on
-- 'keyword'
if not H.should_highlight() then return H.unhighlight() end
-- Get current information
local win_id = vim.api.nvim_get_current_win()
local win_match = H.window_matches[win_id] or {}
local curword = H.get_cursor_word()
-- Only immediately update highlighting of current word under cursor if
-- currently highlighted word equals one under cursor
if win_match.word == curword then
H.unhighlight(true)
H.highlight(true)
return
end
-- Stop highlighting previous match (if it exists)
H.unhighlight()
-- Delay highlighting
H.timer:start(
H.get_config().delay,
0,
vim.schedule_wrap(function()
-- Ensure that always only one word is highlighted
H.unhighlight()
H.highlight()
end)
)
end
H.auto_unhighlight = function()
-- Stop any possible previous delayed highlighting
H.timer:stop()
H.unhighlight()
end
-- Highlighting ---------------------------------------------------------------
---@param only_current boolean|nil Whether to forcefully highlight only current word
--- under cursor.
---@private
H.highlight = function(only_current)
-- A modified version of https://stackoverflow.com/a/25233145
-- Using `matchadd()` instead of a simpler `:match` to tweak priority of
-- 'current word' highlighting: with `:match` it is higher than for
-- `incsearch` which is not convenient.
local win_id = vim.api.nvim_get_current_win()
if not vim.api.nvim_win_is_valid(win_id) then return end
if not H.should_highlight() then return end
H.window_matches[win_id] = H.window_matches[win_id] or {}
-- Add match highlight for current word under cursor
local current_word_pattern = [[\k*\%#\k*]]
local match_id_current = vim.fn.matchadd('MiniCursorwordCurrent', current_word_pattern, -1)
H.window_matches[win_id].id_current = match_id_current
-- Don't add main match id if not needed or if one is already present
if only_current or H.window_matches[win_id].id ~= nil then return end
-- Add match highlight for non-current word under cursor. NOTEs:
-- - Using `\(...\)\@!` allows to not match current word.
-- - Using 'very nomagic' ('\V') allows not escaping.
-- - Using `\<` and `\>` matches whole word (and not as part).
local curword = H.get_cursor_word()
local pattern = string.format([[\(%s\)\@!\&\V\<%s\>]], current_word_pattern, curword)
local match_id = vim.fn.matchadd('MiniCursorword', pattern, -1)
-- Store information about highlight
H.window_matches[win_id].id = match_id
H.window_matches[win_id].word = curword
end
---@param only_current boolean|nil Whether to remove highlighting only of current
--- word under cursor.
---@private
H.unhighlight = function(only_current)
-- Don't do anything if there is no valid information to act upon
local win_id = vim.api.nvim_get_current_win()
local win_match = H.window_matches[win_id]
if not vim.api.nvim_win_is_valid(win_id) or win_match == nil then return end
-- Use `pcall` because there is an error if match id is not present. It can
-- happen if something else called `clearmatches`.
pcall(vim.fn.matchdelete, win_match.id_current)
H.window_matches[win_id].id_current = nil
if not only_current then
pcall(vim.fn.matchdelete, win_match.id)
H.window_matches[win_id] = nil
end
end
H.should_highlight = function() return not H.is_disabled() and H.is_cursor_on_keyword() end
H.is_cursor_on_keyword = function()
local col = vim.fn.col('.')
local curchar = vim.api.nvim_get_current_line():sub(col, col)
-- Use `pcall()` to catch `E5108` (can happen in binary files, see #112)
local ok, match_res = pcall(vim.fn.match, curchar, '[[:keyword:]]')
return ok and match_res >= 0
end
H.get_cursor_word = function() return vim.fn.escape(vim.fn.expand('<cword>'), [[\/]]) end
return MiniCursorword

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,376 @@
--- *mini.fuzzy* Fuzzy matching
--- *MiniFuzzy*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Minimal and fast fuzzy matching algorithm which prioritizes match width.
---
--- - Functions to for common fuzzy matching operations:
--- - |MiniFuzzy.match()|.
--- - |MiniFuzzy.filtersort()|.
--- - |MiniFuzzy.process_lsp_items()|.
---
--- - Generator of |telescope.nvim| sorter: |MiniFuzzy.get_telescope_sorter()|.
---
--- # Setup ~
---
--- This module doesn't need setup, but it can be done to improve usability.
--- Setup with `require('mini.fuzzy').setup({})` (replace `{}` with your
--- `config` table). It will create global Lua table `MiniFuzzy` which you can
--- use for scripting or manually (with `:lua MiniFuzzy.*`).
---
--- See |MiniFuzzy.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minifuzzy_config` which should have same structure as
--- `MiniFuzzy.config`.
--- See |mini.nvim-buffer-local-config| for more details.
---
--- # Notes ~
---
--- 1. Currently there is no explicit design to work with multibyte symbols,
--- but simple examples should work.
--- 2. Smart case is used: case insensitive if input word (which is usually a
--- user input) is all lower case. Case sensitive otherwise.
--- # Algorithm design ~
---
--- General design uses only width of found match and index of first letter
--- match. No special characters or positions (like in fzy and fzf) are used.
---
--- Given input `word` and target `candidate`:
--- - The goal is to find matching between `word`'s letters and letters in
--- `candidate`, which minimizes certain score. It is assumed that order of
--- letters in `word` and those matched in `candidate` should be the same.
--- - Matching is represented by matched positions: an array `positions` of
--- integers with length equal to number of letters in `word`. The following
--- should be always true in case of a match: `candidate`'s letter at index
--- `positions[i]` is letters[i]` for all valid `i`.
--- - Matched positions are evaluated based only on two features: their width
--- (number of indexes between first and last positions) and first match
--- (index of first letter match). There is a global setting `cutoff` for
--- which all feature values greater than it can be considered "equally bad".
--- - Score of matched positions is computed with following explicit formula:
--- `cutoff * min(width, cutoff) + min(first, cutoff)`. It is designed to be
--- equivalent to first comparing widths (lower is better) and then comparing
--- first match (lower is better). For example, if `word = 'time'`:
--- - '_time' (width 4) will have a better match than 't_ime' (width 5).
--- - 'time_a' (width 4, first 1) will have a better match than 'a_time'
--- (width 4, first 3).
--- - Final matched positions are those which minimize score among all possible
--- matched positions of `word` and `candidate`.
---@tag MiniFuzzy-algorithm
-- Module definition ==========================================================
local MiniFuzzy = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniFuzzy.config|.
---
---@usage >lua
--- require('mini.fuzzy').setup() -- use default config
--- -- OR
--- require('mini.fuzzy').setup({}) -- replace {} with your config table
--- <
MiniFuzzy.setup = function(config)
-- Export module
_G.MiniFuzzy = MiniFuzzy
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniFuzzy.config = {
-- Maximum allowed value of match features (width and first match). All
-- feature values greater than cutoff can be considered "equally bad".
cutoff = 100,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Compute match data of input `word` and `candidate` strings
---
--- It tries to find best match for input string `word` (usually user input)
--- and string `candidate`. Returns table with elements:
--- - `positions` - array with letter indexes inside `candidate` which
--- matched to corresponding letters in `word`. Or `nil` if no match.
--- - `score` - positive number representing how good the match is (lower is
--- better). Or `-1` if no match.
---
---@param word string Input word (usually user input).
---@param candidate string Target word (usually with which matching is done).
---
---@return table Table with matching information (see function's description).
MiniFuzzy.match = function(word, candidate)
-- Use 'smart case'
candidate = (word == word:lower()) and candidate:lower() or candidate
local positions = H.find_best_positions(H.string_to_letters(word), candidate)
return { positions = positions, score = H.score_positions(positions) }
end
--- Filter string array
---
--- This leaves only those elements of input array which matched with `word`
--- and sorts from best to worst matches (based on score and index in original
--- array, both lower is better).
---
---@param word string String which will be searched.
---@param candidate_array table Lua array of strings inside which word will be
--- searched.
---
---@return ... Arrays of matched candidates and their indexes in original input.
MiniFuzzy.filtersort = function(word, candidate_array)
-- Use 'smart case'. Create new array to preserve input for later filtering
local cand_array
if word == word:lower() then
cand_array = vim.tbl_map(string.lower, candidate_array)
else
cand_array = candidate_array
end
local filter_ids = H.make_filter_indexes(word, cand_array)
table.sort(filter_ids, H.compare_filter_indexes)
return H.filter_by_indexes(candidate_array, filter_ids)
end
--- Fuzzy matching for `lsp_completion.process_items` of |MiniCompletion.config|
---
---@param items table Array with LSP 'textDocument/completion' response items.
---@param base string Word to complete.
MiniFuzzy.process_lsp_items = function(items, base)
-- Extract completion words from items
local words = vim.tbl_map(function(x)
if type(x.textEdit) == 'table' and type(x.textEdit.newText) == 'string' then return x.textEdit.newText end
if type(x.insertText) == 'string' then return x.insertText end
if type(x.label) == 'string' then return x.label end
return ''
end, items)
-- Fuzzy match
local _, match_inds = MiniFuzzy.filtersort(base, words)
return vim.tbl_map(function(i) return items[i] end, match_inds)
end
--- Custom getter for `telescope.nvim` sorter
---
--- Designed to be used as value for |telescope.defaults.file_sorter| and
--- |telescope.defaults.generic_sorter| inside `setup()` call.
---
---@param opts table|nil Options (currently not used).
---
---@usage >lua
--- require('telescope').setup({
--- defaults = {
--- generic_sorter = require('mini.fuzzy').get_telescope_sorter
--- }
--- })
--- <
MiniFuzzy.get_telescope_sorter = function(opts)
opts = opts or {}
return require('telescope.sorters').Sorter:new({
start = function(self, prompt)
-- Cache prompt's letters
self.letters = H.string_to_letters(prompt)
-- Use 'smart case': insensitive if `prompt` is lowercase
self.case_sensitive = prompt ~= prompt:lower()
end,
-- @param self
-- @param prompt (which is the text on the line)
-- @param line (entry.ordinal)
-- @param entry (the whole entry)
scoring_function = function(self, _, line, _)
if #self.letters == 0 then return 1 end
line = self.case_sensitive and line or line:lower()
local positions = H.find_best_positions(self.letters, line)
return H.score_positions(positions)
end,
-- Currently there doesn't seem to be a proper way to cache matched
-- positions from inside of `scoring_function` (see `highlighter` code of
-- `get_fzy_sorter`'s output). Besides, it seems that `display` and `line`
-- arguments might be different. So, extra calls to `match` are made.
highlighter = function(self, _, display)
if #self.letters == 0 or #display == 0 then return {} end
display = self.case_sensitive and display or display:lower()
return H.find_best_positions(self.letters, display)
end,
})
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniFuzzy.config)
-- 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({
cutoff = {
config.cutoff,
function(x) return type(x) == 'number' and x >= 1 end,
'number not less than 1',
},
})
return config
end
H.apply_config = function(config) MiniFuzzy.config = config end
H.is_disabled = function() return vim.g.minifuzzy_disable == true or vim.b.minifuzzy_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniFuzzy.config, vim.b.minifuzzy_config or {}, config or {})
end
-- Fuzzy matching -------------------------------------------------------------
---@param letters table Array of letters from input word
---@param candidate string String of interest
---
---@return table|nil Table with matched positions (in `candidate`) if there is a
--- match, `nil` otherwise.
---@private
H.find_best_positions = function(letters, candidate)
local n_candidate, n_letters = #candidate, #letters
if n_letters == 0 or n_candidate < n_letters then return nil end
-- Search forward to find matching positions with left-most last letter match
local pos_last = 0
for let_i = 1, #letters do
pos_last = candidate:find(letters[let_i], pos_last + 1)
if not pos_last then break end
end
-- Candidate is matched only if word's last letter is found
if not pos_last then return nil end
-- If there is only one letter, it is already the best match (there will not
-- be better width and it has lowest first match)
if n_letters == 1 then return { pos_last } end
-- Compute best match positions by iteratively checking all possible last
-- letter matches (at and after initial one). At end of each iteration
-- `best_pos_last` holds best match for last letter among all previously
-- checked such matches.
local best_pos_last, best_width = pos_last, math.huge
local rev_candidate = candidate:reverse()
local cutoff = H.get_config().cutoff
while pos_last do
-- Simulate computing best match positions ending exactly at `pos_last` by
-- going backwards from current last letter match. This works because it
-- minimizes width which is the only way to find match with lower score.
-- Not actually creating table with positions and then directly computing
-- score increases speed by up to 40% (on small frequent input word with
-- relatively wide candidate, such as file paths of nested directories).
local rev_first = n_candidate - pos_last + 1
for i = #letters - 1, 1, -1 do
rev_first = rev_candidate:find(letters[i], rev_first + 1)
end
local first = n_candidate - rev_first + 1
local width = math.min(pos_last - first + 1, cutoff)
-- Using strict sign is crucial because when two last letter matches result
-- into positions with similar width, the one which was created earlier
-- (i.e. with smaller last letter match) will have smaller first letter
-- match (hence better score).
if width < best_width then
best_pos_last, best_width = pos_last, width
end
-- Advance iteration
pos_last = candidate:find(letters[n_letters], pos_last + 1)
end
-- Actually compute best matched positions from best last letter match
local best_positions = { best_pos_last }
local rev_pos = n_candidate - best_pos_last + 1
for i = #letters - 1, 1, -1 do
rev_pos = rev_candidate:find(letters[i], rev_pos + 1)
-- For relatively small number of letters (around 10, which is main use
-- case) inserting to front seems to have better performance than
-- inserting at end and then reversing.
table.insert(best_positions, 1, n_candidate - rev_pos + 1)
end
return best_positions
end
-- Compute score of matched positions. Smaller values indicate better match
-- (i.e. like distance). Reasoning behind the score is for it to produce the
-- same ordering as with sequential comparison of match's width and first
-- position. So it shouldn't really be perceived as linear distance (difference
-- between scores don't really matter, only their comparison with each other).
--
-- Reasoning behind comparison logic (based on 'time' input):
-- - '_time' is better than 't_ime' (width is smaller).
-- - 'time_aa' is better than 'aa_time' (same width, first match is smaller).
--
-- Returns -1 if `positions` is `nil` or empty.
H.score_positions = function(positions)
if not positions or #positions == 0 then return -1 end
local first, last = positions[1], positions[#positions]
local cutoff = H.get_config().cutoff
return cutoff * math.min(last - first + 1, cutoff) + math.min(first, cutoff)
end
H.make_filter_indexes = function(word, candidate_array)
-- Precompute a table of word's letters
local letters = H.string_to_letters(word)
local res = {}
for i, cand in ipairs(candidate_array) do
local positions = H.find_best_positions(letters, cand)
if positions then table.insert(res, { index = i, score = H.score_positions(positions) }) end
end
return res
end
H.compare_filter_indexes = function(a, b)
if a.score < b.score then return true end
if a.score == b.score then
-- Make sorting stable by preserving index order
return a.index < b.index
end
return false
end
H.filter_by_indexes = function(candidate_array, ids)
local res, res_ids = {}, {}
for _, id in pairs(ids) do
table.insert(res, candidate_array[id.index])
table.insert(res_ids, id.index)
end
return res, res_ids
end
-- Utilities ------------------------------------------------------------------
H.string_to_letters = function(s) return vim.tbl_map(vim.pesc, vim.split(s, '')) end
return MiniFuzzy

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,386 @@
--- *mini.nvim* Collection of minimal, independent and fast Lua modules
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- |mini.nvim| is a collection of minimal, independent, and fast Lua modules
--- dedicated to improve Neovim (version 0.7 and higher) experience. Each
--- module can be considered as a separate sub-plugin.
---
--- Table of contents:
--- General overview ............................................... |mini.nvim|
--- Disabling recepies ........................... |mini.nvim-disabling-recipes|
--- Buffer-local config ........................ |mini.nvim-buffer-local-config|
--- Plugin colorschemes ................................... |mini-color-schemes|
--- Extend and create a/i textobjects ................................ |mini.ai|
--- Align text interactively ...................................... |mini.align|
--- Animate common Neovim actions ............................... |mini.animate|
--- Base16 colorscheme creation .................................. |mini.base16|
--- Common configuration presets ................................. |mini.basics|
--- Go forward/backward with square brackets .................. |mini.bracketed|
--- Remove buffers ............................................ |mini.bufremove|
--- Show next key clues ............................................ |mini.clue|
--- Tweak and save any color scheme .............................. |mini.colors|
--- Comment lines ............................................... |mini.comment|
--- Completion and signature help ............................ |mini.completion|
--- Autohighlight word under cursor .......................... |mini.cursorword|
--- Plugin manager ................................................. |mini.deps|
--- Work with diff hunks ........................................... |mini.diff|
--- Generate Neovim help files ...................................... |mini.doc|
--- Extra 'mini.nvim' functionality ............................... |mini.extra|
--- Navigate and manipulate file system............................ |mini.files|
--- Fuzzy matching ................................................ |mini.fuzzy|
--- Git integration ................................................. |mini.git|
--- Highlight patterns in text ............................... |mini.hipatterns|
--- Generate configurable color scheme ............................. |mini.hues|
--- Icon provider ................................................. |mini.icons|
--- Visualize and work with indent scope .................... |mini.indentscope|
--- Jump to next/previous single character ......................... |mini.jump|
--- Jump within visible lines .................................... |mini.jump2d|
--- Window with buffer text overview ................................ |mini.map|
--- Miscellaneous functions ........................................ |mini.misc|
--- Move any selection in any direction ............................ |mini.move|
--- Show notifications ........................................... |mini.notify|
--- Text edit operators ....................................... |mini.operators|
--- Autopairs ..................................................... |mini.pairs|
--- Pick anything .................................................. |mini.pick|
--- Session management ......................................... |mini.sessions|
--- Split and join arguments .................................. |mini.splitjoin|
--- Start screen ................................................ |mini.starter|
--- Statusline ............................................... |mini.statusline|
--- Surround actions ........................................... |mini.surround|
--- Tabline ..................................................... |mini.tabline|
--- Test Neovim plugins ............................................ |mini.test|
--- Trailspace (highlight and remove)......................... |mini.trailspace|
--- Track and reuse file system visits ........................... |mini.visits|
---
--- # General principles ~
---
--- - <Design>. Each module is designed to solve a particular problem targeting
--- balance between feature-richness (handling as many edge-cases as
--- possible) and simplicity of implementation/support. Granted, not all of
--- them ended up with the same balance, but it is the goal nevertheless.
---
--- - <Independence>. Modules are independent of each other and can be run
--- without external dependencies. Although some of them may need
--- dependencies for full experience.
---
--- - <Structure>. Each module is a submodule for a placeholder "mini" module. So,
--- for example, "surround" module should be referred to as "mini.surround".
--- As later will be explained, this plugin can also be referred to
--- as "MiniSurround".
---
--- - <Setup>:
--- - Each module you want to use should be enabled separately with
--- `require(<name of module>).setup({})`. Possibly replace `{}` with
--- your config table or omit altogether to use defaults. You can supply
--- only parts of config, the rest will be inferred from defaults.
---
--- - Call to module's `setup()` always creates a global Lua object with
--- coherent camel-case name: `require('mini.surround').setup()` creates
--- `_G.MiniSurround`. This allows for a simpler usage of plugin
--- functionality: instead of `require('mini.surround')` use
--- `MiniSurround` (or manually `:lua MiniSurround.*` in command line);
--- available from `v:lua` like `v:lua.MiniSurround`. Considering this,
--- "module" and "Lua object" names can be used interchangeably:
--- 'mini.surround' and 'MiniSurround' will mean the same thing.
---
--- - Each supplied `config` table (after extending with default values) is
--- stored in `config` field of global object. Like `MiniSurround.config`.
---
--- - Values of `config`, which affect runtime activity, can be changed on
--- the fly to have effect. For example, `MiniSurround.config.n_lines`
--- can be changed during runtime; but changing
--- `MiniSurround.config.mappings` won't have any effect (as mappings are
--- created once during `setup()`).
---
--- - <Buffer local configuration>. Each module can be additionally configured
--- to use certain runtime config settings locally to buffer.
--- See |mini.nvim-buffer-local-config| for more information.
---
--- - <Disabling>. Each module's core functionality can be disabled globally or
--- locally to buffer. See "Disabling" section in module's help page for more
--- details. See |mini.nvim-disabling-recipes| for common recipes.
---
--- - <Silencing>. Each module can be configured to not show non-error feedback
--- globally or locally to buffer. See "Silencing" section in module's help page
--- for more details.
---
--- - <Highlighting>. Appearance of module's output is controlled by certain set
--- of highlight groups (see |highlight-groups|). By default they usually link to
--- some semantically close built-in highlight group. Use |:highlight| command
--- or |nvim_set_hl()| Lua function to customize highlighting.
--- To see a more calibrated look, use |MiniHues|, |MiniBase16|, or plugin's
--- colorschemes.
---
--- - <Stability>. Each module upon release is considered to be relatively
--- stable: both in terms of setup and functionality. Any non-bugfix
--- backward-incompatible change will be released gradually as much as possible.
---
--- # List of modules ~
---
--- - |MiniAi| - extend and create `a`/`i` textobjects (like in `di(` or
--- `va"`). It enhances some builtin |text-objects| (like |a(|, |a)|, |a'|,
--- and more), creates new ones (like `a*`, `a<Space>`, `af`, `a?`, and
--- more), and allows user to create their own (like based on treesitter, and
--- more). Supports dot-repeat, `v:count`, different search methods,
--- consecutive application, and customization via Lua patterns or functions.
--- Has builtins for brackets, quotes, function call, argument, tag, user
--- prompt, and any punctuation/digit/whitespace character.
---
--- - |MiniAlign| - align text interactively (with or without instant preview).
--- Allows 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.
---
--- - |MiniAnimate| - animate common Neovim actions. Has "works out of the box"
--- builtin animations for cursor movement, scroll, resize, window open and
--- close. All of them can be customized and enabled/disabled independently.
---
--- - |MiniBase16| - fast implementation of base16 theme for manually supplied
--- palette. Supports 30+ plugin integrations. Has unique palette generator
--- which needs only background and foreground colors.
---
--- - |MiniBasics| - common configuration presets. Has configurable presets for
--- options, mappings, and autocommands. It doesn't change option or mapping
--- if it was manually created.
---
--- - |MiniBracketed| - go forward/backward with square brackets. Among others,
--- supports variety of non-trivial targets: comments, files on disk, indent
--- changes, tree-sitter nodes, linear undo states, yank history entries.
---
--- - |MiniBufremove| - buffer removing (unshow, delete, wipeout) while saving
--- window layout.
---
--- - |MiniClue| - show next key clues. Implements custom key query process with
--- customizable opt-in triggers, next key descriptions (clues), hydra-like
--- submodes, window delay/config. Provides clue sets for some built-in
--- concepts: `g`/`z` keys, window commands, etc.
---
--- - |MiniColors| - tweak and save any color scheme. Can create colorscheme
--- object with methods to invert/set/modify/etc.
--- lightness/saturation/hue/temperature/etc. of foreground/background/all
--- colors, infer cterm attributes, add transparency, save to a file and more.
--- Has functionality for interactive experiments and animation of
--- transition between color schemes.
---
--- - |MiniComment| - fast and familiar per-line code commenting.
---
--- - |MiniCompletion| - async (with customizable 'debounce' delay) 'two-stage
--- chain completion': first builtin LSP, then configurable fallback. Also
--- has functionality for completion item info and function signature (both
--- in floating window appearing after customizable delay).
---
--- - |MiniCursorword| - automatic highlighting of word under cursor (displayed
--- after customizable delay). Current word under cursor can be highlighted
--- differently.
---
--- - |MiniDeps| - plugin manager for plugins outside of 'mini.nvim'. Uses Git and
--- built-in packages to install, update, clean, and snapshot plugins.
---
--- - |MiniDiff| - visualize difference between buffer text and its reference
--- interactively (with colored signs or line numbers). Uses Git index as
--- default reference. Provides toggleable overview in text area, built-in
--- apply/reset/textobject/goto mappings.
---
--- - |MiniDoc| - generation of help files from EmmyLua-like annotations.
--- Allows flexible customization of output via hook functions. Used for
--- documenting this plugin.
---
--- - |MiniExtra| - extra 'mini.nvim' functionality. Contains 'mini.pick' pickers,
--- 'mini.ai' textobjects, and more.
---
--- - |MiniFiles| - navigate and manipulate file system. A file explorer with
--- column view capable of manipulating file system by editing text. Can
--- create/delete/rename/copy/move files/directories inside and across
--- directories. For full experience needs enabled |MiniIcons| module (but works
--- without it).
---
--- - |MiniFuzzy| - functions for fast and simple fuzzy matching. It has
--- not only functions to perform fuzzy matching of one string to others, but
--- also a sorter for |telescope.nvim|.
---
--- - |MiniGit| - Git integration (https://git-scm.com/). Implements tracking of
--- Git related data (root, branch, etc.), |:Git| command for better integration
--- with running Neovim instance, and various helpers to explore Git history.
---
--- - |MiniHipatterns| - highlight patterns in text with configurable highlighters
--- (pattern and/or highlight group can be string or callable).
--- Works asynchronously with configurable debounce delay.
---
--- - |MiniHues| - generate configurable color scheme. Takes only background
--- and foreground colors as required arguments. Can adjust number of hues
--- for non-base colors, saturation, accent color, plugin integration.
---
--- - |MiniIcons| - icon provider with fixed set of highlight groups.
--- Supports various categories, icon and style customizations, caching for
--- performance. Integrates with Neovim's filetype matching.
---
--- - |MiniIndentscope| - visualize and operate on indent scope. Supports
--- customization of debounce delay, animation style, and different
--- granularity of options for scope computing algorithm.
---
--- - |MiniJump| - minimal and fast module for smarter jumping to a single
--- character.
---
--- - |MiniJump2d| - minimal and fast Lua plugin for jumping (moving cursor)
--- within visible lines via iterative label filtering. Supports custom jump
--- targets (spots), labels, hooks, allowed windows and lines, and more.
---
--- - |MiniMap| - window with buffer text overview, scrollbar, and highlights.
--- Allows configurable symbols for line encode and scrollbar, extensible
--- highlight integration (with pre-built ones for builtin search, diagnostic,
--- git line status), window properties, and more.
---
--- - |MiniMisc| - collection of miscellaneous useful functions. Like `put()`
--- and `put_text()` which print Lua objects to command line and current
--- buffer respectively.
---
--- - |MiniMove| - move any selection in any direction. Supports any Visual
--- mode (charwise, linewise, blockwise) and Normal mode (current line) for
--- all four directions (left, right, down, up). Respects `count` and undo.
---
--- - |MiniNotify| - show one or more highlighted notifications in a single window.
--- Provides both low-level functions (add, update, remove, clear) and maker
--- of |vim.notify()| implementation. Sets up automated LSP progress updates.
---
--- - |MiniOperators| - various text edit operators: replace, exchange,
--- multiply, sort, evaluate. Creates mappings to operate on textobject,
--- line, and visual selection. Supports |[count]| and dot-repeat.
---
--- - |MiniPairs| - autopairs plugin which has minimal defaults and
--- functionality to do per-key expression mappings.
---
--- - |MiniPick| - general purpose interactive non-blocking picker with
--- toggleable preview. Has fast default matching with fuzzy/exact/grouped
--- modes. Provides most used built-in pickers for files, pattern matches,
--- buffers, etc. For full experience needs enabled |MiniIcons| module (but
--- works without it).
---
--- - |MiniSessions| - session management (read, write, delete) which works
--- using |mksession|. Implements both global (from configured directory) and
--- local (from current directory) sessions.
---
--- - |MiniSplitjoin| - split and join arguments (regions inside brackets
--- between allowed separators). Has customizable pre and post hooks.
--- Works inside comments.
---
--- - |MiniStarter| - minimal, fast, and flexible start screen. Displayed items
--- are fully customizable both in terms of what they do and how they look
--- (with reasonable defaults). Item selection can be done using prefix query
--- with instant visual feedback.
---
--- - |MiniStatusline| - minimal and fast statusline. Has ability to use custom
--- content supplied with concise function (using module's provided section
--- functions) along with builtin default. For full experience needs
--- enabled |MiniDiff|, |MiniGit|, and |MiniIcons| modules (but works without
--- any of them).
---
--- - |MiniSurround| - fast and feature-rich surround plugin. Add, delete,
--- replace, find, highlight surrounding (like pair of parenthesis, quotes,
--- etc.). Supports dot-repeat, `v:count`, different search methods,
--- "last"/"next" extended mappings, customization via Lua patterns or
--- functions, and more. Has builtins for brackets, function call, tag, user
--- prompt, and any alphanumeric/punctuation/whitespace character.
---
--- - |MiniTest| - framework for writing extensive Neovim plugin tests.
--- Supports hierarchical tests, hooks, parametrization, filtering (like from
--- current file or cursor position), screen tests, "busted-style" emulation,
--- customizable reporters, and more. Designed to be used with provided
--- wrapper for managing child Neovim processes.
---
--- - |MiniTabline| - minimal tabline which always shows listed (see 'buflisted')
--- buffers. Allows showing extra information section in case of multiple vim
--- tabpages. For full experience needs enabled |MiniIcons| module (but works
--- without it).
---
--- - |MiniTrailspace| - automatic highlighting of trailing whitespace with
--- functionality to remove it.
---
--- - |MiniVisits| - track and reuse file system visits. Tracks data about each
--- file/directory visit (after delay) and stores it (only) locally. This can be
--- used to get a list of "recent"/"frequent"/"frecent" visits.
--- Allows persistently adding labels to visits enabling flexible workflow.
--- Common recipes for disabling functionality
---
--- Each module's core functionality can be disabled globally or buffer-locally
--- by creating appropriate global or buffer-scoped variables equal to |v:true|.
--- Functionality is disabled if at least one of |g:| or |b:| variables is `v:true`.
---
--- Variable names have the same structure: `{g,b}:mini*_disable` where `*` is
--- module's lowercase name. For example, `g:minianimate_disable` disables
--- |mini.animate| globally and `b:minianimate_disable` - for current buffer.
--- Note: in this section disabling 'mini.animate' is used as example;
--- everything holds for other module variables.
---
--- Considering high number of different scenarios and customization intentions,
--- writing exact rules for disabling module's functionality is left to user.
---
--- # Manual disabling ~
--- >lua
--- -- Disable globally
--- vim.g.minianimate_disable = true
---
--- -- Disable for current buffer
--- vim.b.minianimate_disable = true
---
--- -- Toggle (disable if enabled, enable if disabled)
--- vim.g.minianimate_disable = not vim.g.minianimate_disable -- globally
--- vim.b.minianimate_disable = not vim.b.minianimate_disable -- for buffer
--- <
--- # Automated disabling ~
---
--- Automated disabling is suggested to be done inside autocommands: >lua
---
--- -- Disable for a certain filetype (for example, "lua")
--- local f = function(args) vim.b[args.buf].minianimate_disable = true end
--- vim.api.nvim_create_autocmd('Filetype', { pattern = 'lua', callback = f })
---
--- -- Enable only for certain filetypes (for example, "lua" and "help")
--- local f = function(args)
--- local ft = vim.bo[args.buf].filetype
--- if ft == 'lua' or ft == 'help' then return end
--- vim.b[args.buf].minianimate_disable = true
--- end
--- vim.api.nvim_create_autocmd('Filetype', { callback = f })
---
--- -- Disable in Visual mode
--- local f_en = function(args) vim.b[args.buf].minianimate_disable = false end
--- local enable_opts = { pattern = '[vV\x16]*:*', callback = f_en }
--- vim.api.nvim_create_autocmd('ModeChanged', enable_opts)
---
--- local f_dis = function(args) vim.b[args.buf].minianimate_disable = true end
--- local disable_opts = { pattern = '*:[vV\x16]*', callback = f_dis }
--- vim.api.nvim_create_autocmd('ModeChanged', disable_opts)
---
--- -- Disable in Terminal buffer
--- local f = function(args) vim.b[args.buf].minianimate_disable = true end
--- vim.api.nvim_create_autocmd('TermOpen', { callback = f })
--- <
---@tag mini.nvim-disabling-recipes
--- Buffer local config
---
--- Each module can be additionally configured locally to buffer by creating
--- appropriate buffer-scoped variable with values you want to override. It
--- will affect only runtime options and not those used once during setup (like
--- `mappings` or `set_vim_settings`).
---
--- Variable names have the same structure: `b:mini*_config` where `*` is
--- module's lowercase name. For example, `b:minianimate_config` can store
--- information about how |mini.animate| will act inside current buffer. Its
--- value should be a table with same structure as module's `config`. Example: >lua
---
--- -- Disable scroll animation in current buffer
--- vim.b.minianimate_config = { scroll = { enable = false } }
--- <
--- Considering high number of different scenarios and customization intentions,
--- writing exact rules for module's buffer local configuration is left to
--- user. It is done in similar fashion to |mini.nvim-disabling-recipes|.
---@tag mini.nvim-buffer-local-config
vim.notify([[Do not `require('mini')` directly. Setup every module separately.]])
return {}

View File

@ -0,0 +1,541 @@
--- *mini.jump* Jump to next/previous single character
--- *MiniJump*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski, Adam Blažek
---
--- ==============================================================================
---
--- Features:
--- - Extend f, F, t, T to work on multiple lines.
---
--- - Repeat jump by pressing f, F, t, T again. It is reset when cursor moved
--- as a result of not jumping or timeout after idle time (duration
--- customizable).
---
--- - Highlight (after customizable delay) all possible target characters and
--- stop it after some (customizable) idle time.
---
--- - Normal, Visual, and Operator-pending (with full dot-repeat) modes are
--- supported.
---
--- This module follows vim's 'ignorecase' and 'smartcase' options. When
--- 'ignorecase' is set, f, F, t, T will match case-insensitively. When
--- 'smartcase' is also set, f, F, t, T will only match lowercase
--- characters case-insensitively.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.jump').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniJump` which you can use for scripting or manually (with
--- `:lua MiniJump.*`).
---
--- See |MiniJump.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minijump_config` which should have same structure as
--- `MiniJump.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Highlight groups ~
---
--- * `MiniJump` - all possible cursor positions.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable core functionality, set `vim.g.minijump_disable` (globally) or
--- `vim.b.minijump_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.
---@alias __jump_target string|nil The string to jump to.
---@alias __jump_backward boolean|nil Whether to jump backward.
---@alias __jump_till boolean|nil Whether to jump just before/after the match instead of
--- exactly on target. This includes positioning cursor past the end of
--- previous/current line. Note that with backward jump this might lead to
--- cursor being on target if can't be put past the line.
---@alias __jump_n_times number|nil Number of times to perform consecutive jumps.
-- Module definition ==========================================================
local MiniJump = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniJump.config|.
---
---@usage >lua
--- require('mini.jump').setup() -- use default config
--- -- OR
--- require('mini.jump').setup({}) -- replace {} with your config table
--- <
MiniJump.setup = function(config)
-- Export module
_G.MiniJump = MiniJump
-- 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)
MiniJump.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
forward = 'f',
backward = 'F',
forward_till = 't',
backward_till = 'T',
repeat_jump = ';',
},
-- Delay values (in ms) for different functionalities. Set any of them to
-- a very big number (like 10^7) to virtually disable.
delay = {
-- Delay between jump and highlighting all possible jumps
highlight = 250,
-- Delay between jump and automatic stop if idle (no jump is done)
idle_stop = 10000000,
},
-- Whether to disable showing non-error feedback
silent = false,
}
--minidoc_afterlines_end
-- Module data ================================================================
--- Data about jumping state
---
--- It stores various information used in this module. All elements, except
--- `jumping`, is about the latest jump. They are used as default values for
--- similar arguments.
---
---@class JumpingState
---
---@field target __jump_target
---@field backward __jump_backward
---@field till __jump_till
---@field n_times __jump_n_times
---@field mode string Mode of latest jump (output of |mode()| with non-zero argument).
---@field jumping boolean Whether module is currently in "jumping mode": usage of
--- |MiniJump.smart_jump| and all mappings won't require target.
---@text
--- Initial values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniJump.state = {
target = nil,
backward = false,
till = false,
n_times = 1,
mode = nil,
jumping = false,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Jump to target
---
--- Takes a string and jumps to its first occurrence in desired direction.
---
--- All default values are taken from |MiniJump.state| to emulate latest jump.
---
---@param target __jump_target
---@param backward __jump_backward
---@param till __jump_till
---@param n_times __jump_n_times
MiniJump.jump = function(target, backward, till, n_times)
if H.is_disabled() then return end
-- Cache inputs for future use
H.update_state(target, backward, till, n_times)
if MiniJump.state.target == nil then
H.message('Can not jump because there is no recent `target`.')
return
end
-- Determine if target is present anywhere in order to correctly enter
-- jumping mode. If not, jumping mode is not possible.
local search_pattern = [[\V]] .. vim.fn.escape(MiniJump.state.target, [[\]])
local target_is_present = vim.fn.search(search_pattern, 'wn') ~= 0
if not target_is_present then return end
-- Construct search and highlight pattern data
local pattern, hl_pattern, flags = H.make_search_data()
-- Delay highlighting after stopping previous one
local config = H.get_config()
H.timers.highlight:stop()
H.timers.highlight:start(
-- Update highlighting immediately if any highlighting is already present
H.is_highlighting() and 0 or config.delay.highlight,
0,
vim.schedule_wrap(function() H.highlight(hl_pattern) end)
)
-- Start idle timer after stopping previous one
H.timers.idle_stop:stop()
H.timers.idle_stop:start(config.delay.idle_stop, 0, vim.schedule_wrap(function() MiniJump.stop_jumping() end))
-- Make jump(s)
H.cache.n_cursor_moved = 0
local init_cursor_data = H.get_cursor_data()
MiniJump.state.jumping = true
for _ = 1, MiniJump.state.n_times do
vim.fn.search(pattern, flags)
end
-- Open enough folds to show jump
vim.cmd('normal! zv')
-- Track cursor position to account for movement not caught by `CursorMoved`
H.cache.latest_cursor = H.get_cursor_data()
H.cache.has_changed_cursor = not vim.deep_equal(H.cache.latest_cursor, init_cursor_data)
end
--- Make smart jump
---
--- If the last movement was a jump, perform another jump with the same target.
--- Otherwise, wait for a target input (via |getcharstr()|). Respects |v:count|.
---
--- All default values are taken from |MiniJump.state| to emulate latest jump.
---
---@param backward __jump_backward
---@param till __jump_till
MiniJump.smart_jump = function(backward, till)
if H.is_disabled() then return end
-- Jumping should stop after mode change (use `mode(1)` to track 'omap' case)
-- or if cursor has moved after latest jump
local has_changed_mode = MiniJump.state.mode ~= vim.fn.mode(1)
local has_changed_cursor = not vim.deep_equal(H.cache.latest_cursor, H.get_cursor_data())
if has_changed_mode or has_changed_cursor then MiniJump.stop_jumping() end
-- Ask for target only when needed
local target
if not MiniJump.state.jumping or MiniJump.state.target == nil then
target = H.get_target()
-- Stop if user supplied invalid target
if target == nil then return end
end
H.update_state(target, backward, till, vim.v.count1)
MiniJump.jump()
end
--- Stop jumping
---
--- Removes highlights (if any) and forces the next smart jump to prompt for
--- the target. Automatically called on appropriate Neovim |events|.
MiniJump.stop_jumping = function()
H.timers.highlight:stop()
H.timers.idle_stop:stop()
MiniJump.state.jumping = false
H.cache.n_cursor_moved = 0
H.cache.latest_cursor = nil
H.cache.msg_shown = false
H.unhighlight()
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniJump.config)
-- Cache for various operations
H.cache = {
-- Counter of number of CursorMoved events
n_cursor_moved = 0,
-- Latest cursor position data
latest_cursor = nil,
-- Whether helper message was shown
msg_shown = false,
}
-- Timers for different delay-related functionalities
H.timers = { highlight = vim.loop.new_timer(), idle_stop = vim.loop.new_timer() }
-- Information about last match highlighting (stored *per window*):
-- - Key: windows' unique buffer identifiers.
-- - Value: table with:
-- - `id` field for match id (from `vim.fn.matchadd()`).
-- - `pattern` field for highlighted pattern.
H.window_matches = {}
-- 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({
mappings = { config.mappings, 'table' },
delay = { config.delay, 'table' },
silent = { config.silent, 'boolean' },
})
vim.validate({
['delay.highlight'] = { config.delay.highlight, 'number' },
['delay.idle_stop'] = { config.delay.idle_stop, 'number' },
['mappings.forward'] = { config.mappings.forward, 'string' },
['mappings.backward'] = { config.mappings.backward, 'string' },
['mappings.forward_till'] = { config.mappings.forward_till, 'string' },
['mappings.backward_till'] = { config.mappings.backward_till, 'string' },
['mappings.repeat_jump'] = { config.mappings.repeat_jump, 'string' },
})
return config
end
H.apply_config = function(config)
MiniJump.config = config
--stylua: ignore start
H.map('n', config.mappings.forward, '<Cmd>lua MiniJump.smart_jump(false, false)<CR>', { desc = 'Jump forward' })
H.map('n', config.mappings.backward, '<Cmd>lua MiniJump.smart_jump(true, false)<CR>', { desc = 'Jump backward' })
H.map('n', config.mappings.forward_till, '<Cmd>lua MiniJump.smart_jump(false, true)<CR>', { desc = 'Jump forward till' })
H.map('n', config.mappings.backward_till, '<Cmd>lua MiniJump.smart_jump(true, true)<CR>', { desc = 'Jump backward till' })
H.map('n', config.mappings.repeat_jump, '<Cmd>lua MiniJump.jump()<CR>', { desc = 'Repeat jump' })
H.map('x', config.mappings.forward, '<Cmd>lua MiniJump.smart_jump(false, false)<CR>', { desc = 'Jump forward' })
H.map('x', config.mappings.backward, '<Cmd>lua MiniJump.smart_jump(true, false)<CR>', { desc = 'Jump backward' })
H.map('x', config.mappings.forward_till, '<Cmd>lua MiniJump.smart_jump(false, true)<CR>', { desc = 'Jump forward till' })
H.map('x', config.mappings.backward_till, '<Cmd>lua MiniJump.smart_jump(true, true)<CR>', { desc = 'Jump backward till' })
H.map('x', config.mappings.repeat_jump, '<Cmd>lua MiniJump.jump()<CR>', { desc = 'Repeat jump' })
H.map('o', config.mappings.forward, H.make_expr_jump(false, false), { expr = true, desc = 'Jump forward' })
H.map('o', config.mappings.backward, H.make_expr_jump(true, false), { expr = true, desc = 'Jump backward' })
H.map('o', config.mappings.forward_till, H.make_expr_jump(false, true), { expr = true, desc = 'Jump forward till' })
H.map('o', config.mappings.backward_till, H.make_expr_jump(true, true), { expr = true, desc = 'Jump backward till' })
H.map('o', config.mappings.repeat_jump, H.make_expr_jump(), { expr = true, desc = 'Repeat jump' })
--stylua: ignore end
end
H.create_autocommands = function()
local augroup = vim.api.nvim_create_augroup('MiniJump', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
au('CursorMoved', '*', H.on_cursormoved, 'On CursorMoved')
au({ 'BufLeave', 'InsertEnter' }, '*', MiniJump.stop_jumping, 'Stop jumping')
end
H.create_default_hl = function() vim.api.nvim_set_hl(0, 'MiniJump', { default = true, link = 'SpellRare' }) end
H.is_disabled = function() return vim.g.minijump_disable == true or vim.b.minijump_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniJump.config, vim.b.minijump_config or {}, config or {})
end
-- Mappings -------------------------------------------------------------------
H.make_expr_jump = function(backward, till)
return function()
if H.is_disabled() then return '' end
-- Ask for `target` for non-repeating jump as this will be used only in
-- operator-pending mode. Dot-repeat is supported via expression-mapping.
local is_repeat_jump = backward == nil or till == nil
local target = is_repeat_jump and MiniJump.state.target or H.get_target()
-- Stop if user supplied invalid target
if target == nil then return '<Esc>' end
H.update_state(target, backward, till, vim.v.count1)
vim.schedule(function()
if H.cache.has_changed_cursor then return end
vim.cmd('undo!')
end)
return 'v<Cmd>lua MiniJump.jump()<CR>'
end
end
-- Autocommands ---------------------------------------------------------------
H.on_cursormoved = function()
-- Check if jumping to avoid unnecessary actions on every CursorMoved
if MiniJump.state.jumping then
H.cache.n_cursor_moved = H.cache.n_cursor_moved + 1
-- Stop jumping only if `CursorMoved` was not a result of smart jump
if H.cache.n_cursor_moved > 1 then MiniJump.stop_jumping() end
end
end
-- Pattern matching -----------------------------------------------------------
H.make_search_data = function()
local target = vim.fn.escape(MiniJump.state.target, [[\]])
local backward, till = MiniJump.state.backward, MiniJump.state.till
local flags = backward and 'Wb' or 'W'
local pattern, hl_pattern
if till then
-- General logic: moving pattern should match just before/after target,
-- while highlight pattern should match target for every "movable" place.
-- Also allow checking for "just before/after" across lines by accepting
-- `\n` as possible match.
if backward then
-- NOTE: use `\@<=` instead of `\zs` because it behaves better in case of
-- consecutive matches (like `xxxx` for target `x`)
pattern = target .. [[\@<=\_.]]
hl_pattern = target .. [[\ze\_.]]
else
pattern = [[\_.\ze]] .. target
hl_pattern = [[\_.\@<=]] .. target
end
else
local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode())
local is_exclusive = vim.o.selection == 'exclusive'
if not backward and is_visual and is_exclusive then
-- Still select target in case of exclusive visual selection
pattern = target .. [[\zs\_.]]
hl_pattern = target .. [[\ze\_.]]
else
pattern = target
hl_pattern = target
end
end
-- Enable 'very nomagic' mode and possibly case-insensitivity
local ignore_case = vim.o.ignorecase and (not vim.o.smartcase or target == target:lower())
local prefix = ignore_case and [[\V\c]] or [[\V]]
pattern, hl_pattern = prefix .. pattern, prefix .. hl_pattern
return pattern, hl_pattern, flags
end
-- Highlighting ---------------------------------------------------------------
H.highlight = function(pattern)
-- Don't do anything if already highlighting input pattern
if H.is_highlighting(pattern) then return end
-- Stop highlighting possible previous pattern. Needed to adjust highlighting
-- when inside jumping but a different kind one. Example: first jump with
-- `till = false` and then, without jumping stop, jump to same character with
-- `till = true`. If this character is first on line, highlighting should change
H.unhighlight()
-- Never highlight in Insert mode
if vim.fn.mode() == 'i' then return end
local match_id = vim.fn.matchadd('MiniJump', pattern)
H.window_matches[vim.api.nvim_get_current_win()] = { id = match_id, pattern = pattern }
end
H.unhighlight = function()
-- Remove highlighting from all windows as jumping is intended to work only
-- in current window. This will work also from other (usually popup) window.
for win_id, match_info in pairs(H.window_matches) do
if vim.api.nvim_win_is_valid(win_id) then
-- Use `pcall` because there is an error if match id is not present. It
-- can happen if something else called `clearmatches`.
pcall(vim.fn.matchdelete, match_info.id, win_id)
H.window_matches[win_id] = nil
end
end
end
---@param pattern string|nil Highlight pattern to check for. If `nil`, checks for
--- any highlighting registered in current window.
---@private
H.is_highlighting = function(pattern)
local win_id = vim.api.nvim_get_current_win()
local match_info = H.window_matches[win_id]
if match_info == nil then return false end
return pattern == nil or match_info.pattern == pattern
end
-- Utilities ------------------------------------------------------------------
H.echo = function(msg, is_important)
if H.get_config().silent then return end
-- Construct message chunks
msg = type(msg) == 'string' and { { msg } } or msg
table.insert(msg, 1, { '(mini.jump) ', 'WarningMsg' })
-- Avoid hit-enter-prompt
local chunks = msg
if not is_important then
chunks = {}
local max_width = vim.o.columns * math.max(vim.o.cmdheight - 1, 0) + vim.v.echospace
local tot_width = 0
for _, ch in ipairs(msg) do
local new_ch = { vim.fn.strcharpart(ch[1], 0, max_width - tot_width), ch[2] }
table.insert(chunks, new_ch)
tot_width = tot_width + vim.fn.strdisplaywidth(new_ch[1])
if tot_width >= max_width then break end
end
end
-- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
vim.cmd([[echo '' | redraw]])
vim.api.nvim_echo(chunks, is_important, {})
end
H.unecho = function()
if H.cache.msg_shown then vim.cmd([[echo '' | redraw]]) end
end
H.message = function(msg) H.echo(msg, true) end
H.update_state = function(target, backward, till, n_times)
MiniJump.state.mode = vim.fn.mode(1)
-- Don't use `? and <1> or <2>` because it doesn't work when `<1>` is `false`
if target ~= nil then MiniJump.state.target = target end
if backward ~= nil then MiniJump.state.backward = backward end
if till ~= nil then MiniJump.state.till = till end
if n_times ~= nil then MiniJump.state.n_times = n_times end
end
H.get_cursor_data = function() return { vim.api.nvim_get_current_win(), vim.api.nvim_win_get_cursor(0) } end
H.get_target = function()
local needs_help_msg = true
vim.defer_fn(function()
if not needs_help_msg then return end
H.echo('Enter target single character ')
H.cache.msg_shown = true
end, 1000)
local ok, char = pcall(vim.fn.getcharstr)
needs_help_msg = false
H.unecho()
-- Terminate if couldn't get input (like with <C-c>) or it is `<Esc>`
if not ok or char == '\27' then return end
return char
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
return MiniJump

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,645 @@
--- *mini.misc* Miscellaneous functions
--- *MiniMisc*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features the following functions:
--- - |MiniMisc.bench_time()| to benchmark function execution time.
--- Useful in combination with `stat_summary()`.
---
--- - |MiniMisc.put()| and |MiniMisc.put_text()| to pretty print its arguments
--- into command line and current buffer respectively.
---
--- - |MiniMisc.setup_auto_root()| to set up automated change of current directory.
---
--- - |MiniMisc.setup_termbg_sync()| to set up terminal background synchronization
--- (removes possible "frame" around current Neovim instance).
---
--- - |MiniMisc.setup_restore_cursor()| to set up automated restoration of
--- cursor position on file reopen.
---
--- - |MiniMisc.stat_summary()| to compute summary statistics of numerical array.
--- Useful in combination with `bench_time()`.
---
--- - |MiniMisc.tbl_head()| and |MiniMisc.tbl_tail()| to return "first" and "last"
--- elements of table.
---
--- - |MiniMisc.zoom()| to zoom in and out of a buffer, making it full screen
--- in a floating window.
---
--- - And more.
---
--- # Setup ~
---
--- This module doesn't need setup, but it can be done to improve usability.
--- Setup with `require('mini.misc').setup({})` (replace `{}` with your
--- `config` table). It will create global Lua table `MiniMisc` which you can
--- use for scripting or manually (with `:lua MiniMisc.*`).
---
--- See |MiniMisc.config| for `config` structure and default values.
---
--- This module doesn't have runtime options, so using `vim.b.minimisc_config`
--- will have no effect here.
-- Module definition ==========================================================
local MiniMisc = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniMisc.config|.
---
---@usage >lua
--- require('mini.misc').setup() -- use default config
--- -- OR
--- require('mini.misc').setup({}) -- replace {} with your config table
--- <
MiniMisc.setup = function(config)
-- Export module
_G.MiniMisc = MiniMisc
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniMisc.config = {
-- Array of fields to make global (to be used as independent variables)
make_global = { 'put', 'put_text' },
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Execute `f` several times and time how long it took
---
---@param f function Function which execution to benchmark.
---@param n number|nil Number of times to execute `f(...)`. Default: 1.
---@param ... any Arguments when calling `f`.
---
---@return ... Table with durations (in seconds; up to nanoseconds) and
--- output of (last) function execution.
MiniMisc.bench_time = function(f, n, ...)
n = n or 1
local durations, output = {}, nil
for _ = 1, n do
local start_time = vim.loop.hrtime()
output = f(...)
local end_time = vim.loop.hrtime()
table.insert(durations, 0.000000001 * (end_time - start_time))
end
return durations, output
end
--- Compute width of gutter (info column on the left of the window)
---
---@param win_id number|nil Window identifier (see |win_getid()|) for which gutter
--- width is computed. Default: 0 for current.
MiniMisc.get_gutter_width = function(win_id)
win_id = (win_id == nil or win_id == 0) and vim.api.nvim_get_current_win() or win_id
return vim.fn.getwininfo(win_id)[1].textoff
end
--- Print Lua objects in command line
---
---@param ... any Any number of objects to be printed each on separate line.
MiniMisc.put = function(...)
local objects = {}
-- Not using `{...}` because it removes `nil` input
for i = 1, select('#', ...) do
local v = select(i, ...)
table.insert(objects, vim.inspect(v))
end
print(table.concat(objects, '\n'))
return ...
end
--- Print Lua objects in current buffer
---
---@param ... any Any number of objects to be printed each on separate line.
MiniMisc.put_text = function(...)
local objects = {}
-- Not using `{...}` because it removes `nil` input
for i = 1, select('#', ...) do
local v = select(i, ...)
table.insert(objects, vim.inspect(v))
end
local lines = vim.split(table.concat(objects, '\n'), '\n')
local lnum = vim.api.nvim_win_get_cursor(0)[1]
vim.fn.append(lnum, lines)
return ...
end
--- Resize window to have exact number of editable columns
---
---@param win_id number|nil Window identifier (see |win_getid()|) to be resized.
--- Default: 0 for current.
---@param text_width number|nil Number of editable columns resized window will
--- display. Default: first element of 'colorcolumn' or otherwise 'textwidth'
--- (using screen width as its default but not more than 79).
MiniMisc.resize_window = function(win_id, text_width)
win_id = win_id or 0
text_width = text_width or H.default_text_width(win_id)
vim.api.nvim_win_set_width(win_id, text_width + MiniMisc.get_gutter_width(win_id))
end
H.default_text_width = function(win_id)
local buf = vim.api.nvim_win_get_buf(win_id)
local textwidth = vim.bo[buf].textwidth
textwidth = (textwidth == 0) and math.min(vim.o.columns, 79) or textwidth
local colorcolumn = vim.wo[win_id].colorcolumn
if colorcolumn ~= '' then
local cc = vim.split(colorcolumn, ',')[1]
local is_cc_relative = vim.tbl_contains({ '-', '+' }, cc:sub(1, 1))
if is_cc_relative then
return textwidth + tonumber(cc)
else
return tonumber(cc)
end
else
return textwidth
end
end
--- Set up automated change of current directory
---
--- What it does:
--- - Creates autocommand which on every |BufEnter| event with |MiniMisc.find_root()|
--- finds root directory for current buffer file and sets |current-directory|
--- to it (using |chdir()|).
--- - Resets |autochdir| to `false`.
---
---@param names table|function|nil Forwarded to |MiniMisc.find_root()|.
---@param fallback function|nil Forwarded to |MiniMisc.find_root()|.
---
---@usage >lua
--- require('mini.misc').setup()
--- MiniMisc.setup_auto_root()
--- <
MiniMisc.setup_auto_root = function(names, fallback)
names = names or { '.git', 'Makefile' }
if not (H.is_array_of(names, H.is_string) or vim.is_callable(names)) then
H.error('Argument `names` of `setup_auto_root()` should be array of string file names or a callable.')
end
fallback = fallback or function() return nil end
if not vim.is_callable(fallback) then H.error('Argument `fallback` of `setup_auto_root()` should be callable.') end
-- Disable conflicting option
vim.o.autochdir = false
-- Create autocommand
local set_root = function(data)
local root = MiniMisc.find_root(data.buf, names, fallback)
if root == nil then return end
vim.fn.chdir(root)
end
local augroup = vim.api.nvim_create_augroup('MiniMiscAutoRoot', {})
vim.api.nvim_create_autocmd(
'BufEnter',
{ group = augroup, callback = set_root, desc = 'Find root and change current directory' }
)
end
--- Find root directory
---
--- Based on a buffer name (full path to file opened in a buffer) find a root
--- directory. If buffer is not associated with file, returns `nil`.
---
--- Root directory is a directory containing at least one of pre-defined files.
--- It is searched using |vim.fn.find()| with `upward = true` starting from
--- directory of current buffer file until first occurrence of root file(s).
---
--- Notes:
--- - Uses directory path caching to speed up computations. This means that no
--- changes in root directory will be detected after directory path was already
--- used in this function. Reload Neovim to account for that.
---
---@param buf_id number|nil Buffer identifier (see |bufnr()|) to use.
--- Default: 0 for current.
---@param names table|function|nil Array of file names or a callable used to
--- identify a root directory. Forwarded to |vim.fs.find()|.
--- Default: `{ '.git', 'Makefile' }`.
---@param fallback function|nil Callable fallback to use if no root is found
--- with |vim.fs.find()|. Will be called with a buffer path and should return
--- a valid directory path.
MiniMisc.find_root = function(buf_id, names, fallback)
buf_id = buf_id or 0
names = names or { '.git', 'Makefile' }
fallback = fallback or function() return nil end
if type(buf_id) ~= 'number' then H.error('Argument `buf_id` of `find_root()` should be number.') end
if not (H.is_array_of(names, H.is_string) or vim.is_callable(names)) then
H.error('Argument `names` of `find_root()` should be array of string file names or a callable.')
end
if not vim.is_callable(fallback) then H.error('Argument `fallback` of `find_root()` should be callable.') end
-- Compute directory to start search from. NOTEs on why not using file path:
-- - This has better performance because `vim.fs.find()` is called less.
-- - *Needs* to be a directory for callable `names` to work.
-- - Later search is done including initial `path` if directory, so this
-- should work for detecting buffer directory as root.
local path = vim.api.nvim_buf_get_name(buf_id)
if path == '' then return end
local dir_path = vim.fs.dirname(path)
-- Try using cache
local res = H.root_cache[dir_path]
if res ~= nil then return res end
-- Find root
local root_file = vim.fs.find(names, { path = dir_path, upward = true })[1]
if root_file ~= nil then
res = vim.fs.dirname(root_file)
else
res = fallback(path)
end
-- Use absolute path to an existing directory
if type(res) ~= 'string' then return end
res = vim.fn.fnamemodify(res, ':p')
if vim.fn.isdirectory(res) == 0 then return end
-- Cache result per directory path
H.root_cache[dir_path] = res
return res
end
H.root_cache = {}
--- Set up terminal background synchronization
---
--- What it does:
--- - Checks if terminal emulator supports OSC 11 control sequence. Stops if not.
--- - Creates |UIEnter| and |ColorScheme| autocommands which change terminal
--- background to have same color as |guibg| of |hl-Normal|.
--- - Creates |UILeave| autocommand which sets terminal background back to the
--- color at the time this function was called first time in current session.
--- - Synchronizes background immediately to allow not depend on loading order.
---
--- Primary use case is to remove possible "frame" around current Neovim instance
--- which appears if Neovim's |hl-Normal| background color differs from what is
--- used by terminal emulator itself.
---
--- Make sure to call it only during interactive session in terminal emulator.
MiniMisc.setup_termbg_sync = function()
local augroup = vim.api.nvim_create_augroup('MiniMiscTermbgSync', { clear = true })
local f = function(args)
local ok, bg_init = pcall(H.parse_osc11, args.data)
if not (ok and type(bg_init) == 'string') then
H.notify('`setup_termbg_sync()` could not parse terminal emulator response ' .. vim.inspect(args.data), 'WARN')
return
end
-- Set up sync
local sync = function()
local normal = vim.api.nvim_get_hl_by_name('Normal', true)
if normal.background == nil then return end
io.write(string.format('\027]11;#%06x\007', normal.background))
end
vim.api.nvim_create_autocmd({ 'UIEnter', 'ColorScheme' }, { group = augroup, callback = sync })
-- Set up reset to the color returned from the very first call
H.termbg_init = H.termbg_init or bg_init
local reset = function() io.write('\027]11;' .. H.termbg_init .. '\007') end
vim.api.nvim_create_autocmd({ 'UILeave' }, { group = augroup, callback = reset })
-- Sync immediately
sync()
end
-- Ask about current background color and process the response
local id = vim.api.nvim_create_autocmd('TermResponse', { group = augroup, callback = f, once = true, nested = true })
io.write('\027]11;?\007')
vim.defer_fn(function()
local ok = pcall(vim.api.nvim_del_autocmd, id)
if ok then H.notify('`setup_termbg_sync()` did not get response from terminal emulator', 'WARN') end
end, 1000)
end
-- Source: 'runtime/lua/vim/_defaults.lua' in Neovim source
H.parse_osc11 = function(x)
local r, g, b = x:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$')
if not (r and g and b) then
local a
r, g, b, a = x:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$')
if not (a and a:len() <= 4) then return end
end
if not (r and g and b) then return end
if not (r:len() <= 4 and g:len() <= 4 and b:len() <= 4) then return end
local parse_osc_hex = function(c) return c:len() == 1 and (c .. c) or c:sub(1, 2) end
return '#' .. parse_osc_hex(r) .. parse_osc_hex(g) .. parse_osc_hex(b)
end
--- Restore cursor position on file open
---
--- When reopening a file this will make sure the cursor is placed back to the
--- position where you left before. This implements |restore-cursor| in a nicer way.
--- File should have a recognized file type (see 'filetype') and be opened in
--- a normal buffer (see 'buftype').
---
--- Note: it relies on file mark data stored in 'shadafile' (see |shada-f|).
--- Be sure to enable it.
---
---@param opts table|nil Options for |MiniMisc.restore_cursor|. Possible fields:
--- - <center> - (boolean) Center the window after we restored the cursor.
--- Default: `true`.
--- - <ignore_filetype> - Array with file types to be ignored (see 'filetype').
--- Default: `{ "gitcommit", "gitrebase" }`.
---
---@usage >lua
--- require('mini.misc').setup_restore_cursor()
--- <
MiniMisc.setup_restore_cursor = function(opts)
opts = opts or {}
opts.ignore_filetype = opts.ignore_filetype or { 'gitcommit', 'gitrebase' }
if not H.is_array_of(opts.ignore_filetype, H.is_string) then
H.error('In `setup_restore_cursor()` `opts.ignore_filetype` should be an array of strings.')
end
if opts.center == nil then opts.center = true end
if type(opts.center) ~= 'boolean' then H.error('In `setup_restore_cursor()` `opts.center` should be a boolean.') end
-- Create autocommand which runs once on `FileType` for every new buffer
local augroup = vim.api.nvim_create_augroup('MiniMiscRestoreCursor', {})
vim.api.nvim_create_autocmd('BufReadPre', {
group = augroup,
callback = function(data)
vim.api.nvim_create_autocmd('FileType', {
buffer = data.buf,
once = true,
callback = function() H.restore_cursor(opts) end,
})
end,
})
end
H.restore_cursor = function(opts)
-- Stop if not a normal buffer
if vim.bo.buftype ~= '' then return end
-- Stop if filetype is ignored
if vim.tbl_contains(opts.ignore_filetype, vim.bo.filetype) then return end
-- Stop if line is already specified (like during start with `nvim file +num`)
local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
if cursor_line > 1 then return end
-- Stop if can't restore proper line for some reason
local mark_line = vim.api.nvim_buf_get_mark(0, [["]])[1]
local n_lines = vim.api.nvim_buf_line_count(0)
if not (1 <= mark_line and mark_line <= n_lines) then return end
-- Restore cursor and open just enough folds
vim.cmd([[normal! g`"zv]])
-- Center window
if opts.center then vim.cmd('normal! zz') end
end
--- Compute summary statistics of numerical array
---
--- This might be useful to compute summary of time benchmarking with
--- |MiniMisc.bench_time|.
---
---@param t table Array (table suitable for `ipairs`) of numbers.
---
---@return table Table with summary values under following keys (may be
--- extended in the future): <maximum>, <mean>, <median>, <minimum>, <n>
--- (number of elements), <sd> (sample standard deviation).
MiniMisc.stat_summary = function(t)
if not H.is_array_of(t, H.is_number) then
H.error('Input of `MiniMisc.stat_summary()` should be an array of numbers.')
end
-- Welford algorithm of computing variance
-- Source: https://www.johndcook.com/blog/skewness_kurtosis/
local n = #t
local delta, m1, m2 = 0, 0, 0
local minimum, maximum = math.huge, -math.huge
for i, x in ipairs(t) do
delta = x - m1
m1 = m1 + delta / i
m2 = m2 + delta * (x - m1)
-- Extremums
minimum = x < minimum and x or minimum
maximum = x > maximum and x or maximum
end
return {
maximum = maximum,
mean = m1,
median = H.compute_median(t),
minimum = minimum,
n = n,
sd = math.sqrt(n > 1 and m2 / (n - 1) or 0),
}
end
H.compute_median = function(t)
local n = #t
if n == 0 then return 0 end
local t_sorted = vim.deepcopy(t)
table.sort(t_sorted)
return 0.5 * (t_sorted[math.ceil(0.5 * n)] + t_sorted[math.ceil(0.5 * (n + 1))])
end
--- Return "first" elements of table as decided by `pairs`
---
--- Note: order of elements might vary.
---
---@param t table Input table.
---@param n number|nil Maximum number of first elements. Default: 5.
---
---@return table Table with at most `n` first elements of `t` (with same keys).
MiniMisc.tbl_head = function(t, n)
n = n or 5
local res, n_res = {}, 0
for k, val in pairs(t) do
if n_res >= n then return res end
res[k] = val
n_res = n_res + 1
end
return res
end
--- Return "last" elements of table as decided by `pairs`
---
--- This function makes two passes through elements of `t`:
--- - First to count number of elements.
--- - Second to construct result.
---
--- Note: order of elements might vary.
---
---@param t table Input table.
---@param n number|nil Maximum number of last elements. Default: 5.
---
---@return table Table with at most `n` last elements of `t` (with same keys).
MiniMisc.tbl_tail = function(t, n)
n = n or 5
-- Count number of elements on first pass
local n_all = 0
for _, _ in pairs(t) do
n_all = n_all + 1
end
-- Construct result on second pass
local res = {}
local i, start_i = 0, n_all - n + 1
for k, val in pairs(t) do
i = i + 1
if i >= start_i then res[k] = val end
end
return res
end
--- Add possibility of nested comment leader
---
--- This works by parsing 'commentstring' buffer option, extracting
--- non-whitespace comment leader (symbols on the left of commented line), and
--- locally modifying 'comments' option (by prepending `n:<leader>`). Does
--- nothing if 'commentstring' is empty or has comment symbols both in front
--- and back (like "/*%s*/").
---
--- Nested comment leader added with this function is useful for formatting
--- nested comments. For example, have in Lua "first-level" comments with '--'
--- and "second-level" comments with '----'. With nested comment leader second
--- type can be formatted with `gq` in the same way as first one.
---
--- Recommended usage is with |autocmd|: >lua
---
--- local use_nested_comments = function() MiniMisc.use_nested_comments() end
--- vim.api.nvim_create_autocmd('BufEnter', { callback = use_nested_comments })
--- <
--- Note: for most filetypes 'commentstring' option is added only when buffer
--- with this filetype is entered, so using non-current `buf_id` can not lead
--- to desired effect.
---
---@param buf_id number|nil Buffer identifier (see |bufnr()|) in which function
--- will operate. Default: 0 for current.
MiniMisc.use_nested_comments = function(buf_id)
buf_id = buf_id or 0
local commentstring = vim.bo[buf_id].commentstring
if commentstring == '' then return end
-- Extract raw comment leader from 'commentstring' option
local comment_parts = vim.tbl_filter(function(x) return x ~= '' end, vim.split(commentstring, '%s', true))
-- Don't do anything if 'commentstring' is like '/*%s*/' (as in 'json')
if #comment_parts > 1 then return end
-- Get comment leader by removing whitespace
local leader = vim.trim(comment_parts[1])
local comments = vim.bo[buf_id].comments
local new_comments = string.format('n:%s,%s', leader, comments)
vim.api.nvim_buf_set_option(buf_id, 'comments', new_comments)
end
--- Zoom in and out of a buffer, making it full screen in a floating window
---
--- This function is useful when working with multiple windows but temporarily
--- needing to zoom into one to see more of the code from that buffer. Call it
--- again (without arguments) to zoom out.
---
---@param buf_id number|nil Buffer identifier (see |bufnr()|) to be zoomed.
--- Default: 0 for current.
---@param config table|nil Optional config for window (as for |nvim_open_win()|).
MiniMisc.zoom = function(buf_id, config)
if H.zoom_winid and vim.api.nvim_win_is_valid(H.zoom_winid) then
vim.api.nvim_win_close(H.zoom_winid, true)
H.zoom_winid = nil
else
buf_id = buf_id or 0
-- Currently very big `width` and `height` get truncated to maximum allowed
local default_config = { relative = 'editor', row = 0, col = 0, width = 1000, height = 1000 }
config = vim.tbl_deep_extend('force', default_config, config or {})
H.zoom_winid = vim.api.nvim_open_win(buf_id, true, config)
vim.wo.winblend = 0
vim.cmd('normal! zz')
end
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniMisc.config)
-- Window identifier of current zoom (for `zoom()`)
H.zoom_winid = nil
-- 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({
make_global = {
config.make_global,
function(x)
if type(x) ~= 'table' then return false end
local present_fields = vim.tbl_keys(MiniMisc)
for _, v in pairs(x) do
if not vim.tbl_contains(present_fields, v) then return false end
end
return true
end,
'`make_global` should be a table with `MiniMisc` actual fields',
},
})
return config
end
H.apply_config = function(config)
MiniMisc.config = config
for _, v in pairs(config.make_global) do
_G[v] = MiniMisc[v]
end
end
-- Utilities ------------------------------------------------------------------
H.error = function(msg) error('(mini.misc) ' .. msg) end
H.notify = function(msg, level) vim.notify('(mini.misc) ' .. msg, vim.log.levels[level]) end
H.is_array_of = function(x, predicate)
if not H.islist(x) then return false end
for _, v in ipairs(x) do
if not predicate(v) then return false end
end
return true
end
H.is_number = function(x) return type(x) == 'number' end
H.is_string = function(x) return type(x) == 'string' end
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
return MiniMisc

View File

@ -0,0 +1,496 @@
--- *mini.move* Move any selection in any direction
--- *MiniMove*
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Works in two modes:
--- - Visual mode. Select text (charwise with |v|, linewise with |V|, and
--- blockwise with |CTRL-V|) and press customizable mapping to move in
--- all four directions (left, right, down, up). It keeps Visual mode.
--- - Normal mode. Press customizable mapping to move current line in all
--- four directions (left, right, down, up).
--- - Special handling of linewise movement:
--- - Vertical movement gets reindented with |=|.
--- - Horizontal movement is improved indent/dedent with |>| / |<|.
--- - Cursor moves along with selection.
---
--- - Provides both mappings and Lua functions for motions. See
--- |MiniMove.move_selection()| and |MiniMove.move_line()|.
---
--- - Respects |v:count|. Movement mappings can be preceded by a number which
--- multiplies command effect.
---
--- - All consecutive moves (regardless of direction) can be undone by a single |u|.
---
--- - Respects preferred column for vertical movement. It will vertically move
--- selection as how cursor is moving (not strictly vertically if target
--- column is not present in target line).
---
--- Notes:
--- - Doesn't allow moving selection outside of current lines (by design).
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.move').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniMove`
--- which you can use for scripting or manually (with `:lua MiniMove.*`).
---
--- See |MiniMove.config| for available config settings.
---
--- You can override runtime config settings (but not `config.mappings`) locally
--- to buffer inside `vim.b.minimove_config` which should have same structure
--- as `MiniMove.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - 'matze/vim-move':
--- - Doesn't support vertical movement of charwise and blockwise selections.
--- While 'mini.move' does.
--- - Doesn't support horizontal movement of current line in favor of
--- horizontal movement of current character. While 'mini.move' supports
--- horizontal movement of current line and doesn't support such movement
--- of current character.
--- - Has extra functionality for certain moves (like move by half page).
--- While 'mini.move' does not (by design).
--- - 'booperlv/nvim-gomove':
--- - Doesn't support movement in charwise visual selection.
--- While 'mini.move' does.
--- - Has extra functionality beyond moving text, like duplication.
--- While 'mini.move' concentrates only on moving functionality.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minimove_disable` (globally) or `vim.b.minimove_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.
---@alias __move_direction string One of "left", "down", "up", "right".
---@alias __move_opts table|nil Options. Same structure as `options` in |MiniMove.config|
--- (with its values as defaults) plus these allowed extra fields:
--- - <n_times> (number) - number of times to try to make a move.
--- Default: |v:count1|.
---@diagnostic disable:undefined-field
-- Module definition ==========================================================
local MiniMove = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniMove.config|.
---
---@usage >lua
--- require('mini.move').setup() -- use default config
--- -- OR
--- require('mini.move').setup({}) -- replace {} with your config table
--- <
MiniMove.setup = function(config)
-- Export module
_G.MiniMove = MiniMove
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Mappings ~
---
--- Other possible choices of mappings: >lua
---
--- -- `HJKL` for moving visual selection (overrides H, L, J in Visual mode)
--- require('mini.move').setup({
--- mappings = {
--- left = 'H',
--- right = 'L',
--- down = 'J',
--- up = 'K',
--- }
--- })
---
--- -- Shift + arrows
--- require('mini.move').setup({
--- mappings = {
--- left = '<S-left>',
--- right = '<S-right>',
--- down = '<S-down>',
--- up = '<S-up>',
---
--- line_left = '<S-left>',
--- line_right = '<S-right>',
--- line_down = '<S-down>',
--- line_up = '<S-up>',
--- }
--- })
--- <
MiniMove.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Move visual selection in Visual mode. Defaults are Alt (Meta) + hjkl.
left = '<M-h>',
right = '<M-l>',
down = '<M-j>',
up = '<M-k>',
-- Move current line in Normal mode
line_left = '<M-h>',
line_right = '<M-l>',
line_down = '<M-j>',
line_up = '<M-k>',
},
-- Options which control moving behavior
options = {
-- Automatically reindent selection during linewise vertical move
reindent_linewise = true,
},
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Move visually selected region in any direction within present lines
---
--- Main function powering visual selection move in Visual mode.
---
--- Notes:
--- - Vertical movement in linewise mode is followed up by reindent with |v_=|.
--- - Horizontal movement in linewise mode is same as |v_<| and |v_>|.
---
---@param direction __move_direction
---@param opts __move_opts
MiniMove.move_selection = function(direction, opts)
if H.is_disabled() or not vim.o.modifiable then return end
opts = vim.tbl_deep_extend('force', H.get_config().options, opts or {})
-- This could have been a one-line expression mappings, but there are issues:
-- - Initial yanking modifies some register. Not critical, but also not good.
-- - Doesn't work at movement edges (first line for `K`, etc.). See
-- https://github.com/vim/vim/issues/11786
-- - Results into each movement being a separate undo block, which is
-- inconvenient with several back-to-back movements.
local cur_mode = vim.fn.mode()
-- Act only inside visual mode
if not (cur_mode == 'v' or cur_mode == 'V' or cur_mode == '\22') then return end
-- Define common predicates
local dir_type = (direction == 'up' or direction == 'down') and 'vert' or 'hori'
local is_linewise = cur_mode == 'V'
-- Cache useful data because it will be reset when executing commands
local n_times = opts.n_times or vim.v.count1
local ref_curpos, ref_last_col = vim.fn.getcurpos(), vim.fn.col('$')
local is_cursor_on_selection_start = vim.fn.line('.') < vim.fn.line('v')
-- Determine if previous action was this type of move
local is_moving = vim.deep_equal(H.state, H.get_move_state())
if not is_moving then H.curswant = nil end
-- Allow undo of consecutive moves at once (direction doesn't matter)
local cmd = H.make_cmd_normal(is_moving)
-- Treat horizontal linewise movement specially
if is_linewise and dir_type == 'hori' then
-- Use indentation as horizontal movement for linewise selection
cmd(n_times .. H.indent_keys[direction] .. 'gv')
-- Make cursor move along selection
H.correct_cursor_col(ref_curpos, ref_last_col)
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
return
end
-- Temporarily ensure possibility to put cursor just after line end.
-- This allows a more intuitive cursor positioning from and to end of line.
-- NOTE: somehow, this should be done before initial cut to take effect.
local cache_virtualedit = vim.o.virtualedit
if not cache_virtualedit:find('all') then vim.o.virtualedit = 'onemore' end
-- Cut selection while saving caching register
local cache_z_reg = vim.fn.getreg('z')
cmd('"zx')
-- Detect edge selection: last line(s) for vertical and last character(s)
-- for horizontal. At this point (after cutting selection) cursor is on the
-- edge which can happen in two cases:
-- - Move second to last selection towards edge (like in 'abc' move 'b'
-- to right or second to last line down).
-- - Move edge selection away from edge (like in 'abc' move 'c' to left
-- or last line up).
-- Use condition that removed selection was further than current cursor
-- to distinguish between two cases.
local is_edge_selection_hori = dir_type == 'hori' and vim.fn.col('.') < vim.fn.col("'<")
local is_edge_selection_vert = dir_type == 'vert' and vim.fn.line('.') < vim.fn.line("'<")
local is_edge_selection = is_edge_selection_hori or is_edge_selection_vert
-- Use `p` as paste key instead of `P` in cases which might require moving
-- selection to place which is unreachable with `P`: right to be line end
-- and down to be last line. NOTE: temporary `virtualedit=onemore` solves
-- this only for horizontal movement, but not for vertical.
local can_go_overline = not is_linewise and direction == 'right'
local can_go_overbuf = is_linewise and direction == 'down'
local paste_key = (can_go_overline or can_go_overbuf) and 'p' or 'P'
-- Restore `curswant` to try move cursor to initial column (just like
-- default `hjkl` moves)
if dir_type == 'vert' then H.set_curswant(H.curswant) end
-- Possibly reduce number of moves by one to not overshoot move
local n = n_times - ((paste_key == 'p' or is_edge_selection) and 1 or 0)
-- Don't allow movement past last line of block selection (any part)
if cur_mode == '\22' and direction == 'down' and vim.fn.line('$') == vim.fn.line("'>") then n = 0 end
-- Move cursor
if n > 0 then cmd(n .. H.move_keys[direction]) end
-- Save curswant. Correct for one less move when using `p` as paste.
H.curswant = H.get_curswant() + ((direction == 'right' and paste_key == 'p') and 1 or 0)
-- Open just enough folds (but not in linewise mode, as it allows moving
-- past folds)
if not is_linewise then cmd('zv') end
-- Paste
cmd('"z' .. paste_key)
-- Select newly moved region. Another way is to use something like `gvhoho`
-- but it doesn't work well with selections spanning several lines.
cmd('`[1v')
-- Do extra in case of linewise selection
if is_linewise then
-- Reindent linewise selection if `=` can do that.
-- NOTE: this sometimes doesn't work well with folds (and probably
-- `foldmethod=indent`) and linewise mode because it recomputes folds after
-- that and the whole "move past fold" doesn't work.
if opts.reindent_linewise and dir_type == 'vert' and vim.o.equalprg == '' then cmd('=gv') end
-- Move cursor along the selection. NOTE: do this *after* reindent to
-- account for its effect.
-- - Ensure that cursor is on the right side of selection
if is_cursor_on_selection_start then cmd('o') end
H.correct_cursor_col(ref_curpos, ref_last_col)
end
-- Restore intermediate values
vim.fn.setreg('z', cache_z_reg)
vim.o.virtualedit = cache_virtualedit
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
end
--- Move current line in any direction
---
--- Main function powering current line move in Normal mode.
---
--- Notes:
--- - Vertical movement is followed up by reindent with |v_=|.
--- - Horizontal movement is almost the same as |<<| and |>>| with a different
--- handling of |v:count| (multiplies shift effect instead of modifying that
--- number of lines).
---
---@param direction __move_direction
---@param opts __move_opts
MiniMove.move_line = function(direction, opts)
if H.is_disabled() or not vim.o.modifiable then return end
opts = vim.tbl_deep_extend('force', H.get_config().options, opts or {})
-- Determine if previous action was this type of move
local is_moving = vim.deep_equal(H.state, H.get_move_state())
-- Allow undo of consecutive moves at once (direction doesn't matter)
local cmd = H.make_cmd_normal(is_moving)
-- Cache useful data because it will be reset when executing commands
local n_times = opts.n_times or vim.v.count1
local is_last_line_up = direction == 'up' and vim.fn.line('.') == vim.fn.line('$')
local ref_curpos, ref_last_col = vim.fn.getcurpos(), vim.fn.col('$')
if direction == 'left' or direction == 'right' then
-- Use indentation as horizontal movement. Explicitly call `count1` because
-- `<`/`>` use `v:count` to define number of lines.
-- Go to first non-blank at the end.
local key = H.indent_keys[direction]
cmd(string.rep(key .. key, n_times))
-- Make cursor move along selection
H.correct_cursor_col(ref_curpos, ref_last_col)
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
return
end
-- Cut curre lint while saving caching register
local cache_z_reg = vim.fn.getreg('z')
cmd('"zdd')
-- Move cursor
local paste_key = direction == 'up' and 'P' or 'p'
local n = n_times - ((paste_key == 'p' or is_last_line_up) and 1 or 0)
if n > 0 then cmd(n .. H.move_keys[direction]) end
-- Paste
cmd('"z' .. paste_key)
-- Reindent and put cursor on first non-blank
if opts.reindent_linewise and vim.o.equalprg == '' then cmd('==') end
-- Move cursor along the selection. NOTE: do this *after* reindent to
-- account for its effect.
H.correct_cursor_col(ref_curpos, ref_last_col)
-- Restore intermediate values
vim.fn.setreg('z', cache_z_reg)
-- Track new state to allow joining in single undo block
H.state = H.get_move_state()
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniMove.config)
H.move_keys = { left = 'h', down = 'j', up = 'k', right = 'l' }
H.indent_keys = { left = '<', right = '>' }
-- Moving state used to decide when to start new undo block ...
H.state = {
-- ... on buffer change
buf_id = nil,
-- ... on text change
changedtick = nil,
-- ... on cursor move
cursor = nil,
-- ... on mode change
mode = nil,
}
H.curswant = nil
-- 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({
mappings = { config.mappings, 'table' },
options = { config.options, 'table' },
})
vim.validate({
['mappings.left'] = { config.mappings.left, 'string' },
['mappings.down'] = { config.mappings.down, 'string' },
['mappings.up'] = { config.mappings.up, 'string' },
['mappings.right'] = { config.mappings.right, 'string' },
['mappings.line_left'] = { config.mappings.line_left, 'string' },
['mappings.line_right'] = { config.mappings.line_right, 'string' },
['mappings.line_down'] = { config.mappings.line_down, 'string' },
['mappings.line_up'] = { config.mappings.line_up, 'string' },
['options.reindent_linewise'] = { config.options.reindent_linewise, 'boolean' },
})
return config
end
--stylua: ignore
H.apply_config = function(config)
MiniMove.config = config
-- Make mappings
local maps = config.mappings
H.map('x', maps.left, [[<Cmd>lua MiniMove.move_selection('left')<CR>]], { desc = 'Move left' })
H.map('x', maps.right, [[<Cmd>lua MiniMove.move_selection('right')<CR>]], { desc = 'Move right' })
H.map('x', maps.down, [[<Cmd>lua MiniMove.move_selection('down')<CR>]], { desc = 'Move down' })
H.map('x', maps.up, [[<Cmd>lua MiniMove.move_selection('up')<CR>]], { desc = 'Move up' })
H.map('n', maps.line_left, [[<Cmd>lua MiniMove.move_line('left')<CR>]], { desc = 'Move line left' })
H.map('n', maps.line_right, [[<Cmd>lua MiniMove.move_line('right')<CR>]], { desc = 'Move line right' })
H.map('n', maps.line_down, [[<Cmd>lua MiniMove.move_line('down')<CR>]], { desc = 'Move line down' })
H.map('n', maps.line_up, [[<Cmd>lua MiniMove.move_line('up')<CR>]], { desc = 'Move line up' })
end
H.is_disabled = function() return vim.g.minimove_disable == true or vim.b.minimove_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniMove.config, vim.b.minimove_config or {}, config or {})
end
-- Utilities ------------------------------------------------------------------
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.make_cmd_normal = function(include_undojoin)
local normal_command = (include_undojoin and 'undojoin | ' or '') .. 'silent keepjumps normal! '
return function(x)
-- Caching and restoring data on every command is not necessary but leads
-- to a nicer implementation
-- Disable 'mini.bracketed' to avoid unwanted entries to its yank history
local cache_minibracketed_disable = vim.b.minibracketed_disable
local cache_unnamed_register = vim.fn.getreg('"')
-- Don't track possible put commands into yank history
vim.b.minibracketed_disable = true
vim.cmd(normal_command .. x)
vim.b.minibracketed_disable = cache_minibracketed_disable
vim.fn.setreg('"', cache_unnamed_register)
end
end
H.get_move_state = function()
return {
buf_id = vim.api.nvim_get_current_buf(),
changedtick = vim.b.changedtick,
cursor = vim.api.nvim_win_get_cursor(0),
mode = vim.fn.mode(),
}
end
H.correct_cursor_col = function(ref_curpos, ref_last_col)
-- Use `ref_curpos = getcurpos()` instead of `vim.api.nvim_win_get_cursor(0)`
-- allows to also account for `virtualedit=all`
local col_diff = vim.fn.col('$') - ref_last_col
local new_col = math.max(ref_curpos[3] + col_diff, 1)
vim.fn.cursor({ vim.fn.line('.'), new_col, ref_curpos[4], ref_curpos[5] + col_diff })
end
H.get_curswant = function() return vim.fn.winsaveview().curswant end
H.set_curswant = function(x)
if x == nil then return end
vim.fn.winrestview({ curswant = x })
end
return MiniMove

View File

@ -0,0 +1,845 @@
--- *mini.notify* Show notifications
--- *MiniNotify*
---
--- MIT License Copyright (c) 2024 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
---
--- - Show one or more highlighted notifications in a single floating window.
---
--- - Manage notifications (add, update, remove, clear).
---
--- - |vim.notify()| wrapper generator (see |MiniNotify.make_notify()|).
---
--- - Automated show of LSP progress report.
---
--- - Track history which can be accessed with |MiniNotify.get_all()|
--- and shown with |MiniNotify.show_history()|.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.notify').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniNotify`
--- which you can use for scripting or manually (with `:lua MiniNotify.*`).
---
--- See |MiniNotify.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.mininotify_config` which should have same structure as
--- `MiniNotify.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - 'j-hui/fidget.nvim':
--- - Basic goals of providing interface for notifications are similar.
--- - Has more configuration options and visual effects, while this module
--- does not (by design).
---
--- - 'rcarriga/nvim-notify':
--- - Similar to 'j-hui/fidget.nvim'.
---
--- # Highlight groups ~
---
--- * `MiniNotifyBorder` - window border.
--- * `MiniNotifyNormal` - basic foreground/background highlighting.
--- * `MiniNotifyTitle` - window title.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable showing notifications, set `vim.g.mininotify_disable` (globally) or
--- `vim.b.mininotify_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.
--- # Notification specification ~
---
--- Notification is a table with the following keys:
---
--- - <msg> `(string)` - single string with notification message.
--- Use `\n` to delimit several lines.
--- - <level> `(string)` - notification level as key of |vim.log.levels|.
--- Like "ERROR", "WARN", "INFO", etc.
--- - <hl_group> `(string)` - highlight group with which notification is shown.
--- - <ts_add> `(number)` - timestamp of when notification is added.
--- - <ts_update> `(number)` - timestamp of the latest notification update.
--- - <ts_remove> `(number|nil)` - timestamp of when notification is removed.
--- It is `nil` if notification was never removed and thus considered "active".
---
--- Notes:
--- - Timestamps are compatible with |strftime()| and have fractional part.
---@tag MiniNotify-specification
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
---@diagnostic disable:undefined-doc-name
---@diagnostic disable:luadoc-miss-type-name
-- Module definition ==========================================================
local MiniNotify = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniNotify.config|.
---
---@usage >lua
--- require('mini.notify').setup() -- use default config
--- -- OR
--- require('mini.notify').setup({}) -- replace {} with your config table
--- <
MiniNotify.setup = function(config)
-- Export module
_G.MiniNotify = MiniNotify
-- 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 # Content ~
---
--- `config.content` defines how notifications are shown.
---
--- `content.format` is a function which takes single notification object
--- (see |MiniNotify-specification|) and returns a string to be used directly
--- when showing notification.
--- Default: `nil` for |MiniNotify.default_format()|.
---
--- `content.sort` is a function which takes array of notification objects
--- (see |MiniNotify-specification|) and returns an array of such objects.
--- It can be used to define custom order and/or filter for notifications which
--- are shown simultaneously.
--- Note: Input contains notifications before applying `content.format`.
--- Default: `nil` for |MiniNotify.default_sort()|.
---
--- Example: >lua
---
--- require('mini.notify').setup({
--- content = {
--- -- Use notification message as is
--- format = function(notif) return notif.msg end,
---
--- -- Show more recent notifications first
--- sort = function(notif_arr)
--- table.sort(
--- notif_arr,
--- function(a, b) return a.ts_update > b.ts_update end
--- )
--- return notif_arr
--- end,
--- },
--- })
--- <
--- # LSP progress ~
---
--- `config.lsp_progress` defines automated notifications for LSP progress.
--- It is implemented as a single updating notification with all information
--- about the progress.
--- Setting up is done inside |MiniNotify.setup()| via |vim.schedule()|'ed setting
--- of |lsp-handler| for "$/progress" method.
---
--- `lsp_progress.enable` is a boolean indicating whether LSP progress should
--- be shown in notifications. Can be disabled in current session.
--- Default: `true`. Note: Should be `true` during |MiniNotify.setup()| call to be able
--- to enable it in current session.
---
--- `lsp_progress.duration_last` is a number of milliseconds for the last progress
--- report to be shown on screen before removing it.
--- Default: 1000.
---
--- Notes:
--- - This respects previously set handler by saving and calling it.
--- - Overrding "$/progress" method of `vim.lsp.handlers` disables notifications.
---
--- # Window ~
---
--- `config.window` defines behavior of notification window.
---
--- `window.config` is a table defining floating window characteristics
--- or a callable returning such table (will be called with identifier of
--- window's buffer already showing notifications). It should have the same
--- structure as in |nvim_open_win()|. It has the following default values
--- which show notifications in the upper right corner with upper limit on width:
--- - `width` is chosen to fit buffer content but at most `window.max_width_share`
--- share of 'columns'.
--- To have higher maximum width, use function in `config.window` which computes
--- dimensions inside of it (based on buffer content).
--- - `height` is chosen to fit buffer content with enabled 'wrap' (assuming
--- default value of `width`).
--- - `anchor`, `col`, and `row` are "NE", 'columns', and 0 or 1 (depending on tabline).
--- - `border` is "single".
--- - `zindex` is 999 to be as much on top as reasonably possible.
---
--- `window.max_width_share` defines maximum window width as a share of 'columns'.
--- Should be a number between 0 (not included) and 1.
--- Default: 0.382.
---
--- `window.winblend` defines 'winblend' value for notification window.
--- Default: 25.
MiniNotify.config = {
-- Content management
content = {
-- Function which formats the notification message
-- By default prepends message with notification time
format = nil,
-- Function which orders notification array from most to least important
-- By default orders first by level and then by update timestamp
sort = nil,
},
-- Notifications about LSP progress
lsp_progress = {
-- Whether to enable showing
enable = true,
-- Duration (in ms) of how long last message should be shown
duration_last = 1000,
},
-- Window options
window = {
-- Floating window config
config = {},
-- Maximum window width as share (between 0 and 1) of available columns
max_width_share = 0.382,
-- Value of 'winblend' option
winblend = 25,
},
}
--minidoc_afterlines_end
--- Make vim.notify wrapper
---
--- Calling this function creates an implementation of |vim.notify()| powered
--- by this module. General idea is that notification is shown right away (as
--- soon as safely possible, see |vim.schedule()|) and removed after a configurable
--- amount of time.
---
--- Examples: >lua
---
--- -- Defaults
--- vim.notify = require('mini.notify').make_notify()
---
--- -- Change duration for errors to show them longer
--- local opts = { ERROR = { duration = 10000 } }
--- vim.notify = require('mini.notify').make_notify(opts)
--- <
---@param opts table|nil Options to configure behavior of notification `level`
--- (as in |MiniNotfiy.add()|). Fields are the same as names of `vim.log.levels`
--- with values being tables with possible fields:
--- - <duration> `(number)` - duration (in ms) of how much a notification
--- should be shown. If 0 or negative, notification is not shown at all.
--- - <hl_group> `(string)` - highlight group of notification.
--- Only data different to default can be supplied.
---
--- Default: >lua
---
--- {
--- ERROR = { duration = 5000, hl_group = 'DiagnosticError' },
--- WARN = { duration = 5000, hl_group = 'DiagnosticWarn' },
--- INFO = { duration = 5000, hl_group = 'DiagnosticInfo' },
--- DEBUG = { duration = 0, hl_group = 'DiagnosticHint' },
--- TRACE = { duration = 0, hl_group = 'DiagnosticOk' },
--- OFF = { duration = 0, hl_group = 'MiniNotifyNormal' },
--- }
--- <
MiniNotify.make_notify = function(opts)
local level_names = {}
for k, v in pairs(vim.log.levels) do
level_names[v] = k
end
--stylua: ignore
local default_opts = {
ERROR = { duration = 5000, hl_group = 'DiagnosticError' },
WARN = { duration = 5000, hl_group = 'DiagnosticWarn' },
INFO = { duration = 5000, hl_group = 'DiagnosticInfo' },
DEBUG = { duration = 0, hl_group = 'DiagnosticHint' },
TRACE = { duration = 0, hl_group = 'DiagnosticOk' },
OFF = { duration = 0, hl_group = 'MiniNotifyNormal' },
}
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
for key, val in pairs(opts) do
if default_opts[key] == nil then H.error('Keys should be log level names.') end
if type(val) ~= 'table' then H.error('Level data should be table.') end
if type(val.duration) ~= 'number' then H.error('`duration` in level data should be number.') end
if type(val.hl_group) ~= 'string' then H.error('`hl_group` in level data should be string.') end
end
-- Use `vim.schedule_wrap` for output to be usable inside `vim.uv` callbacks
local notify = function(msg, level)
level = level or vim.log.levels.INFO
local level_name = level_names[level]
if level_name == nil then H.error('Only valid values of `vim.log.levels` are supported.') end
local level_data = opts[level_name]
if level_data.duration <= 0 then return end
local id = MiniNotify.add(msg, level_name, level_data.hl_group)
vim.defer_fn(function() MiniNotify.remove(id) end, level_data.duration)
end
return function(msg, level)
if not vim.in_fast_event() then return notify(msg, level) end
vim.schedule(function() notify(msg, level) end)
end
end
--- Add notification
---
--- Add notification to history. It is considered "active" and is shown.
--- To hide, call |MiniNotfiy.remove()| with identifier this function returns.
---
--- Example: >lua
---
--- local id = MiniNotify.add('Hello', 'WARN', 'Comment')
--- vim.defer_fn(function() MiniNotify.remove(id) end, 1000)
--- <
---@param msg string Notification message.
---@param level string|nil Notification level as key of |vim.log.levels|.
--- Default: `'INFO'`.
---@param hl_group string|nil Notification highlight group.
--- Default: `'MiniNotifyNormal'`.
---
---@return number Notification identifier.
MiniNotify.add = function(msg, level, hl_group)
H.validate_msg(msg)
level = level or 'INFO'
H.validate_level(level)
hl_group = hl_group or 'MiniNotifyNormal'
H.validate_hl_group(hl_group)
local cur_ts = H.get_timestamp()
local new_notif = { msg = msg, level = level, hl_group = hl_group, ts_add = cur_ts, ts_update = cur_ts }
local new_id = #H.history + 1
-- NOTE: Crucial to use the same table here and later only update values
-- inside of it in place. This makes sure that history entries are in sync.
H.history[new_id], H.active[new_id] = new_notif, new_notif
-- Refresh active notifications
MiniNotify.refresh()
return new_id
end
--- Update active notification
---
--- Modify data of active notification.
---
---@param id number Identifier of currently active notification as returned
--- by |MiniNotify.add()|.
---@param new_data table Table with data to update. Keys should be as non-timestamp
--- fields of |MiniNotify-specification| and values - new notification values.
MiniNotify.update = function(id, new_data)
local notif = H.active[id]
if notif == nil then H.error('`id` is not an identifier of active notification.') end
if type(new_data) ~= 'table' then H.error('`new_data` should be table.') end
if new_data.msg ~= nil then H.validate_msg(new_data.msg) end
if new_data.level ~= nil then H.validate_level(new_data.level) end
if new_data.hl_group ~= nil then H.validate_hl_group(new_data.hl_group) end
notif.msg = new_data.msg or notif.msg
notif.level = new_data.level or notif.level
notif.hl_group = new_data.hl_group or notif.hl_group
notif.ts_update = H.get_timestamp()
MiniNotify.refresh()
end
--- Remove notification
---
--- If notification is active, make it not active (by setting `ts_remove` field).
--- If not active, do nothing.
---
---@param id number|nil Identifier of previously added notification.
--- If it is not, nothing is done (silently).
MiniNotify.remove = function(id)
local notif = H.active[id]
if notif == nil then return end
notif.ts_remove = H.get_timestamp()
H.active[id] = nil
MiniNotify.refresh()
end
--- Remove all active notifications
---
--- Hide all active notifications and stop showing window (if shown).
MiniNotify.clear = function()
local cur_ts = H.get_timestamp()
for id, _ in pairs(H.active) do
H.active[id].ts_remove = cur_ts
end
H.active = {}
MiniNotify.refresh()
end
--- Refresh notification window
---
--- Make notification window show relevant data:
--- - Create an array of active notifications (see |MiniNotify-specification|).
--- - Apply `config.content.sort` to an array. If output has zero notifications,
--- make notification window to not show.
--- - Apply `config.content.format` to each element of notification array and
--- update its message.
--- - Construct content from notifications and show them in a window.
MiniNotify.refresh = function()
if H.is_disabled() or type(vim.v.exiting) == 'number' then return H.window_close() end
-- Prepare array of active notifications
local notif_arr = vim.deepcopy(vim.tbl_values(H.active))
local config_content = H.get_config().content
local sort = vim.is_callable(config_content.sort) and config_content.sort or MiniNotify.default_sort
notif_arr = sort(notif_arr)
if not H.is_notification_array(notif_arr) then H.error('Output of `content.sort` should be notification array.') end
if #notif_arr == 0 then return H.window_close() end
local format = vim.is_callable(config_content.format) and config_content.format or MiniNotify.default_format
notif_arr = H.notif_apply_format(notif_arr, format)
-- Refresh buffer
local buf_id = H.cache.buf_id
if not H.is_valid_buf(buf_id) then buf_id = H.buffer_create() end
H.buffer_refresh(buf_id, notif_arr)
-- Refresh window
local win_id = H.cache.win_id
if not (H.is_valid_win(win_id) and H.is_win_in_tabpage(win_id)) then
H.window_close()
win_id = H.window_open(buf_id)
else
local new_config = H.window_compute_config(buf_id)
vim.api.nvim_win_set_config(win_id, new_config)
end
-- Redraw
vim.cmd('redraw')
-- Update cache
H.cache.buf_id, H.cache.win_id = buf_id, win_id
end
--- Get previously added notification by id
---
---@param id number Identifier of notification.
---
---@return table Notification object (see |MiniNotify-specification|).
MiniNotify.get = function(id) return vim.deepcopy(H.history[id]) end
--- Get all previously added notifications
---
--- Get map of used notifications with keys being notification identifiers.
---
--- Can be used to get only active notification objects. Example: >lua
---
--- -- Get active notifications
--- vim.tbl_filter(
--- function(notif) return notif.ts_remove == nil end,
--- MiniNotify.get_all()
--- )
--- <
---@return table Map with notification object values (see |MiniNotify-specification|).
--- Note: messages are taken from last valid update.
MiniNotify.get_all = function() return vim.deepcopy(H.history) end
--- Show history
---
--- Open or reuse a scratch buffer with all previously shown notifications.
---
--- Notes:
--- - Content is ordered from oldest to newest based on latest update time.
--- - Message is formatted with `config.content.format`.
MiniNotify.show_history = function()
-- Prepare content
local config_content = H.get_config().content
local notif_arr = MiniNotify.get_all()
table.sort(notif_arr, function(a, b) return a.ts_update < b.ts_update end)
local format = vim.is_callable(config_content.format) and config_content.format or MiniNotify.default_format
notif_arr = H.notif_apply_format(notif_arr, format)
-- Show content in a reusable buffer
local buf_id
for _, id in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[id].filetype == 'mininotify-history' then buf_id = id end
end
if buf_id == nil then
buf_id = vim.api.nvim_create_buf(true, true)
vim.bo[buf_id].filetype = 'mininotify-history'
end
H.buffer_refresh(buf_id, notif_arr)
vim.api.nvim_win_set_buf(0, buf_id)
end
--- Default content format
---
--- Used by default as `config.content.format`. Prepends notification message
--- with the human readable update time and a separator.
---
---@param notif table Notification object (see |MiniNotify-specification|).
---
---@return string Formatted notification message.
MiniNotify.default_format = function(notif)
local time = vim.fn.strftime('%H:%M:%S', math.floor(notif.ts_update))
return string.format('%s │ %s', time, notif.msg)
end
--- Default content sort
---
--- Used by default as `config.content.sort`. First sorts by notification's `level`
--- ("ERROR" > "WARN" > "INFO" > "DEBUG" > "TRACE" > "OFF"; the bigger the more
--- important); if draw - by latest update time (the later the more important).
---
---@param notif_arr table Array of notifications (see |MiniNotify-specification|).
---
---@return table Sorted array of notifications.
MiniNotify.default_sort = function(notif_arr)
local res = vim.deepcopy(notif_arr)
table.sort(res, H.notif_compare)
return res
end
-- Helper data ================================================================
-- Module default config
H.default_config = MiniNotify.config
-- Map of currently active notifications with their id as key
H.active = {}
-- History of all notifications in order they are created
H.history = {}
-- Map of LSP progress process id to notification data
H.lsp_progress = {}
-- Priorities of levels
H.level_priority = { ERROR = 6, WARN = 5, INFO = 4, DEBUG = 3, TRACE = 2, OFF = 1 }
-- Namespaces
H.ns_id = {
highlight = vim.api.nvim_create_namespace('MiniNotifyHighlight'),
}
-- Various cache
H.cache = {
-- Notification buffer and window
buf_id = nil,
win_id = nil,
}
-- 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({
content = { config.content, 'table' },
lsp_progress = { config.lsp_progress, 'table' },
window = { config.window, 'table' },
})
local is_table_or_callable = function(x) return type(x) == 'table' or vim.is_callable(x) end
vim.validate({
['content.format'] = { config.content.format, 'function', true },
['content.sort'] = { config.content.sort, 'function', true },
['lsp_progress.enable'] = { config.lsp_progress.enable, 'boolean' },
['lsp_progress.duration_last'] = { config.lsp_progress.duration_last, 'number' },
['window.config'] = { config.window.config, is_table_or_callable, 'table or callable' },
['window.max_width_share'] = { config.window.max_width_share, 'number' },
['window.winblend'] = { config.window.winblend, 'number' },
})
return config
end
H.apply_config = function(config)
MiniNotify.config = config
if config.lsp_progress.enable then
-- Use `vim.schedule` to reduce startup time (sourcing `vim.lsp` is costly)
vim.schedule(function()
-- Cache original handler only once (to avoid infinite loop)
if vim.lsp.handlers['$/progress before mini.notify'] == nil then
vim.lsp.handlers['$/progress before mini.notify'] = vim.lsp.handlers['$/progress']
end
vim.lsp.handlers['$/progress'] = H.lsp_progress_handler
end)
end
end
H.create_autocommands = function(config)
local augroup = vim.api.nvim_create_augroup('MiniNotify', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
au({ 'TabEnter', 'VimResized' }, '*', function() MiniNotify.refresh() end, 'Refresh notifications')
end
--stylua: ignore
H.create_default_hl = function()
local hi = function(name, opts)
opts.default = true
vim.api.nvim_set_hl(0, name, opts)
end
hi('MiniNotifyBorder', { link = 'FloatBorder' })
hi('MiniNotifyNormal', { link = 'NormalFloat' })
hi('MiniNotifyTitle', { link = 'FloatTitle' })
end
H.is_disabled = function() return vim.g.mininotify_disable == true or vim.b.mininotify_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniNotify.config, vim.b.mininotify_config or {}, config or {})
end
-- LSP progress ---------------------------------------------------------------
H.lsp_progress_handler = function(err, result, ctx, config)
-- Make basic response processing. First call original LSP handler.
-- On Neovim>=0.10 this is crucial to not override `LspProgress` event.
if vim.is_callable(vim.lsp.handlers['$/progress before mini.notify']) then
vim.lsp.handlers['$/progress before mini.notify'](err, result, ctx, config)
end
local lsp_progress_config = H.get_config().lsp_progress
if not lsp_progress_config.enable then return end
if err ~= nil then return vim.notify(vim.inspect(err), vim.log.levels.ERROR) end
if not (type(result) == 'table' and type(result.value) == 'table') then return end
local value = result.value
-- Construct LSP progress id
local client_name = vim.lsp.get_client_by_id(ctx.client_id).name
if type(client_name) ~= 'string' then client_name = string.format('LSP[id=%s]', ctx.client_id) end
local buf_id = ctx.bufnr or 'nil'
local lsp_progress_id = buf_id .. client_name .. (result.token or '')
local progress_data = H.lsp_progress[lsp_progress_id] or {}
-- Store percentage to be used if no new one was sent
progress_data.percentage = value.percentage or progress_data.percentage or 0
-- Stop notifications without update on progress end.
-- This usually results into a cleaner and more informative history.
-- Delay removal to not cause flicker.
if value.kind == 'end' then
H.lsp_progress[lsp_progress_id] = nil
local delay = math.max(lsp_progress_config.duration_last, 0)
vim.defer_fn(function() MiniNotify.remove(progress_data.notif_id) end, delay)
return
end
-- Cache title because it is only supplied on 'begin'
if value.kind == 'begin' then progress_data.title = value.title end
-- Make notification
--stylua: ignore
local msg = string.format(
'%s: %s %s (%s%%)',
client_name, progress_data.title or '', value.message or '', progress_data.percentage
)
if progress_data.notif_id == nil then
progress_data.notif_id = MiniNotify.add(msg)
else
MiniNotify.update(progress_data.notif_id, { msg = msg })
end
-- Cache progress data
H.lsp_progress[lsp_progress_id] = progress_data
end
-- Buffer ---------------------------------------------------------------------
H.buffer_create = function()
local buf_id = vim.api.nvim_create_buf(false, true)
vim.bo[buf_id].filetype = 'mininotify'
return buf_id
end
H.buffer_refresh = function(buf_id, notif_arr)
local ns_id = H.ns_id.highlight
-- Ensure clear buffer
vim.api.nvim_buf_clear_namespace(buf_id, ns_id, 0, -1)
vim.api.nvim_buf_set_lines(buf_id, 0, -1, true, {})
-- Compute lines and highlight regions
local lines, highlights = {}, {}
for _, notif in ipairs(notif_arr) do
local notif_lines = vim.split(notif.msg, '\n')
for _, l in ipairs(notif_lines) do
table.insert(lines, l)
end
table.insert(highlights, { group = notif.hl_group, from_line = #lines - #notif_lines + 1, to_line = #lines })
end
-- Set lines and highlighting
vim.api.nvim_buf_set_lines(buf_id, 0, -1, true, lines)
local extmark_opts = { end_col = 0, hl_eol = true, hl_mode = 'combine' }
for _, hi_data in ipairs(highlights) do
extmark_opts.end_row, extmark_opts.hl_group = hi_data.to_line, hi_data.group
vim.api.nvim_buf_set_extmark(buf_id, ns_id, hi_data.from_line - 1, 0, extmark_opts)
end
end
H.buffer_default_dimensions = function(buf_id, max_width_share)
local line_widths = vim.tbl_map(vim.fn.strdisplaywidth, vim.api.nvim_buf_get_lines(buf_id, 0, -1, true))
-- Compute width so as to fit all lines
local width = 1
for _, l_w in ipairs(line_widths) do
width = math.max(width, l_w)
end
-- - Limit from above for better visuals
max_width_share = math.min(math.max(max_width_share, 0), 1)
local max_width = math.max(math.floor(max_width_share * vim.o.columns), 1)
width = math.min(width, max_width)
-- Compute height based on the width so as to fit all lines with 'wrap' on
local height = 0
for _, l_w in ipairs(line_widths) do
height = height + math.floor(math.max(l_w - 1, 0) / width) + 1
end
return width, height
end
-- Window ---------------------------------------------------------------------
H.window_open = function(buf_id)
local config = H.window_compute_config(buf_id, true)
local win_id = vim.api.nvim_open_win(buf_id, false, config)
vim.wo[win_id].foldenable = false
vim.wo[win_id].wrap = true
vim.wo[win_id].winblend = H.get_config().window.winblend
vim.wo[win_id].winhighlight = 'NormalFloat:MiniNotifyNormal,FloatBorder:MiniNotifyBorder,FloatTitle:MiniNotifyTitle'
return win_id
end
H.window_compute_config = function(buf_id, is_for_open)
local has_tabline = vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)
local has_statusline = vim.o.laststatus > 0
local max_height = vim.o.lines - vim.o.cmdheight - (has_tabline and 1 or 0) - (has_statusline and 1 or 0)
local max_width = vim.o.columns
local config_win = H.get_config().window
local default_config = { relative = 'editor', style = 'minimal', noautocmd = is_for_open, zindex = 999 }
default_config.anchor, default_config.col, default_config.row = 'NE', vim.o.columns, has_tabline and 1 or 0
default_config.width, default_config.height = H.buffer_default_dimensions(buf_id, config_win.max_width_share)
default_config.border = 'single'
-- Don't allow focus to not disrupt window navigation
default_config.focusable = false
local win_config = config_win.config
if vim.is_callable(win_config) then win_config = win_config(buf_id) end
local config = vim.tbl_deep_extend('force', default_config, win_config or {})
-- Tweak config values to ensure they are proper, accounting for border
local offset = config.border == 'none' and 0 or 2
config.height = math.min(config.height, max_height - offset)
config.width = math.min(config.width, max_width - offset)
return config
end
H.window_close = function()
if H.is_valid_win(H.cache.win_id) then vim.api.nvim_win_close(H.cache.win_id, true) end
H.cache.win_id = nil
end
-- Notifications --------------------------------------------------------------
H.validate_msg = function(x)
if type(x) ~= 'string' then H.error('`msg` should be string.') end
end
H.validate_level = function(x)
if vim.log.levels[x] == nil then H.error('`level` should be key of `vim.log.levels`.') end
end
H.validate_hl_group = function(x)
if type(x) ~= 'string' then H.error('`hl_group` should be string.') end
end
H.is_notification = function(x)
return type(x) == 'table'
and type(x.msg) == 'string'
and vim.log.levels[x.level] ~= nil
and type(x.hl_group) == 'string'
and type(x.ts_add) == 'number'
and type(x.ts_update) == 'number'
and (x.ts_remove == nil or type(x.ts_remove) == 'number')
end
H.is_notification_array = function(x)
if not H.islist(x) then return false end
for _, y in ipairs(x) do
if not H.is_notification(y) then return false end
end
return true
end
H.notif_apply_format = function(notif_arr, format)
for _, notif in ipairs(notif_arr) do
local res = format(notif)
if type(res) ~= 'string' then H.error('Output of `content.format` should be string.') end
notif.msg = res
end
return notif_arr
end
H.notif_compare = function(a, b)
local a_priority, b_priority = H.level_priority[a.level], H.level_priority[b.level]
return a_priority > b_priority or (a_priority == b_priority and a.ts_update > b.ts_update)
end
-- Utilities ------------------------------------------------------------------
H.error = function(msg) error(string.format('(mini.notify) %s', msg), 0) end
H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
H.is_win_in_tabpage = function(win_id) return vim.api.nvim_win_get_tabpage(win_id) == vim.api.nvim_get_current_tabpage() end
H.get_timestamp = function()
-- This is more acceptable for `vim.fn.strftime()` than `vim.loop.hrtime()`
local seconds, microseconds = vim.loop.gettimeofday()
return seconds + 0.000001 * microseconds
end
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
return MiniNotify

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,645 @@
--- *mini.pairs* Autopairs
--- *MiniPairs*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Functionality to work with 'paired' characters conditional on cursor's
--- neighborhood (two characters to its left and right).
---
--- - Usage should be through making appropriate mappings using |MiniPairs.map|
--- or in |MiniPairs.setup| (for global mapping), |MiniPairs.map_buf| (for
--- buffer mapping).
---
--- - Pairs get automatically registered to be recognized by `<BS>` and `<CR>`.
---
--- What it doesn't do:
--- - It doesn't support multiple characters as "open" and "close" symbols. Use
--- snippets for that.
---
--- - It doesn't support dependency on filetype. Use |i_CTRL-V| to insert
--- single symbol or `autocmd` command or 'after/ftplugin' approach to:
--- - `:lua MiniPairs.map_buf(0, 'i', <*>, <pair_info>)` - make new mapping
--- for '<*>' in current buffer.
--- - `:lua MiniPairs.unmap_buf(0, 'i', <*>, <pair>)` - unmap key `<*>` while
--- unregistering `<pair>` pair in current buffer. Note: this reverts
--- mapping done by |MiniPairs.map_buf|. If mapping was done with
--- |MiniPairs.map|, unmap for buffer in usual Neovim manner:
--- `inoremap <buffer> <*> <*>` (this maps `<*>` key to do the same it
--- does by default).
--- - Disable module for buffer (see 'Disabling' section).
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.pairs').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniPairs` which you can use for scripting or manually (with
--- `:lua MiniPairs.*`).
---
--- See |MiniPairs.config| for `config` structure and default values.
---
--- This module doesn't have runtime options, so using `vim.b.minipairs_config`
--- will have no effect here.
---
--- # Example mappings ~
--- >lua
--- -- Register quotes inside `config` of `MiniPairs.setup()`
--- mappings = {
--- ['"'] = { register = { cr = true } },
--- ["'"] = { register = { cr = true } },
--- }
---
--- -- Insert `<>` pair if `<` is typed at line start, don't register for `<CR>`
--- local lt_opts = {
--- action = 'open',
--- pair = '<>',
--- neigh_pattern = '\r.',
--- register = { cr = false },
--- }
--- MiniPairs.map('i', '<', lt_opts)
---
--- local gt_opts = { action = 'close', pair = '<>', register = { cr = false } }
--- MiniPairs.map('i', '>', gt_opts)
---
--- -- Create symmetrical `$$` pair only in Tex files
--- local map_tex = function()
--- MiniPairs.map_buf(0, 'i', '$', { action = 'closeopen', pair = '$$' })
--- end
--- vim.api.nvim_create_autocmd(
--- 'FileType',
--- { pattern = 'tex', callback = map_tex }
--- )
--- <
--- # Notes ~
---
--- - Make sure to make proper mapping of `<CR>` in order to support completion
--- plugin of your choice:
--- - For |MiniCompletion| see 'Helpful key mappings' section.
--- - For current implementation of "hrsh7th/nvim-cmp" there is no need to
--- make custom mapping. You can use default setup, which will confirm
--- completion selection if popup is visible and expand pair otherwise.
--- - Having mapping in terminal mode can conflict with:
--- - Autopairing capabilities of interpretators (`ipython`, `radian`).
--- - Vim mode of terminal itself.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minipairs_disable` (globally) or `vim.b.minipairs_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.
---@alias __pairs_neigh_pattern string|nil Pattern for two neighborhood characters.
--- Character "\r" indicates line start, "\n" - line end.
---@alias __pairs_pair string String with two characters representing pair.
---@alias __pairs_unregistered_pair string Pair which should be unregistered from both
--- `<BS>` and `<CR>`. Should be explicitly supplied to avoid confusion.
--- Supply `''` to not unregister pair.
-- Module definition ==========================================================
local MiniPairs = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniPairs.config|.
---
---@usage >lua
--- require('mini.pairs').setup() -- use default config
--- -- OR
--- require('mini.pairs').setup({}) -- replace {} with your config table
--- <
MiniPairs.setup = function(config)
-- Export module
_G.MiniPairs = MiniPairs
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
end
--stylua: ignore
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniPairs.config = {
-- In which modes mappings from this `config` should be created
modes = { insert = true, command = false, terminal = false },
-- Global mappings. Each right hand side should be a pair information, a
-- table with at least these fields (see more in |MiniPairs.map|):
-- - <action> - one of "open", "close", "closeopen".
-- - <pair> - two character string for pair to be used.
-- By default pair is not inserted after `\`, quotes are not recognized by
-- `<CR>`, `'` does not insert pair after a letter.
-- Only parts of tables can be tweaked (others will use these defaults).
-- Supply `false` instead of table to not map particular key.
mappings = {
['('] = { action = 'open', pair = '()', neigh_pattern = '[^\\].' },
['['] = { action = 'open', pair = '[]', neigh_pattern = '[^\\].' },
['{'] = { action = 'open', pair = '{}', neigh_pattern = '[^\\].' },
[')'] = { action = 'close', pair = '()', neigh_pattern = '[^\\].' },
[']'] = { action = 'close', pair = '[]', neigh_pattern = '[^\\].' },
['}'] = { action = 'close', pair = '{}', neigh_pattern = '[^\\].' },
['"'] = { action = 'closeopen', pair = '""', neigh_pattern = '[^\\].', register = { cr = false } },
["'"] = { action = 'closeopen', pair = "''", neigh_pattern = '[^%a\\].', register = { cr = false } },
['`'] = { action = 'closeopen', pair = '``', neigh_pattern = '[^\\].', register = { cr = false } },
},
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Make global mapping
---
--- This is a wrapper for |nvim_set_keymap()| but instead of right hand side of
--- mapping (as string) it expects table with pair information.
---
--- Using this function instead of |nvim_set_keymap()| allows automatic
--- registration of pairs which will be recognized by `<BS>` and `<CR>`.
--- It also infers mapping description from `pair_info`.
---
---@param mode string `mode` for |nvim_set_keymap()|.
---@param lhs string `lhs` for |nvim_set_keymap()|.
---@param pair_info table Table with pair information. Fields:
--- - <action> - one of "open" (for |MiniPairs.open|),
--- "close" (for |MiniPairs.close|), or "closeopen" (for |MiniPairs.closeopen|).
--- - <pair> - two character string to be used as argument for action function.
--- - <neigh_pattern> - optional 'two character' neighborhood pattern to be
--- used as argument for action function.
--- Default: `'..'` (no restriction from neighborhood).
--- - <register> - optional table with information about whether this pair will
--- be recognized by `<BS>` (in |MiniPairs.bs|) and/or `<CR>` (in |MiniPairs.cr|).
--- Should have boolean fields <bs> and <cr> which are both `true` by
--- default (if not overridden explicitly).
---@param opts table|nil Optional table `opts` for |nvim_set_keymap()|. Elements
--- `expr` and `noremap` won't be recognized (`true` by default).
MiniPairs.map = function(mode, lhs, pair_info, opts)
pair_info = H.validate_pair_info(pair_info)
opts = vim.tbl_deep_extend('force', opts or {}, { expr = true, noremap = true })
opts.desc = H.infer_mapping_description(pair_info)
vim.api.nvim_set_keymap(mode, lhs, H.pair_info_to_map_rhs(pair_info), opts)
H.register_pair(pair_info, mode, 'all')
-- Ensure that `<BS>` and `<CR>` are mapped for input mode
H.ensure_cr_bs(mode)
end
--- Make buffer mapping
---
--- This is a wrapper for |nvim_buf_set_keymap()| but instead of string right
--- hand side of mapping it expects table with pair information similar to one
--- in |MiniPairs.map|.
---
--- Using this function instead of |nvim_buf_set_keymap()| allows automatic
--- registration of pairs which will be recognized by `<BS>` and `<CR>`.
--- It also infers mapping description from `pair_info`.
---
---@param buffer number `buffer` for |nvim_buf_set_keymap()|.
---@param mode string `mode` for |nvim_buf_set_keymap()|.
---@param lhs string `lhs` for |nvim_buf_set_keymap()|.
---@param pair_info table Table with pair information.
---@param opts table|nil Optional table `opts` for |nvim_buf_set_keymap()|.
--- Elements `expr` and `noremap` won't be recognized (`true` by default).
MiniPairs.map_buf = function(buffer, mode, lhs, pair_info, opts)
pair_info = H.validate_pair_info(pair_info)
opts = vim.tbl_deep_extend('force', opts or {}, { expr = true, noremap = true })
opts.desc = H.infer_mapping_description(pair_info)
vim.api.nvim_buf_set_keymap(buffer, mode, lhs, H.pair_info_to_map_rhs(pair_info), opts)
H.register_pair(pair_info, mode, buffer == 0 and vim.api.nvim_get_current_buf() or buffer)
-- Ensure that `<BS>` and `<CR>` are mapped for input mode
H.ensure_cr_bs(mode)
end
--- Remove global mapping
---
--- A wrapper for |nvim_del_keymap()| which registers supplied `pair`.
---
---@param mode string `mode` for |nvim_del_keymap()|.
---@param lhs string `lhs` for |nvim_del_keymap()|.
---@param pair __pairs_unregistered_pair
MiniPairs.unmap = function(mode, lhs, pair)
-- `pair` should be supplied explicitly
vim.validate({ pair = { pair, 'string' } })
-- Use `pcall` to allow 'deleting' already deleted mapping
pcall(vim.api.nvim_del_keymap, mode, lhs)
if pair == '' then return end
H.unregister_pair(pair, mode, 'all')
end
--- Remove buffer mapping
---
--- Wrapper for |nvim_buf_del_keymap()| which also unregisters supplied `pair`.
---
--- Note: this only reverts mapping done by |MiniPairs.map_buf|. If mapping was
--- done with |MiniPairs.map|, revert to default behavior for buffer: >lua
---
--- -- Map `X` key to do the same it does by default
--- vim.keymap.set('i', 'X', 'X', { buffer = true })
--- <
---@param buffer number `buffer` for |nvim_buf_del_keymap()|.
---@param mode string `mode` for |nvim_buf_del_keymap()|.
---@param lhs string `lhs` for |nvim_buf_del_keymap()|.
---@param pair __pairs_unregistered_pair
MiniPairs.unmap_buf = function(buffer, mode, lhs, pair)
-- `pair` should be supplied explicitly
vim.validate({ pair = { pair, 'string' } })
-- Use `pcall` to allow 'deleting' already deleted mapping
pcall(vim.api.nvim_buf_del_keymap, buffer, mode, lhs)
if pair == '' then return end
H.unregister_pair(pair, mode, buffer == 0 and vim.api.nvim_get_current_buf() or buffer)
end
--- Process "open" symbols
---
--- Used as |map-expr| mapping for "open" symbols in asymmetric pair ('(', '[',
--- etc.). If neighborhood doesn't match supplied pattern, function results
--- into "open" symbol. Otherwise, it pastes whole pair and moves inside pair
--- with |<Left>|.
---
--- Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping.
---
---@param pair __pairs_pair
---@param neigh_pattern __pairs_neigh_pattern
---
---@return string Keys performing "open" action.
MiniPairs.open = function(pair, neigh_pattern)
if H.is_disabled() or not H.neigh_match(neigh_pattern) then return pair:sub(1, 1) end
return ('%s%s'):format(pair, H.get_arrow_key('left'))
end
--- Process "close" symbols
---
--- Used as |map-expr| mapping for "close" symbols in asymmetric pair (')',
--- ']', etc.). If neighborhood doesn't match supplied pattern, function
--- results into "close" symbol. Otherwise it jumps over symbol to the right of
--- cursor (with |<Right>|) if it is equal to "close" one and inserts it
--- otherwise.
---
--- Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping.
---
---@param pair __pairs_pair
---@param neigh_pattern __pairs_neigh_pattern
---
---@return string Keys performing "close" action.
MiniPairs.close = function(pair, neigh_pattern)
if H.is_disabled() or not H.neigh_match(neigh_pattern) then return pair:sub(2, 2) end
local close = pair:sub(2, 2)
if H.get_cursor_neigh(1, 1) == close then
return H.get_arrow_key('right')
else
return close
end
end
--- Process "closeopen" symbols
---
--- Used as |map-expr| mapping for 'symmetrical' symbols (from pairs '""',
--- '\'\'', '``'). It tries to perform 'closeopen action': move over right
--- character (with |<Right>|) if it is equal to second character from pair or
--- conditionally paste pair otherwise (with |MiniPairs.open()|).
---
--- Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping.
---
---@param pair __pairs_pair
---@param neigh_pattern __pairs_neigh_pattern
---
---@return string Keys performing "closeopen" action.
MiniPairs.closeopen = function(pair, neigh_pattern)
if H.is_disabled() or H.get_cursor_neigh(1, 1) ~= pair:sub(2, 2) then
return MiniPairs.open(pair, neigh_pattern)
else
return H.get_arrow_key('right')
end
end
--- Process |<BS>|
---
--- Used as |map-expr| mapping for `<BS>` in Insert mode. It removes whole pair
--- (via executing `<Del>` after input key) if neighborhood is equal to a whole
--- pair recognized for current buffer. Pair is recognized for current buffer
--- if it is registered for global or current buffer mapping. Pair is
--- registered as a result of calling |MiniPairs.map| or |MiniPairs.map_buf|.
---
--- Mapped by default inside |MiniPairs.setup|.
---
--- This can be used to modify other Insert mode keys to respect neighborhood
--- pair. Examples: >lua
---
--- local map_bs = function(lhs, rhs)
--- vim.keymap.set('i', lhs, rhs, { expr = true, replace_keycodes = false })
--- end
---
--- map_bs('<C-h>', 'v:lua.MiniPairs.bs()')
--- map_bs('<C-w>', 'v:lua.MiniPairs.bs("\23")')
--- map_bs('<C-u>', 'v:lua.MiniPairs.bs("\21")')
--- <
---@param key string|nil Key to use. Default: `<BS>`.
---
---@return string Keys performing "backspace" action.
MiniPairs.bs = function(key)
local res = key or H.keys.bs
local neigh = H.get_cursor_neigh(0, 1)
if not H.is_disabled() and H.is_pair_registered(neigh, vim.fn.mode(), 0, 'bs') then
res = ('%s%s'):format(res, H.keys.del)
end
return res
end
--- Process |i_<CR>|
---
--- Used as |map-expr| mapping for `<CR>` in insert mode. It puts "close"
--- symbol on next line (via `<CR><C-o>O`) if neighborhood is equal to a whole
--- pair recognized for current buffer. Pair is recognized for current buffer
--- if it is registered for global or current buffer mapping. Pair is
--- registered as a result of calling |MiniPairs.map| or |MiniPairs.map_buf|.
---
--- Note: some relevant mode changing events are temporarily ignored
--- (with |eventignore|) to counter effect of using |i_CTRL-O|.
---
--- Mapped by default inside |MiniPairs.setup|.
---
---@param key string|nil Key to use. Default: `<CR>`.
---
---@return string Keys performing "new line" action.
MiniPairs.cr = function(key)
local res = key or H.keys.cr
local neigh = H.get_cursor_neigh(0, 1)
if not H.is_disabled() and H.is_pair_registered(neigh, vim.fn.mode(), 0, 'cr') then
-- Temporarily ignore mode change to not trigger some common expensive
-- autocommands (like diagnostic check, etc.)
local cache_eventignore = vim.o.eventignore
vim.o.eventignore = 'InsertLeave,InsertLeavePre,InsertEnter,ModeChanged'
H.restore_eventignore(cache_eventignore)
res = ('%s%s'):format(res, H.keys.above)
end
return res
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniPairs.config)
-- Default value of `pair_info` for mapping functions
H.default_pair_info = { neigh_pattern = '..', register = { bs = true, cr = true } }
-- Pair sets registered *per mode-buffer-key*. Buffer `'all'` contains pairs
-- registered for all buffers.
H.registered_pairs = {
i = { all = { bs = {}, cr = {} } },
c = { all = { bs = {}, cr = {} } },
t = { all = { bs = {}, cr = {} } },
}
-- Precomputed keys to increase speed
-- stylua: ignore start
local function escape(s) return vim.api.nvim_replace_termcodes(s, true, true, true) end
H.keys = {
above = escape('<C-o>O'),
bs = escape('<bs>'),
cr = escape('<cr>'),
del = escape('<del>'),
keep_undo = escape('<C-g>U'),
-- NOTE: use `get_arrow_key()` instead of `H.keys.left` or `H.keys.right`
left = escape('<left>'),
right = escape('<right>')
}
-- stylua: ignore 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({
modes = { config.modes, 'table' },
mappings = { config.mappings, 'table' },
})
vim.validate({
['modes.insert'] = { config.modes.insert, 'boolean' },
['modes.command'] = { config.modes.command, 'boolean' },
['modes.terminal'] = { config.modes.terminal, 'boolean' },
})
local validate_mapping = function(pair_info, prefix)
-- Allow `false` to not create mapping
if pair_info == false then return end
H.validate_pair_info(pair_info, prefix)
end
validate_mapping(config.mappings['('], "mappings['(']")
validate_mapping(config.mappings['['], "mappings['[']")
validate_mapping(config.mappings['{'], "mappings['{']")
validate_mapping(config.mappings[')'], "mappings[')']")
validate_mapping(config.mappings[']'], "mappings[']']")
validate_mapping(config.mappings['}'], "mappings['}']")
validate_mapping(config.mappings['"'], "mappings['\"']")
validate_mapping(config.mappings["'"], 'mappings["\'"]')
validate_mapping(config.mappings['`'], "mappings['`']")
return config
end
H.apply_config = function(config)
MiniPairs.config = config
-- Setup mappings in supplied modes
local mode_ids = { insert = 'i', command = 'c', terminal = 't' }
-- Compute in which modes mapping should be set up
local mode_array = {}
for name, to_set in pairs(config.modes) do
if to_set then table.insert(mode_array, mode_ids[name]) end
end
local map_conditionally = function(mode, key, pair_info)
-- Allow `false` to not create mapping
if pair_info == false then return end
-- This also should take care of mapping `<BS>` and `<CR>`
MiniPairs.map(mode, key, pair_info)
end
for _, mode in pairs(mode_array) do
for key, pair_info in pairs(config.mappings) do
map_conditionally(mode, key, pair_info)
end
end
end
H.create_autocommands = function()
local augroup = vim.api.nvim_create_augroup('MiniPairs', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
au('FileType', { 'TelescopePrompt', 'fzf' }, function() vim.b.minipairs_disable = true end, 'Disable locally')
end
H.is_disabled = function() return vim.g.minipairs_disable == true or vim.b.minipairs_disable == true end
-- Pair registration ----------------------------------------------------------
H.register_pair = function(pair_info, mode, buffer)
-- Process new mode
H.registered_pairs[mode] = H.registered_pairs[mode] or { all = { bs = {}, cr = {} } }
local mode_pairs = H.registered_pairs[mode]
-- Process new buffer
mode_pairs[buffer] = mode_pairs[buffer] or { bs = {}, cr = {} }
-- Register pair if it is not already registered
local register, pair = pair_info.register, pair_info.pair
if register.bs and not vim.tbl_contains(mode_pairs[buffer].bs, pair) then
table.insert(mode_pairs[buffer].bs, pair)
end
if register.cr and not vim.tbl_contains(mode_pairs[buffer].cr, pair) then
table.insert(mode_pairs[buffer].cr, pair)
end
end
H.unregister_pair = function(pair, mode, buffer)
local mode_pairs = H.registered_pairs[mode]
if not (mode_pairs and mode_pairs[buffer]) then return end
local buf_pairs = mode_pairs[buffer]
for _, key in ipairs({ 'bs', 'cr' }) do
for i, p in ipairs(buf_pairs[key]) do
if p == pair then table.remove(buf_pairs[key], i) end
end
end
end
H.is_pair_registered = function(pair, mode, buffer, key)
local mode_pairs = H.registered_pairs[mode]
if not mode_pairs then return false end
if vim.tbl_contains(mode_pairs['all'][key], pair) then return true end
buffer = buffer == 0 and vim.api.nvim_get_current_buf() or buffer
local buf_pairs = mode_pairs[buffer]
if not buf_pairs then return false end
return vim.tbl_contains(buf_pairs[key], pair)
end
H.ensure_cr_bs = function(mode)
local has_any_cr_pair, has_any_bs_pair = false, false
for _, pair_tbl in pairs(H.registered_pairs[mode]) do
has_any_cr_pair = has_any_cr_pair or not vim.tbl_isempty(pair_tbl.cr)
has_any_bs_pair = has_any_bs_pair or not vim.tbl_isempty(pair_tbl.bs)
end
-- NOTE: this doesn't distinguish between global and buffer mappings. Both
-- `<BS>` and `<CR>` should work as normal even if no pairs are registered
if has_any_bs_pair then
-- Use not `silent` in Command mode to make it redraw
local opts = { silent = mode ~= 'c', expr = true, replace_keycodes = false, desc = 'MiniPairs <BS>' }
H.map(mode, '<BS>', 'v:lua.MiniPairs.bs()', opts)
end
if mode == 'i' and has_any_cr_pair then
local opts = { expr = true, replace_keycodes = false, desc = 'MiniPairs <CR>' }
H.map(mode, '<CR>', 'v:lua.MiniPairs.cr()', opts)
end
end
-- Work with pair_info --------------------------------------------------------
H.validate_pair_info = function(pair_info, prefix)
prefix = prefix or 'pair_info'
vim.validate({ [prefix] = { pair_info, 'table' } })
pair_info = vim.tbl_deep_extend('force', H.default_pair_info, pair_info)
vim.validate({
[prefix .. '.action'] = { pair_info.action, 'string' },
[prefix .. '.pair'] = { pair_info.pair, 'string' },
[prefix .. '.neigh_pattern'] = { pair_info.neigh_pattern, 'string' },
[prefix .. '.register'] = { pair_info.register, 'table' },
})
vim.validate({
[prefix .. '.register.bs'] = { pair_info.register.bs, 'boolean' },
[prefix .. '.register.cr'] = { pair_info.register.cr, 'boolean' },
})
return pair_info
end
H.pair_info_to_map_rhs = function(pair_info)
return ('v:lua.MiniPairs.%s(%s, %s)'):format(
pair_info.action,
vim.inspect(pair_info.pair),
vim.inspect(pair_info.neigh_pattern)
)
end
H.infer_mapping_description = function(pair_info)
local action_name = pair_info.action:sub(1, 1):upper() .. pair_info.action:sub(2)
return ('%s action for %s pair'):format(action_name, vim.inspect(pair_info.pair))
end
-- Utilities ------------------------------------------------------------------
H.get_cursor_neigh = function(start, finish)
local line, col
if vim.fn.mode() == 'c' then
line = vim.fn.getcmdline()
col = vim.fn.getcmdpos()
-- Adjust start and finish because output of `getcmdpos()` starts counting
-- columns from 1
start = start - 1
finish = finish - 1
else
line = vim.api.nvim_get_current_line()
col = vim.api.nvim_win_get_cursor(0)[2]
end
-- Add '\r' and '\n' to always return 2 characters
return string.sub(('%s%s%s'):format('\r', line, '\n'), col + 1 + start, col + 1 + finish)
end
H.neigh_match = function(pattern) return (pattern == nil) or (H.get_cursor_neigh(0, 1):find(pattern) ~= nil) end
H.get_arrow_key = function(key)
if vim.fn.mode() == 'i' then
-- Using left/right keys in insert mode breaks undo sequence and, more
-- importantly, dot-repeat. To avoid this, use 'i_CTRL-G_U' mapping.
return H.keys.keep_undo .. H.keys[key]
else
return H.keys[key]
end
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.restore_eventignore = vim.schedule_wrap(function(val) vim.o.eventignore = val end)
return MiniPairs

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,643 @@
--- *mini.sessions* Session management
--- *MiniSessions*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Read, write, and delete sessions. Works using |mksession| (meaning
--- 'sessionoptions' is fully respected). This is intended as a drop-in Lua
--- replacement for session management part of 'mhinz/vim-startify' (works out
--- of the box with sessions created by it). Implements both global (from
--- configured directory) and local (from current directory) sessions.
---
--- Key design ideas:
--- - Sessions are represented by readable files (results of applying
--- |mksession|). There are two kinds of sessions:
--- - Global: any file inside a configurable directory.
--- - Local: configurable file inside current working directory (|getcwd|).
---
--- - All session files are detected during `MiniSessions.setup()` and on any
--- relevant action with session names being file names (including their
--- possible extension).
---
--- - Store information about detected sessions in separate table
--- (|MiniSessions.detected|) and operate only on it. Meaning if this
--- information changes, there will be no effect until next detection. So to
--- avoid confusion, don't directly use |mksession| and |source| for writing
--- and reading sessions files.
---
--- Features:
--- - Autoread default session (local if detected, latest otherwise) if Neovim
--- was called without intention to show something else.
---
--- - Autowrite current session before quitting Neovim.
---
--- - Configurable severity level of all actions.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.sessions').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniSessions` which you can use for scripting or manually (with
--- `:lua MiniSessions.*`).
---
--- See |MiniSessions.config| for `config` structure and default values.
---
--- This module doesn't benefit from buffer local configuration, so using
--- `vim.b.minisessions_config` will have no effect here.
---
--- # Disabling ~
---
--- To disable core functionality, set `vim.g.minisessions_disable` (globally) or
--- `vim.b.minisessions_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 MiniSessions = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniSessions.config|.
---
---@usage >lua
--- require('mini.sessions').setup() -- use default config
--- -- OR
--- require('mini.sessions').setup({}) -- replace {} with your config table
--- <
MiniSessions.setup = function(config)
-- Export module
_G.MiniSessions = MiniSessions
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniSessions.config = {
-- Whether to read latest session if Neovim opened without file arguments
autoread = false,
-- Whether to write current session before quitting Neovim
autowrite = true,
-- Directory where global sessions are stored (use `''` to disable)
--minidoc_replace_start directory = --<"session" subdir of user data directory from |stdpath()|>,
directory = vim.fn.stdpath('data') .. '/session',
--minidoc_replace_end
-- File for local session (use `''` to disable)
file = 'Session.vim',
-- Whether to force possibly harmful actions (meaning depends on function)
force = { read = false, write = true, delete = false },
-- Hook functions for actions. Default `nil` means 'do nothing'.
-- Takes table with active session data as argument.
hooks = {
-- Before successful action
pre = { read = nil, write = nil, delete = nil },
-- After successful action
post = { read = nil, write = nil, delete = nil },
},
-- Whether to print session path after action
verbose = { read = false, write = true, delete = true },
}
--minidoc_afterlines_end
-- Module data ================================================================
--- Table of detected sessions. Keys represent session name. Values are tables
--- with session information that currently has these fields (but subject to
--- change):
--- - <modify_time> `(number)` modification time (see |getftime|) of session file.
--- - <name> `(string)` name of session (should be equal to table key).
--- - <path> `(string)` full path to session file.
--- - <type> `(string)` type of session ('global' or 'local').
MiniSessions.detected = {}
-- Module functionality =======================================================
--- Read detected session
---
--- What it does:
--- - If there is an active session, write it with |MiniSessions.write()|.
--- - Delete all current buffers with |bwipeout|. This is needed to correctly
--- restore buffers from target session. If `force` is not `true`, checks
--- beforehand for unsaved listed buffers and stops if there is any.
--- - Source session with supplied name.
---
---@param session_name string|nil Name of detected session file to read. Default:
--- `nil` for default session: local (if detected) or latest session (see
--- |MiniSessions.get_latest|).
---@param opts table|nil Table with options. Current allowed keys:
--- - <force> (whether to delete unsaved buffers; default:
--- `MiniSessions.config.force.read`).
--- - <verbose> (whether to print session path after action; default
--- `MiniSessions.config.verbose.read`).
--- - <hooks> (a table with <pre> and <post> function hooks to be executed
--- with session data argument before and after successful read; overrides
--- `MiniSessions.config.hooks.pre.read` and
--- `MiniSessions.config.hooks.post.read`).
MiniSessions.read = function(session_name, opts)
if H.is_disabled() then return end
-- Make sessions up to date
H.detect_sessions()
if vim.tbl_count(MiniSessions.detected) == 0 then
return H.message('There is no detected sessions. Change configuration and rerun `MiniSessions.setup()`.')
end
-- Get session data
if session_name == nil then
if MiniSessions.detected[MiniSessions.config.file] ~= nil then
session_name = MiniSessions.config.file
else
session_name = MiniSessions.get_latest()
end
end
opts = vim.tbl_deep_extend('force', H.default_opts('read'), opts or {})
if not H.validate_detected(session_name) then return end
local data = MiniSessions.detected[session_name]
-- Possibly check for unsaved listed buffers and do nothing if present
if not opts.force then
local unsaved_listed_buffers = H.get_unsaved_listed_buffers()
if #unsaved_listed_buffers > 0 then
local buf_list = table.concat(unsaved_listed_buffers, ', ')
H.error(('There are unsaved listed buffers: %s.'):format(buf_list))
end
end
-- Write current session to allow proper switching between sessions
if vim.v.this_session ~= '' then MiniSessions.write(nil, { force = true, verbose = false }) end
-- Execute 'pre' hook
H.possibly_execute(opts.hooks.pre, data)
-- Wipeout all buffers
vim.cmd('silent! %bwipeout!')
-- Read session file
local session_path = data.path
vim.cmd(('silent! source %s'):format(vim.fn.fnameescape(session_path)))
vim.v.this_session = session_path
-- Possibly notify
if opts.verbose then H.message(('Read session %s'):format(session_path)) end
-- Execute 'post' hook
H.possibly_execute(opts.hooks.post, data)
end
--- Write session
---
--- What it does:
--- - Check if file for supplied session name already exists. If it does and
--- `force` is not `true`, then stop.
--- - Write session with |mksession| to a file named `session_name`. Its
--- directory is determined based on type of session:
--- - It is at location |v:this_session| if `session_name` is `nil` and
--- there is current session.
--- - It is current working directory (|getcwd|) if `session_name` is equal
--- to `MiniSessions.config.file` (represents local session).
--- - It is `MiniSessions.config.directory` otherwise (represents global
--- session).
--- - Update |MiniSessions.detected|.
---
---@param session_name string|nil Name of session file to write. Default: `nil` for
--- current session (|v:this_session|).
---@param opts table|nil Table with options. Current allowed keys:
--- - <force> (whether to ignore existence of session file; default:
--- `MiniSessions.config.force.write`).
--- - <verbose> (whether to print session path after action; default
--- `MiniSessions.config.verbose.write`).
--- - <hooks> (a table with <pre> and <post> function hooks to be executed
--- with session data argument before and after successful write; overrides
--- `MiniSessions.config.hooks.pre.write` and
--- `MiniSessions.config.hooks.post.write`).
MiniSessions.write = function(session_name, opts)
if H.is_disabled() then return end
opts = vim.tbl_deep_extend('force', H.default_opts('write'), opts or {})
local session_path = H.name_to_path(session_name)
if not opts.force and H.is_readable_file(session_path) then
H.error([[Can't write to existing session when `opts.force` is not `true`.]])
end
local data = H.new_session(session_path)
-- Execute 'pre' hook
H.possibly_execute(opts.hooks.pre, data)
-- Make session file
local cmd = ('mksession%s'):format(opts.force and '!' or '')
vim.cmd(('%s %s'):format(cmd, vim.fn.fnameescape(session_path)))
data.modify_time = vim.fn.getftime(session_path)
-- Update detected sessions
MiniSessions.detected[data.name] = data
-- Possibly notify
if opts.verbose then H.message(('Written session %s'):format(session_path)) end
-- Execute 'post' hook
H.possibly_execute(opts.hooks.post, data)
end
--- Delete detected session
---
--- What it does:
--- - Check if session name is a current one. If yes and `force` is not `true`,
--- then stop.
--- - Delete session.
--- - Update |MiniSessions.detected|.
---
---@param session_name string|nil Name of detected session file to delete. Default:
--- `nil` for name of current session (taken from |v:this_session|).
---@param opts table|nil Table with options. Current allowed keys:
--- - <force> (whether to allow deletion of current session; default:
--- `MiniSessions.config.force.delete`).
--- - <verbose> (whether to print session path after action; default
--- `MiniSessions.config.verbose.delete`).
--- - <hooks> (a table with <pre> and <post> function hooks to be executed
--- with session data argument before and after successful delete; overrides
--- `MiniSessions.config.hooks.pre.delete` and
--- `MiniSessions.config.hooks.post.delete`).
MiniSessions.delete = function(session_name, opts)
if H.is_disabled() then return end
if vim.tbl_count(MiniSessions.detected) == 0 then
H.error('There is no detected sessions. Change configuration and rerun `MiniSessions.setup()`.')
end
opts = vim.tbl_deep_extend('force', H.default_opts('delete'), opts or {})
local session_path = H.name_to_path(session_name)
-- Make sessions up to date
H.detect_sessions()
-- Make sure to delete only detected session (matters for local session)
session_name = vim.fn.fnamemodify(session_path, ':t')
if not H.validate_detected(session_name) then return end
session_path = MiniSessions.detected[session_name].path
local is_current_session = session_path == vim.v.this_session
if not opts.force and is_current_session then
H.error([[Can't delete current session when `opts.force` is not `true`.]])
end
local data = MiniSessions.detected[session_name]
-- Execute 'pre' hook
H.possibly_execute(opts.hooks.pre, data)
-- Delete and update detected sessions
vim.fn.delete(session_path)
MiniSessions.detected[session_name] = nil
if is_current_session then vim.v.this_session = '' end
-- Possibly notify
if opts.verbose then H.message(('Deleted session %s'):format(session_path)) end
-- Execute 'pre' hook
H.possibly_execute(opts.hooks.post, data)
end
--- Select session interactively and perform action
---
--- Note: this uses |vim.ui.select()| function. For more user-friendly
--- experience, override it (for example, with external plugins like
--- "stevearc/dressing.nvim").
---
---@param action string|nil Action to perform. Should be one of "read" (default),
--- "write", or "delete".
---@param opts table|nil Options for specified action.
MiniSessions.select = function(action, opts)
if not (type(vim.ui) == 'table' and type(vim.ui.select) == 'function') then
H.error('`MiniSessions.select()` requires `vim.ui.select()` function.')
end
action = action or 'read'
if not vim.tbl_contains({ 'read', 'write', 'delete' }, action) then
H.error("`action` should be one of 'read', 'write', or 'delete'.")
end
-- Make sessions up to date
H.detect_sessions()
-- Ensure consistent order of items
local detected = {}
for _, session in pairs(MiniSessions.detected) do
table.insert(detected, session)
end
local sort_fun = function(a, b)
-- Put local session first, others - increasing alphabetically
local a_name = a.type == 'local' and '' or a.name
local b_name = b.type == 'local' and '' or b.name
return a_name < b_name
end
table.sort(detected, sort_fun)
local detected_names = vim.tbl_map(function(x) return x.name end, detected)
vim.ui.select(detected_names, {
prompt = 'Select session to ' .. action,
format_item = function(x) return ('%s (%s)'):format(x, MiniSessions.detected[x].type) end,
}, function(item, idx)
if item == nil then return end
MiniSessions[action](item, opts)
end)
end
--- Get name of latest detected session
---
--- Latest session is the session with the latest modification time determined
--- by |getftime|.
---
---@return string|nil Name of latest session or `nil` if there is no sessions.
MiniSessions.get_latest = function()
if vim.tbl_count(MiniSessions.detected) == 0 then return end
local latest_time, latest_name = -1, nil
for name, data in pairs(MiniSessions.detected) do
if data.modify_time > latest_time then
latest_time, latest_name = data.modify_time, name
end
end
return latest_name
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniSessions.config)
-- 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({
autoread = { config.autoread, 'boolean' },
autowrite = { config.autowrite, 'boolean' },
directory = { config.directory, 'string' },
file = { config.file, 'string' },
force = { config.force, 'table' },
hooks = { config.hooks, 'table' },
verbose = { config.verbose, 'table' },
})
vim.validate({
['force.read'] = { config.force.read, 'boolean' },
['force.write'] = { config.force.write, 'boolean' },
['force.delete'] = { config.force.delete, 'boolean' },
['hooks.pre'] = { config.hooks.pre, 'table' },
['hooks.post'] = { config.hooks.post, 'table' },
['verbose.read'] = { config.verbose.read, 'boolean' },
['verbose.write'] = { config.verbose.write, 'boolean' },
['verbose.delete'] = { config.verbose.delete, 'boolean' },
})
vim.validate({
['hooks.pre.read'] = { config.hooks.pre.read, 'function', true },
['hooks.pre.write'] = { config.hooks.pre.write, 'function', true },
['hooks.pre.delete'] = { config.hooks.pre.delete, 'function', true },
['hooks.post.read'] = { config.hooks.post.read, 'function', true },
['hooks.post.write'] = { config.hooks.post.write, 'function', true },
['hooks.post.delete'] = { config.hooks.post.delete, 'function', true },
})
return config
end
H.apply_config = function(config)
MiniSessions.config = config
H.detect_sessions(config)
end
H.create_autocommands = function(config)
local augroup = vim.api.nvim_create_augroup('MiniSessions', {})
if config.autoread then
local autoread = function()
if not H.is_something_shown() then MiniSessions.read() end
end
local opts = { group = augroup, nested = true, once = true, callback = autoread, desc = 'Autoread latest session' }
vim.api.nvim_create_autocmd('VimEnter', opts)
end
if config.autowrite then
local autowrite = function()
if vim.v.this_session ~= '' then MiniSessions.write(nil, { force = true }) end
end
vim.api.nvim_create_autocmd(
'VimLeavePre',
{ group = augroup, callback = autowrite, desc = 'Autowrite current session' }
)
end
end
H.is_disabled = function() return vim.g.minisessions_disable == true or vim.b.minisessions_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniSessions.config, vim.b.minisessions_config or {}, config or {})
end
-- Work with sessions ---------------------------------------------------------
H.detect_sessions = function(config)
config = H.get_config(config)
local res_global = config.directory == '' and {} or H.detect_sessions_global(config.directory)
local res_local = config.file == '' and {} or H.detect_sessions_local(config.file)
-- If there are both local and global session with same name, prefer local
MiniSessions.detected = vim.tbl_deep_extend('force', res_global, res_local)
end
H.detect_sessions_global = function(global_dir)
-- Ensure correct directory path: create if doesn't exist
global_dir = H.full_path(global_dir)
if vim.fn.isdirectory(global_dir) ~= 1 then
local ok, _ = pcall(vim.fn.mkdir, global_dir, 'p')
if not ok then
H.message(('%s is not a directory path.'):format(vim.inspect(global_dir)))
return {}
end
end
-- Find global sessions
local globs = vim.fn.globpath(global_dir, '*')
if #globs == 0 then return {} end
local res = {}
for _, f in pairs(vim.split(globs, '\n')) do
if H.is_readable_file(f) then
local s = H.new_session(f, 'global')
res[s.name] = s
end
end
return res
end
H.detect_sessions_local = function(local_file)
local f = H.join_path(vim.fn.getcwd(), local_file)
if not H.is_readable_file(f) then return {} end
local res = {}
local s = H.new_session(f, 'local')
res[s.name] = s
return res
end
H.new_session = function(session_path, session_type)
return {
modify_time = vim.fn.getftime(session_path),
name = vim.fn.fnamemodify(session_path, ':t'),
path = H.full_path(session_path),
type = session_type or H.get_session_type(session_path),
}
end
H.get_session_type = function(session_path)
if MiniSessions.config.directory == '' then return 'local' end
local session_dir = H.full_path(session_path)
local global_dir = H.full_path(MiniSessions.config.directory)
return session_dir == global_dir and 'global' or 'local'
end
H.validate_detected = function(session_name)
local is_detected = vim.tbl_contains(vim.tbl_keys(MiniSessions.detected), session_name)
if is_detected then return true end
H.error(('%s is not a name for detected session.'):format(vim.inspect(session_name)))
end
H.get_unsaved_listed_buffers = function()
return vim.tbl_filter(
function(buf_id) return vim.bo[buf_id].modified and vim.bo[buf_id].buflisted end,
vim.api.nvim_list_bufs()
)
end
H.get_current_session_name = function() return vim.fn.fnamemodify(vim.v.this_session, ':t') end
H.name_to_path = function(session_name)
if session_name == nil then
if vim.v.this_session == '' then H.error('There is no active session. Supply non-nil session name.') end
return vim.v.this_session
end
session_name = tostring(session_name)
if session_name == '' then H.error('Supply non-empty session name.') end
local session_dir = (session_name == MiniSessions.config.file) and vim.fn.getcwd() or MiniSessions.config.directory
local path = H.join_path(session_dir, session_name)
return H.full_path(path)
end
-- Utilities ------------------------------------------------------------------
H.echo = function(msg, is_important)
-- Construct message chunks
msg = type(msg) == 'string' and { { msg } } or msg
table.insert(msg, 1, { '(mini.sessions) ', 'WarningMsg' })
-- Avoid hit-enter-prompt
local max_width = vim.o.columns * math.max(vim.o.cmdheight - 1, 0) + vim.v.echospace
local chunks, tot_width = {}, 0
for _, ch in ipairs(msg) do
local new_ch = { vim.fn.strcharpart(ch[1], 0, max_width - tot_width), ch[2] }
table.insert(chunks, new_ch)
tot_width = tot_width + vim.fn.strdisplaywidth(new_ch[1])
if tot_width >= max_width then break end
end
-- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
vim.cmd([[echo '' | redraw]])
vim.api.nvim_echo(chunks, is_important, {})
end
H.message = function(msg) H.echo(msg, true) end
H.error = function(msg) error(('(mini.sessions) %s'):format(msg)) end
H.default_opts = function(action)
local config = MiniSessions.config
return {
force = config.force[action],
verbose = config.verbose[action],
hooks = { pre = config.hooks.pre[action], post = config.hooks.post[action] },
}
end
H.is_readable_file = function(path) return vim.fn.isdirectory(path) ~= 1 and vim.fn.getfperm(path):sub(1, 1) == 'r' end
H.join_path = function(directory, filename)
return (string.format('%s/%s', directory, filename):gsub('\\', '/'):gsub('/+', '/'))
end
H.full_path = function(path) return vim.fn.resolve(vim.fn.fnamemodify(path, ':p')) end
H.is_something_shown = function()
-- Don't autoread session if Neovim is opened to show something. That is
-- when at least one of the following is true:
-- - There are files in arguments (like `nvim foo.txt` with new file).
if vim.fn.argc() > 0 then return true end
-- - Several buffers are listed (like session with placeholder buffers). That
-- means unlisted buffers (like from `nvim-tree`) don't affect decision.
local listed_buffers = vim.tbl_filter(
function(buf_id) return vim.fn.buflisted(buf_id) == 1 end,
vim.api.nvim_list_bufs()
)
if #listed_buffers > 1 then return true end
-- - Current buffer is meant to show something else
if vim.bo.filetype ~= '' then return true end
-- - Current buffer has any lines (something opened explicitly).
-- NOTE: Usage of `line2byte(line('$') + 1) < 0` seemed to be fine, but it
-- doesn't work if some automated changed was made to buffer while leaving it
-- empty (returns 2 instead of -1). This was also the reason of not being
-- able to test with child Neovim process from 'tests/helpers'.
local n_lines = vim.api.nvim_buf_line_count(0)
if n_lines > 1 then return true end
local first_line = vim.api.nvim_buf_get_lines(0, 0, 1, true)[1]
if string.len(first_line) > 0 then return true end
return false
end
H.possibly_execute = function(f, ...)
if f == nil then return end
return f(...)
end
return MiniSessions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,695 @@
--- *mini.statusline* Statusline
--- *MiniStatusline*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Define own custom statusline structure for active and inactive windows.
--- This is done with a function which should return string appropriate for
--- |statusline|. Its code should be similar to default one with structure:
--- - Compute string data for every section you want to be displayed.
--- - Combine them in groups with |MiniStatusline.combine_groups()|.
---
--- - Built-in active mode indicator with colors.
---
--- - Sections can hide information when window is too narrow (specific window
--- width is configurable per section).
---
--- # Dependencies ~
---
--- Suggested dependencies (provide extra functionality, will work without them):
---
--- - Nerd font (to support extra icons).
---
--- - Enabled |MiniIcons| module for |MiniStatusline.section_fileinfo()|.
--- Falls back to using 'nvim-tree/nvim-web-devicons' plugin or shows nothing.
---
--- - Enabled |MiniGit| module for |MiniStatusline.section_git()|.
--- Falls back to using 'lewis6991/gitsigns.nvim' plugin or shows nothing.
---
--- - Enabled |MiniDiff| module for |MiniStatusline.section_diff()|.
--- Falls back to using 'lewis6991/gitsigns.nvim' plugin or shows nothing.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.statusline').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniStatusline` which you can use for scripting or manually (with
--- `:lua MiniStatusline.*`).
---
--- See |MiniStatusline.config| for `config` structure and default values. For
--- some content examples, see |MiniStatusline-example-content|.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.ministatusline_config` which should have same structure as
--- `MiniStatusline.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Highlight groups ~
---
--- Highlight depending on mode (second output from |MiniStatusline.section_mode|):
--- * `MiniStatuslineModeNormal` - Normal mode.
--- * `MiniStatuslineModeInsert` - Insert mode.
--- * `MiniStatuslineModeVisual` - Visual mode.
--- * `MiniStatuslineModeReplace` - Replace mode.
--- * `MiniStatuslineModeCommand` - Command mode.
--- * `MiniStatuslineModeOther` - other modes (like Terminal, etc.).
---
--- Highlight used in default statusline:
--- * `MiniStatuslineDevinfo` - for "dev info" group
--- (|MiniStatusline.section_git| and |MiniStatusline.section_diagnostics|).
--- * `MiniStatuslineFilename` - for |MiniStatusline.section_filename| section.
--- * `MiniStatuslineFileinfo` - for |MiniStatusline.section_fileinfo| section.
---
--- Other groups:
--- * `MiniStatuslineInactive` - highliting in not focused window.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable (show empty statusline), set `vim.g.ministatusline_disable`
--- (globally) or `vim.b.ministatusline_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.
--- Example content
---
--- # Default content ~
---
--- This function is used as default value for active content: >lua
---
--- function()
--- local mode, mode_hl = MiniStatusline.section_mode({ trunc_width = 120 })
--- local git = MiniStatusline.section_git({ trunc_width = 40 })
--- local diff = MiniStatusline.section_diff({ trunc_width = 75 })
--- local diagnostics = MiniStatusline.section_diagnostics({ trunc_width = 75 })
--- local lsp = MiniStatusline.section_lsp({ trunc_width = 75 })
--- local filename = MiniStatusline.section_filename({ trunc_width = 140 })
--- local fileinfo = MiniStatusline.section_fileinfo({ trunc_width = 120 })
--- local location = MiniStatusline.section_location({ trunc_width = 75 })
--- local search = MiniStatusline.section_searchcount({ trunc_width = 75 })
---
--- return MiniStatusline.combine_groups({
--- { hl = mode_hl, strings = { mode } },
--- { hl = 'MiniStatuslineDevinfo', strings = { git, diff, diagnostics, lsp } },
--- '%<', -- Mark general truncate point
--- { hl = 'MiniStatuslineFilename', strings = { filename } },
--- '%=', -- End left alignment
--- { hl = 'MiniStatuslineFileinfo', strings = { fileinfo } },
--- { hl = mode_hl, strings = { search, location } },
--- })
--- end
--- <
--- # Show boolean options ~
---
--- To compute section string for boolean option use variation of this code
--- snippet inside content function (you can modify option itself, truncation
--- width, short and long displayed names): >lua
---
--- local spell = vim.wo.spell and (MiniStatusline.is_truncated(120) and 'S' or 'SPELL') or ''
--- <
--- Here `x and y or z` is a common Lua way of doing ternary operator: if `x`
--- is `true`-ish then return `y`, if not - return `z`.
---@tag MiniStatusline-example-content
---@alias __statusline_args table Section arguments.
---@alias __statusline_section string Section string.
-- Module definition ==========================================================
local MiniStatusline = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniStatusline.config|.
---
---@usage >lua
--- require('mini.statusline').setup() -- use default config
--- -- OR
--- require('mini.statusline').setup({}) -- replace {} with your config table
--- <
MiniStatusline.setup = function(config)
-- Export module
_G.MiniStatusline = MiniStatusline
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
-- - Disable built-in statusline in Quickfix window
vim.g.qf_disable_statusline = 1
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniStatusline.config = {
-- Content of statusline as functions which return statusline string. See
-- `:h statusline` and code of default contents (used instead of `nil`).
content = {
-- Content for active window
active = nil,
-- Content for inactive window(s)
inactive = nil,
},
-- Whether to use icons by default
use_icons = true,
-- Whether to set Vim's settings for statusline (make it always shown with
-- 'laststatus' set to 2).
-- To use global statusline, set this to `false` and 'laststatus' to 3.
set_vim_settings = true,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Compute content for active window
MiniStatusline.active = function()
if H.is_disabled() then return '' end
return (H.get_config().content.active or H.default_content_active)()
end
--- Compute content for inactive window
MiniStatusline.inactive = function()
if H.is_disabled() then return '' end
return (H.get_config().content.inactive or H.default_content_inactive)()
end
--- Combine groups of sections
---
--- Each group can be either a string or a table with fields `hl` (group's
--- highlight group) and `strings` (strings representing sections).
---
--- General idea of this function is as follows;
--- - String group is used as is (useful for special strings like `%<` or `%=`).
--- - Each table group has own highlighting in `hl` field (if missing, the
--- previous one is used) and string parts in `strings` field. Non-empty
--- strings from `strings` are separated by one space. Non-empty groups are
--- separated by two spaces (one for each highlighting).
---
---@param groups table Array of groups.
---
---@return string String suitable for 'statusline'.
MiniStatusline.combine_groups = function(groups)
local parts = vim.tbl_map(function(s)
if type(s) == 'string' then return s end
if type(s) ~= 'table' then return '' end
local string_arr = vim.tbl_filter(function(x) return type(x) == 'string' and x ~= '' end, s.strings or {})
local str = table.concat(string_arr, ' ')
-- Use previous highlight group
if s.hl == nil then return ' ' .. str .. ' ' end
-- Allow using this highlight group later
if str:len() == 0 then return '%#' .. s.hl .. '#' end
return string.format('%%#%s# %s ', s.hl, str)
end, groups)
return table.concat(parts, '')
end
--- Decide whether to truncate
---
--- This basically computes window width and compares it to `trunc_width`: if
--- window is smaller then truncate; otherwise don't. Don't truncate by
--- default.
---
--- Use this to manually decide if section needs truncation or not.
---
---@param trunc_width number|nil Truncation width. If `nil`, output is `false`.
---
---@return boolean Whether to truncate.
MiniStatusline.is_truncated = function(trunc_width)
-- Use -1 to default to 'not truncated'
local cur_width = vim.o.laststatus == 3 and vim.o.columns or vim.api.nvim_win_get_width(0)
return cur_width < (trunc_width or -1)
end
-- Sections ===================================================================
-- Functions should return output text without whitespace on sides.
-- Return empty string to omit section.
--- Section for Vim |mode()|
---
--- Short output is returned if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args
---
---@return ... Section string and mode's highlight group.
MiniStatusline.section_mode = function(args)
local mode_info = H.modes[vim.fn.mode()]
local mode = MiniStatusline.is_truncated(args.trunc_width) and mode_info.short or mode_info.long
return mode, mode_info.hl
end
--- Section for Git information
---
--- Shows Git summary from |MiniGit| (should be set up; recommended). To tweak
--- formatting of what data is shown, modify buffer-local summary string directly
--- as described in |MiniGit-examples|.
---
--- If 'mini.git' is not set up, section falls back on 'lewis6991/gitsigns' data
--- or showing empty string.
---
--- Empty string is returned if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args Use `args.icon` to supply your own icon.
---
---@return __statusline_section
MiniStatusline.section_git = function(args)
if MiniStatusline.is_truncated(args.trunc_width) then return '' end
local summary = vim.b.minigit_summary_string or vim.b.gitsigns_head
if summary == nil then return '' end
local use_icons = H.use_icons or H.get_config().use_icons
local icon = args.icon or (use_icons and '' or 'Git')
return icon .. ' ' .. (summary == '' and '-' or summary)
end
--- Section for diff information
---
--- Shows diff summary from |MiniDiff| (should be set up; recommended). To tweak
--- formatting of what data is shown, modify buffer-local summary string directly
--- as described in |MiniDiff-diff-summary|.
---
--- If 'mini.diff' is not set up, section falls back on 'lewis6991/gitsigns' data
--- or showing empty string.
---
--- Empty string is returned if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args Use `args.icon` to supply your own icon.
---
---@return __statusline_section
MiniStatusline.section_diff = function(args)
if MiniStatusline.is_truncated(args.trunc_width) then return '' end
local summary = vim.b.minidiff_summary_string or vim.b.gitsigns_status
if summary == nil then return '' end
local use_icons = H.use_icons or H.get_config().use_icons
local icon = args.icon or (use_icons and '' or 'Diff')
return icon .. ' ' .. (summary == '' and '-' or summary)
end
--- Section for Neovim's builtin diagnostics
---
--- Shows nothing if diagnostics is disabled, no diagnostic is set, or for short
--- output. Otherwise uses |vim.diagnostic.get()| to compute and show number of
--- errors ('E'), warnings ('W'), information ('I'), and hints ('H').
---
--- Short output is returned if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args Use `args.icon` to supply your own icon.
--- Use `args.signs` to use custom signs per severity level name. For example: >lua
---
--- { ERROR = '!', WARN = '?', INFO = '@', HINT = '*' }
--- <
---@return __statusline_section
MiniStatusline.section_diagnostics = function(args)
if MiniStatusline.is_truncated(args.trunc_width) or H.diagnostic_is_disabled() then return '' end
-- Construct string parts
local count = H.diagnostic_get_count()
local severity, signs, t = vim.diagnostic.severity, args.signs or {}, {}
for _, level in ipairs(H.diagnostic_levels) do
local n = count[severity[level.name]] or 0
-- Add level info only if diagnostic is present
if n > 0 then table.insert(t, ' ' .. (signs[level.name] or level.sign) .. n) end
end
if #t == 0 then return '' end
local use_icons = H.use_icons or H.get_config().use_icons
local icon = args.icon or (use_icons and '' or 'Diag')
return icon .. table.concat(t, '')
end
--- Section for attached LSP servers
---
--- Shows number of LSP servers (each as separate "+" character) attached to
--- current buffer or nothing if none is attached.
--- Nothing is shown if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args Use `args.icon` to supply your own icon.
---
---@return __statusline_section
MiniStatusline.section_lsp = function(args)
if MiniStatusline.is_truncated(args.trunc_width) then return '' end
local attached = H.get_attached_lsp()
if attached == '' then return '' end
local use_icons = H.use_icons or H.get_config().use_icons
local icon = args.icon or (use_icons and '󰰎' or 'LSP')
return icon .. ' ' .. attached
end
--- Section for file name
---
--- Show full file name or relative in short output.
---
--- Short output is returned if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args
---
---@return __statusline_section
MiniStatusline.section_filename = function(args)
-- In terminal always use plain name
if vim.bo.buftype == 'terminal' then
return '%t'
elseif MiniStatusline.is_truncated(args.trunc_width) then
-- File name with 'truncate', 'modified', 'readonly' flags
-- Use relative path if truncated
return '%f%m%r'
else
-- Use fullpath if not truncated
return '%F%m%r'
end
end
--- Section for file information
---
--- Short output contains only buffer's 'filetype' and is returned if window
--- width is lower than `args.trunc_width` or buffer is not normal.
---
--- Nothing is shown if there is no 'filetype' set (treated as temporary buffer).
---
--- If `config.use_icons` is true and icon provider is present (see
--- "Dependencies" section in |mini.statusline|), shows icon near the filetype.
---
---@param args __statusline_args
---
---@return __statusline_section
MiniStatusline.section_fileinfo = function(args)
local filetype = vim.bo.filetype
-- Don't show anything if there is no filetype
if filetype == '' then return '' end
-- Add filetype icon
H.ensure_get_icon()
if H.get_icon ~= nil then filetype = H.get_icon(filetype) .. ' ' .. filetype end
-- Construct output string if truncated or buffer is not normal
if MiniStatusline.is_truncated(args.trunc_width) or vim.bo.buftype ~= '' then return filetype end
-- Construct output string with extra file info
local encoding = vim.bo.fileencoding or vim.bo.encoding
local format = vim.bo.fileformat
local size = H.get_filesize()
return string.format('%s %s[%s] %s', filetype, encoding, format, size)
end
--- Section for location inside buffer
---
--- Show location inside buffer in the form:
--- - Normal: `'<cursor line>|<total lines>│<cursor column>|<total columns>'`
--- - Short: `'<cursor line>│<cursor column>'`
---
--- Short output is returned if window width is lower than `args.trunc_width`.
---
---@param args __statusline_args
---
---@return __statusline_section
MiniStatusline.section_location = function(args)
-- Use virtual column number to allow update when past last column
if MiniStatusline.is_truncated(args.trunc_width) then return '%l│%2v' end
-- Use `virtcol()` to correctly handle multi-byte characters
return '%l|%L│%2v|%-2{virtcol("$") - 1}'
end
--- Section for current search count
---
--- Show the current status of |searchcount()|. Empty output is returned if
--- window width is lower than `args.trunc_width`, search highlighting is not
--- on (see |v:hlsearch|), or if number of search result is 0.
---
--- `args.options` is forwarded to |searchcount()|. By default it recomputes
--- data on every call which can be computationally expensive (although still
--- usually on 0.1 ms order of magnitude). To prevent this, supply
--- `args.options = { recompute = false }`.
---
---@param args __statusline_args
---
---@return __statusline_section
MiniStatusline.section_searchcount = function(args)
if vim.v.hlsearch == 0 or MiniStatusline.is_truncated(args.trunc_width) then return '' end
-- `searchcount()` can return errors because it is evaluated very often in
-- statusline. For example, when typing `/` followed by `\(`, it gives E54.
local ok, s_count = pcall(vim.fn.searchcount, (args or {}).options or { recompute = true })
if not ok or s_count.current == nil or s_count.total == 0 then return '' end
if s_count.incomplete == 1 then return '?/?' end
local too_many = '>' .. s_count.maxcount
local current = s_count.current > s_count.maxcount and too_many or s_count.current
local total = s_count.total > s_count.maxcount and too_many or s_count.total
return current .. '/' .. total
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniStatusline.config)
-- Showed diagnostic levels
H.diagnostic_levels = {
{ name = 'ERROR', sign = 'E' },
{ name = 'WARN', sign = 'W' },
{ name = 'INFO', sign = 'I' },
{ name = 'HINT', sign = 'H' },
}
-- String representation of attached LSP clients per buffer id
H.attached_lsp = {}
-- 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({
content = { config.content, 'table' },
set_vim_settings = { config.set_vim_settings, 'boolean' },
use_icons = { config.use_icons, 'boolean' },
})
vim.validate({
['content.active'] = { config.content.active, 'function', true },
['content.inactive'] = { config.content.inactive, 'function', true },
})
return config
end
H.apply_config = function(config)
MiniStatusline.config = config
-- Set settings to ensure statusline is displayed properly
if config.set_vim_settings then vim.o.laststatus = 2 end
-- Ensure proper 'statusline' values (to not rely on autocommands trigger)
H.ensure_content()
-- Set global value to reduce flickering when first time entering buffer, as
-- it is used by default before content is ensured on next loop
vim.go.statusline = '%{%v:lua.MiniStatusline.active()%}'
end
H.create_autocommands = function()
local augroup = vim.api.nvim_create_augroup('MiniStatusline', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
au({ 'WinEnter', 'BufWinEnter' }, '*', H.ensure_content, 'Ensure statusline content')
-- Use `schedule_wrap()` because at `LspDetach` server is still present
local track_lsp = vim.schedule_wrap(function(data)
H.attached_lsp[data.buf] = H.compute_attached_lsp(data.buf)
vim.cmd('redrawstatus')
end)
au({ 'LspAttach', 'LspDetach' }, '*', track_lsp, 'Track LSP clients')
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
set_default_hl('MiniStatuslineModeNormal', { link = 'Cursor' })
set_default_hl('MiniStatuslineModeInsert', { link = 'DiffChange' })
set_default_hl('MiniStatuslineModeVisual', { link = 'DiffAdd' })
set_default_hl('MiniStatuslineModeReplace', { link = 'DiffDelete' })
set_default_hl('MiniStatuslineModeCommand', { link = 'DiffText' })
set_default_hl('MiniStatuslineModeOther', { link = 'IncSearch' })
set_default_hl('MiniStatuslineDevinfo', { link = 'StatusLine' })
set_default_hl('MiniStatuslineFilename', { link = 'StatusLineNC' })
set_default_hl('MiniStatuslineFileinfo', { link = 'StatusLine' })
set_default_hl('MiniStatuslineInactive', { link = 'StatusLineNC' })
end
H.is_disabled = function() return vim.g.ministatusline_disable == true or vim.b.ministatusline_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniStatusline.config, vim.b.ministatusline_config or {}, config or {})
end
-- Content --------------------------------------------------------------------
H.ensure_content = vim.schedule_wrap(function()
-- NOTE: Use `schedule_wrap()` to properly work inside autocommands because
-- they might temporarily change current window
local cur_win_id, is_global_stl = vim.api.nvim_get_current_win(), vim.o.laststatus == 3
for _, win_id in ipairs(vim.api.nvim_list_wins()) do
vim.wo[win_id].statusline = (win_id == cur_win_id or is_global_stl) and '%{%v:lua.MiniStatusline.active()%}'
or '%{%v:lua.MiniStatusline.inactive()%}'
end
end)
-- Mode -----------------------------------------------------------------------
-- Custom `^V` and `^S` symbols to make this file appropriate for copy-paste
-- (otherwise those symbols are not displayed).
local CTRL_S = vim.api.nvim_replace_termcodes('<C-S>', true, true, true)
local CTRL_V = vim.api.nvim_replace_termcodes('<C-V>', true, true, true)
-- stylua: ignore start
H.modes = setmetatable({
['n'] = { long = 'Normal', short = 'N', hl = 'MiniStatuslineModeNormal' },
['v'] = { long = 'Visual', short = 'V', hl = 'MiniStatuslineModeVisual' },
['V'] = { long = 'V-Line', short = 'V-L', hl = 'MiniStatuslineModeVisual' },
[CTRL_V] = { long = 'V-Block', short = 'V-B', hl = 'MiniStatuslineModeVisual' },
['s'] = { long = 'Select', short = 'S', hl = 'MiniStatuslineModeVisual' },
['S'] = { long = 'S-Line', short = 'S-L', hl = 'MiniStatuslineModeVisual' },
[CTRL_S] = { long = 'S-Block', short = 'S-B', hl = 'MiniStatuslineModeVisual' },
['i'] = { long = 'Insert', short = 'I', hl = 'MiniStatuslineModeInsert' },
['R'] = { long = 'Replace', short = 'R', hl = 'MiniStatuslineModeReplace' },
['c'] = { long = 'Command', short = 'C', hl = 'MiniStatuslineModeCommand' },
['r'] = { long = 'Prompt', short = 'P', hl = 'MiniStatuslineModeOther' },
['!'] = { long = 'Shell', short = 'Sh', hl = 'MiniStatuslineModeOther' },
['t'] = { long = 'Terminal', short = 'T', hl = 'MiniStatuslineModeOther' },
}, {
-- By default return 'Unknown' but this shouldn't be needed
__index = function()
return { long = 'Unknown', short = 'U', hl = '%#MiniStatuslineModeOther#' }
end,
})
-- stylua: ignore end
-- Default content ------------------------------------------------------------
--stylua: ignore
H.default_content_active = function()
H.use_icons = H.get_config().use_icons
local mode, mode_hl = MiniStatusline.section_mode({ trunc_width = 120 })
local git = MiniStatusline.section_git({ trunc_width = 40 })
local diff = MiniStatusline.section_diff({ trunc_width = 75 })
local diagnostics = MiniStatusline.section_diagnostics({ trunc_width = 75 })
local lsp = MiniStatusline.section_lsp({ trunc_width = 75 })
local filename = MiniStatusline.section_filename({ trunc_width = 140 })
local fileinfo = MiniStatusline.section_fileinfo({ trunc_width = 120 })
local location = MiniStatusline.section_location({ trunc_width = 75 })
local search = MiniStatusline.section_searchcount({ trunc_width = 75 })
H.use_icons = nil
-- Usage of `MiniStatusline.combine_groups()` ensures highlighting and
-- correct padding with spaces between groups (accounts for 'missing'
-- sections, etc.)
return MiniStatusline.combine_groups({
{ hl = mode_hl, strings = { mode } },
{ hl = 'MiniStatuslineDevinfo', strings = { git, diff, diagnostics, lsp } },
'%<', -- Mark general truncate point
{ hl = 'MiniStatuslineFilename', strings = { filename } },
'%=', -- End left alignment
{ hl = 'MiniStatuslineFileinfo', strings = { fileinfo } },
{ hl = mode_hl, strings = { search, location } },
})
end
H.default_content_inactive = function() return '%#MiniStatuslineInactive#%F%=' end
-- LSP ------------------------------------------------------------------------
H.get_attached_lsp = function() return H.attached_lsp[vim.api.nvim_get_current_buf()] or '' end
H.compute_attached_lsp = function(buf_id) return string.rep('+', vim.tbl_count(H.get_buf_lsp_clients(buf_id))) end
H.get_buf_lsp_clients = function(buf_id) return vim.lsp.get_clients({ bufnr = buf_id }) end
if vim.fn.has('nvim-0.10') == 0 then
H.get_buf_lsp_clients = function(buf_id) return vim.lsp.buf_get_clients(buf_id) end
end
-- Diagnostics ----------------------------------------------------------------
H.diagnostic_get_count = function()
local res = {}
for _, d in ipairs(vim.diagnostic.get(0)) do
res[d.severity] = (res[d.severity] or 0) + 1
end
return res
end
if vim.fn.has('nvim-0.10') == 1 then H.diagnostic_get_count = function() return vim.diagnostic.count(0) end end
if vim.fn.has('nvim-0.10') == 1 then
H.diagnostic_is_disabled = function(_) return not vim.diagnostic.is_enabled({ bufnr = 0 }) end
elseif vim.fn.has('nvim-0.9') == 1 then
H.diagnostic_is_disabled = function(_) return vim.diagnostic.is_disabled(0) end
else
H.diagnostic_is_disabled = function(_) return false end
end
-- Utilities ------------------------------------------------------------------
H.get_filesize = function()
local size = vim.fn.getfsize(vim.fn.getreg('%'))
if size < 1024 then
return string.format('%dB', size)
elseif size < 1048576 then
return string.format('%.2fKiB', size / 1024)
else
return string.format('%.2fMiB', size / 1048576)
end
end
H.ensure_get_icon = function()
if not (H.use_icons or H.get_config().use_icons) then
-- Show no icon
H.get_icon = nil
elseif H.get_icon ~= nil then
-- Cache only once
return
elseif _G.MiniIcons ~= nil then
-- Prefer 'mini.icons'
H.get_icon = function(filetype) return (_G.MiniIcons.get('filetype', filetype)) end
else
-- Try falling back to 'nvim-web-devicons'
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
if not has_devicons then return end
H.get_icon = function() return (devicons.get_icon(vim.fn.expand('%:t'), nil, { default = true })) end
end
end
return MiniStatusline

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,550 @@
--- *mini.tabline* Tabline
--- *MiniTabline*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Key idea: show all listed buffers in readable way with minimal total width.
--- Also allow showing extra information section in case of multiple vim tabpages.
---
--- Features:
--- - Buffers are listed in the order of their identifier (see |bufnr()|).
---
--- - Different highlight groups for "states" of buffer affecting 'buffer tabs'.
---
--- - Buffer names are made unique by extending paths to files or appending
--- unique identifier to buffers without name.
---
--- - Current buffer is displayed "optimally centered" (in center of screen
--- while maximizing the total number of buffers shown) when there are many
--- buffers open.
---
--- - 'Buffer tabs' are clickable if Neovim allows it.
---
--- What it doesn't do:
--- - Custom buffer order is not supported.
---
--- # Dependencies ~
---
--- Suggested dependencies (provide extra functionality, will work without them):
---
--- - Enabled |MiniIcons| module to show icons near file names.
--- Falls back to using 'nvim-tree/nvim-web-devicons' plugin or shows nothing.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.tabline').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniTabline` which you can use for scripting or manually (with
--- `:lua MiniTabline.*`).
---
--- See |MiniTabline.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minitabline_config` which should have same structure as
--- `MiniTabline.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Highlight groups ~
---
--- * `MiniTablineCurrent` - buffer is current (has cursor in it).
--- * `MiniTablineVisible` - buffer is visible (displayed in some window).
--- * `MiniTablineHidden` - buffer is hidden (not displayed).
--- * `MiniTablineModifiedCurrent` - buffer is modified and current.
--- * `MiniTablineModifiedVisible` - buffer is modified and visible.
--- * `MiniTablineModifiedHidden` - buffer is modified and hidden.
--- * `MiniTablineFill` - unused right space of tabline.
--- * `MiniTablineTabpagesection` - section with tabpage information.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable (show empty tabline), set `vim.g.minitabline_disable` (globally) or
--- `vim.b.minitabline_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 MiniTabline = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniTabline.config|.
---
---@usage >lua
--- require('mini.tabline').setup() -- use default config
--- -- OR
--- require('mini.tabline').setup({}) -- replace {} with your config table
--- <
MiniTabline.setup = function(config)
-- Export module
_G.MiniTabline = MiniTabline
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Function to make tabs clickable
vim.api.nvim_exec(
[[function! MiniTablineSwitchBuffer(buf_id, clicks, button, mod)
execute 'buffer' a:buf_id
endfunction]],
false
)
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Format ~
---
--- `config.format` is a callable that takes buffer identifier and pre-computed
--- label as arguments and returns a string with formatted label.
--- This function will be called for all displayable in tabline buffers.
--- Default: |MiniTabline.default_format()|.
---
--- Example of adding "+" suffix for modified buffers: >lua
---
--- function(buf_id, label)
--- local suffix = vim.bo[buf_id].modified and '+ ' or ''
--- return MiniTabline.default_format(buf_id, label) .. suffix
--- end
--- <
MiniTabline.config = {
-- Whether to show file icons (requires 'mini.icons')
show_icons = true,
-- Function which formats the tab label
-- By default surrounds with space and possibly prepends with icon
format = nil,
-- Whether to set Vim's settings for tabline (make it always shown and
-- allow hidden buffers)
set_vim_settings = true,
-- Where to show tabpage section in case of multiple vim tabpages.
-- One of 'left', 'right', 'none'.
tabpage_section = 'left',
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Make string for |tabline|
MiniTabline.make_tabline_string = function()
if H.is_disabled() then return '' end
H.make_tabpage_section()
H.list_tabs()
H.finalize_labels()
H.fit_width()
return H.concat_tabs()
end
--- Default tab format
---
--- Used by default as `config.format`.
--- Prepends label with padded icon based on buffer's name (if `show_icon`
--- in |MiniTabline.config| is `true`) and surrounds label with single space.
--- Note: it is meant to be used only as part of `format` in |MiniTabline.config|.
---
---@param buf_id number Buffer identifier.
---@param label string Pre-computed label.
---
---@return string Formatted label.
MiniTabline.default_format = function(buf_id, label)
if H.get_icon == nil then return string.format(' %s ', label) end
return string.format(' %s %s ', H.get_icon(vim.api.nvim_buf_get_name(buf_id)), label)
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniTabline.config)
-- Table to keep track of tabs
H.tabs = {}
-- Indicator of whether there is clickable support
H.tablineat = vim.fn.has('tablineat')
-- Keep track of initially unnamed buffers
H.unnamed_buffers_seq_ids = {}
-- Separator of file path
H.path_sep = package.config:sub(1, 1)
-- String with tabpage prefix
H.tabpage_section = ''
-- Buffer number of center buffer
H.center_buf_id = nil
-- 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({
show_icons = { config.show_icons, 'boolean' },
format = { config.format, 'function', true },
set_vim_settings = { config.set_vim_settings, 'boolean' },
tabpage_section = { config.tabpage_section, 'string' },
})
return config
end
H.apply_config = function(config)
MiniTabline.config = config
-- Set settings to ensure tabline is displayed properly
if config.set_vim_settings then
vim.o.showtabline = 2 -- Always show tabline
vim.o.hidden = true -- Allow switching buffers without saving them
end
-- Set tabline string
vim.o.tabline = '%!v:lua.MiniTabline.make_tabline_string()'
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
set_default_hl('MiniTablineCurrent', { link = 'TabLineSel' })
set_default_hl('MiniTablineVisible', { link = 'TabLineSel' })
set_default_hl('MiniTablineHidden', { link = 'TabLine' })
set_default_hl('MiniTablineModifiedCurrent', { link = 'StatusLine' })
set_default_hl('MiniTablineModifiedVisible', { link = 'StatusLine' })
set_default_hl('MiniTablineModifiedHidden', { link = 'StatusLineNC' })
set_default_hl('MiniTablineTabpagesection', { link = 'Search' })
set_default_hl('MiniTablineFill', { link = 'Normal' })
end
H.is_disabled = function() return vim.g.minitabline_disable == true or vim.b.minitabline_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniTabline.config, vim.b.minitabline_config or {}, config or {})
end
-- Work with tabpages ---------------------------------------------------------
H.make_tabpage_section = function()
local n_tabpages = vim.fn.tabpagenr('$')
if n_tabpages == 1 or H.get_config().tabpage_section == 'none' then
H.tabpage_section = ''
return
end
local cur_tabpagenr = vim.fn.tabpagenr()
H.tabpage_section = (' Tab %s/%s '):format(cur_tabpagenr, n_tabpages)
end
-- Work with tabs -------------------------------------------------------------
-- List tabs
H.list_tabs = function()
local tabs = {}
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
if H.is_buffer_in_minitabline(buf_id) then
local tab = { buf_id = buf_id }
tab['hl'] = H.construct_highlight(buf_id)
tab['tabfunc'] = H.construct_tabfunc(buf_id)
tab['label'], tab['label_extender'] = H.construct_label_data(buf_id)
table.insert(tabs, tab)
end
end
H.tabs = tabs
end
H.is_buffer_in_minitabline = function(buf_id) return vim.bo[buf_id].buflisted end
-- Tab's highlight group
H.construct_highlight = function(buf_id)
local hl_type
if buf_id == vim.api.nvim_get_current_buf() then
hl_type = 'Current'
elseif vim.fn.bufwinnr(buf_id) > 0 then
hl_type = 'Visible'
else
hl_type = 'Hidden'
end
if vim.bo[buf_id].modified then hl_type = 'Modified' .. hl_type end
return string.format('%%#MiniTabline%s#', hl_type)
end
-- Tab's clickable action (if supported)
H.construct_tabfunc = function(buf_id)
if H.tablineat > 0 then
return string.format('%%%d@MiniTablineSwitchBuffer@', buf_id)
else
return ''
end
end
-- Tab's label and label extender
H.construct_label_data = function(buf_id)
local label, label_extender
local bufpath = vim.api.nvim_buf_get_name(buf_id)
if bufpath ~= '' then
-- Process path buffer
label = vim.fn.fnamemodify(bufpath, ':t')
label_extender = H.make_path_extender(buf_id)
else
-- Process unnamed buffer
label = H.make_unnamed_label(buf_id)
label_extender = function(x) return x end
end
return label, label_extender
end
H.make_path_extender = function(buf_id)
-- Add parent to current label (if possible)
return function(label)
local full_path = vim.api.nvim_buf_get_name(buf_id)
-- Using `vim.pesc` prevents effect of problematic characters (like '.')
local pattern = string.format('[^%s]+%s%s$', vim.pesc(H.path_sep), vim.pesc(H.path_sep), vim.pesc(label))
return string.match(full_path, pattern) or label
end
end
-- Work with unnamed buffers --------------------------------------------------
-- Unnamed buffers are tracked in `H.unnamed_buffers_seq_ids` for
-- disambiguation. This table is designed to store 'sequential' buffer
-- identifier. This approach allows to have the following behavior:
-- - Create three unnamed buffers.
-- - Delete second one.
-- - Tab label for third one remains the same.
H.make_unnamed_label = function(buf_id)
local label
if vim.bo[buf_id].buftype == 'quickfix' then
-- It would be great to differentiate for buffer `buf_id` between quickfix
-- and location lists but it seems there is no reliable way to do so.
-- The only one is to use `getwininfo(bufwinid(buf_id))` and look for
-- `quickfix` and `loclist` fields, but that fails if buffer `buf_id` is
-- not visible.
label = '*quickfix*'
else
label = H.is_buffer_scratch(buf_id) and '!' or '*'
end
-- Possibly add tracking id
local unnamed_id = H.get_unnamed_id(buf_id)
if unnamed_id > 1 then label = string.format('%s(%d)', label, unnamed_id) end
return label
end
H.is_buffer_scratch = function(buf_id)
local buftype = vim.bo[buf_id].buftype
return (buftype == 'acwrite') or (buftype == 'nofile')
end
H.get_unnamed_id = function(buf_id)
-- Use existing sequential id if possible
local seq_id = H.unnamed_buffers_seq_ids[buf_id]
if seq_id ~= nil then return seq_id end
-- Cache sequential id for currently unnamed buffer `buf_id`
H.unnamed_buffers_seq_ids[buf_id] = vim.tbl_count(H.unnamed_buffers_seq_ids) + 1
return H.unnamed_buffers_seq_ids[buf_id]
end
-- Work with labels -----------------------------------------------------------
H.finalize_labels = function()
-- Deduplicate
local nonunique_tab_ids = H.get_nonunique_tab_ids()
while #nonunique_tab_ids > 0 do
local nothing_changed = true
-- Extend labels
for _, buf_id in ipairs(nonunique_tab_ids) do
local tab = H.tabs[buf_id]
local old_label = tab.label
tab.label = tab.label_extender(tab.label)
if old_label ~= tab.label then nothing_changed = false end
end
if nothing_changed then break end
nonunique_tab_ids = H.get_nonunique_tab_ids()
end
-- Format labels
local config = H.get_config()
-- - Ensure cached `get_icon` for `default_format` (for better performance)
H.ensure_get_icon(config)
-- - Apply formatting
local format = config.format or MiniTabline.default_format
for _, tab in pairs(H.tabs) do
tab.label = format(tab.buf_id, tab.label)
end
end
---@return table Array of `H.tabs` ids which have non-unique labels.
---@private
H.get_nonunique_tab_ids = function()
-- Collect tab-array-id per label
local label_tab_ids = {}
for i, tab in ipairs(H.tabs) do
local label = tab.label
if label_tab_ids[label] == nil then
label_tab_ids[label] = { i }
else
table.insert(label_tab_ids[label], i)
end
end
-- Collect tab-array-ids with non-unique labels
return H.tbl_flatten(vim.tbl_filter(function(x) return #x > 1 end, label_tab_ids))
end
-- Fit tabline to maximum displayed width -------------------------------------
H.fit_width = function()
H.update_center_buf_id()
-- Compute label width data
local center_offset = 1
local tot_width = 0
for _, tab in pairs(H.tabs) do
-- Use `nvim_strwidth()` and not `:len()` to respect multibyte characters
tab.label_width = vim.api.nvim_strwidth(tab.label)
tab.chars_on_left = tot_width
tot_width = tot_width + tab.label_width
if tab.buf_id == H.center_buf_id then
-- Make right end of 'center tab' to be always displayed in center in
-- case of truncation
center_offset = tot_width
end
end
local display_interval = H.compute_display_interval(center_offset, tot_width)
H.truncate_tabs_display(display_interval)
end
H.update_center_buf_id = function()
local cur_buf = vim.api.nvim_get_current_buf()
if H.is_buffer_in_minitabline(cur_buf) then H.center_buf_id = cur_buf end
end
H.compute_display_interval = function(center_offset, tabline_width)
-- left - first character to be displayed (starts with 1)
-- right - last character to be displayed
-- Conditions to be satisfied:
-- 1) right - left + 1 = math.min(tot_width, tabline_width)
-- 2) 1 <= left <= tabline_width; 1 <= right <= tabline_width
local tot_width = vim.o.columns - vim.api.nvim_strwidth(H.tabpage_section)
-- Usage of `math.floor` is crucial to avoid non-integer values which might
-- affect total width of output tabline string.
-- Using `floor` instead of `ceil` has effect when `tot_width` is odd:
-- - `floor` makes "true center" to be between second to last and last label
-- character (usually non-space and space).
-- - `ceil` - between last character of center label and first character of
-- next label (both whitespaces).
local right = math.min(tabline_width, math.floor(center_offset + 0.5 * tot_width))
local left = math.max(1, right - tot_width + 1)
right = left + math.min(tot_width, tabline_width) - 1
return { left, right }
end
H.truncate_tabs_display = function(display_interval)
local display_left, display_right = display_interval[1], display_interval[2]
local tabs = {}
for _, tab in ipairs(H.tabs) do
local tab_left = tab.chars_on_left + 1
local tab_right = tab.chars_on_left + tab.label_width
if (display_left <= tab_right) and (tab_left <= display_right) then
-- Process tab that should be displayed (even partially)
local n_trunc_left = math.max(0, display_left - tab_left)
local n_trunc_right = math.max(0, tab_right - display_right)
-- Take desired amount of characters starting from `n_trunc_left`
tab.label = vim.fn.strcharpart(tab.label, n_trunc_left, tab.label_width - n_trunc_right)
table.insert(tabs, tab)
end
end
H.tabs = tabs
end
-- Concatenate tabs into single tablien string --------------------------------
H.concat_tabs = function()
-- NOTE: it is assumed that all padding is incorporated into labels
local t = {}
for _, tab in ipairs(H.tabs) do
-- Escape '%' in labels
table.insert(t, ('%s%s%s'):format(tab.hl, tab.tabfunc, tab.label:gsub('%%', '%%%%')))
end
-- Usage of `%X` makes filled space to the right 'non-clickable'
local res = ('%s%%X%%#MiniTablineFill#'):format(table.concat(t, ''))
-- Add tabpage section
local position = H.get_config().tabpage_section
if H.tabpage_section ~= '' then
if position == 'left' then res = ('%%#MiniTablineTabpagesection#%s%s'):format(H.tabpage_section, res) end
if position == 'right' then
-- Use `%=` to make it stick to right hand side
res = ('%s%%=%%#MiniTablineTabpagesection#%s'):format(res, H.tabpage_section)
end
end
return res
end
-- Utilities ------------------------------------------------------------------
H.ensure_get_icon = function(config)
if not config.show_icons then
-- Show no icon
H.get_icon = nil
elseif H.get_icon ~= nil then
-- Cache only once
return
elseif _G.MiniIcons ~= nil then
-- Prefer 'mini.icons'
H.get_icon = function(name) return (_G.MiniIcons.get('file', name)) end
else
-- Try falling back to 'nvim-web-devicons'
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')
if not has_devicons then return end
-- Use basename because it makes exact file name matching work
H.get_icon = function(name) return (devicons.get_icon(vim.fn.fnamemodify(name, ':t'), nil, { default = true })) end
end
end
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
H.tbl_flatten = vim.fn.has('nvim-0.10') == 1 and function(x) return vim.iter(x):flatten(math.huge):totable() end
or vim.tbl_flatten
return MiniTabline

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,201 @@
--- *mini.trailspace* Trailspace (highlight and remove)
--- *MiniTrailspace*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Highlighting is done only in modifiable buffer by default, only in Normal
--- mode, and stops in Insert mode and when leaving window.
---
--- - Trim all trailing whitespace with |MiniTrailspace.trim()|.
---
--- - Trim all trailing empty lines with |MiniTrailspace.trim_last_lines()|.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.trailspace').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniTrailspace` which you can use for scripting or manually (with
--- `:lua MiniTrailspace.*`).
---
--- See |MiniTrailspace.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minitrailspace_config` which should have same structure as
--- `MiniTrailspace.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Highlight groups ~
---
--- * `MiniTrailspace` - highlight group for trailing space.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minitrailspace_disable` (globally) or
--- `vim.b.minitrailspace_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. Note: after disabling
--- there might be highlighting left; it will be removed after next
--- highlighting update (see |events| and `MiniTrailspace` |augroup|).
-- Module definition ==========================================================
local MiniTrailspace = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniTrailspace.config|.
---
---@usage >lua
--- require('mini.trailspace').setup() -- use default config
--- -- OR
--- require('mini.trailspace').setup({}) -- replace {} with your config table
--- <
MiniTrailspace.setup = function(config)
-- Export module
_G.MiniTrailspace = MiniTrailspace
-- 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()
-- Initialize highlight (usually takes effect during startup)
vim.defer_fn(MiniTrailspace.highlight, 0)
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniTrailspace.config = {
-- Highlight only in normal buffers (ones with empty 'buftype'). This is
-- useful to not show trailing whitespace where it usually doesn't matter.
only_in_normal_buffers = true,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Highlight trailing whitespace in current window
MiniTrailspace.highlight = function()
-- Highlight only in normal mode
if H.is_disabled() or vim.fn.mode() ~= 'n' then
MiniTrailspace.unhighlight()
return
end
-- Possibly work only in normal buffers
if H.get_config().only_in_normal_buffers and not H.is_buffer_normal() then return end
-- Don't add match id on top of existing one
if H.get_match_id() ~= nil then return end
vim.fn.matchadd('MiniTrailspace', [[\s\+$]])
end
--- Unhighlight trailing whitespace in current window
MiniTrailspace.unhighlight = function()
-- Use `pcall` because there is an error if match id is not present. It can
-- happen if something else called `clearmatches`.
pcall(vim.fn.matchdelete, H.get_match_id())
end
--- Trim trailing whitespace
MiniTrailspace.trim = function()
-- Save cursor position to later restore
local curpos = vim.api.nvim_win_get_cursor(0)
-- Search and replace trailing whitespace
vim.cmd([[keeppatterns %s/\s\+$//e]])
vim.api.nvim_win_set_cursor(0, curpos)
end
--- Trim last blank lines
MiniTrailspace.trim_last_lines = function()
local n_lines = vim.api.nvim_buf_line_count(0)
local last_nonblank = vim.fn.prevnonblank(n_lines)
if last_nonblank < n_lines then vim.api.nvim_buf_set_lines(0, last_nonblank, n_lines, true, {}) end
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniTrailspace.config)
-- 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({ only_in_normal_buffers = { config.only_in_normal_buffers, 'boolean' } })
return config
end
H.apply_config = function(config) MiniTrailspace.config = config end
H.create_autocommands = function(config)
local augroup = vim.api.nvim_create_augroup('MiniTrailspace', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
-- NOTE: Respecting both `WinEnter` and `BufEnter` seems to be useful to
-- account of different order of handling buffer opening in new window.
-- Notable example: 'nvim-tree' at commit a1600e5.
au({ 'WinEnter', 'BufEnter', 'InsertLeave' }, '*', MiniTrailspace.highlight, 'Highlight')
au({ 'WinLeave', 'BufLeave', 'InsertEnter' }, '*', MiniTrailspace.unhighlight, 'Unhighlight')
if config.only_in_normal_buffers then
-- Add tracking of 'buftype' changing because it can be set after events on
-- which highlighting is done. If not done, highlighting appears but
-- disappears if buffer is reentered.
au('OptionSet', 'buftype', H.track_normal_buffer, 'Track normal buffer')
end
end
H.create_default_hl = function() vim.api.nvim_set_hl(0, 'MiniTrailspace', { default = true, link = 'Error' }) end
H.is_disabled = function() return vim.g.minitrailspace_disable == true or vim.b.minitrailspace_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniTrailspace.config, vim.b.minitrailspace_config or {}, config or {})
end
H.track_normal_buffer = function()
if not H.get_config().only_in_normal_buffers then return end
-- This should be used with 'OptionSet' event for 'buftype' option
-- Empty 'buftype' means "normal buffer"
if vim.v.option_new == '' then
MiniTrailspace.highlight()
else
MiniTrailspace.unhighlight()
end
end
H.is_buffer_normal = function(buf_id) return vim.bo[buf_id or 0].buftype == '' end
H.get_match_id = function()
-- NOTE: this can be replaced with more efficient custom tracking of id per
-- window but it will have more edge cases (like won't update on manual
-- `clearmatches()`)
for _, match in ipairs(vim.fn.getmatches()) do
if match.group == 'MiniTrailspace' then return match.id end
end
end
return MiniTrailspace

File diff suppressed because it is too large Load Diff