8000 mini.nvim/lua/mini/map.lua at main · daniel-samson/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
1618 lines (1400 loc) · 63.1 KB

map.lua

File metadata and controls

1618 lines (1400 loc) · 63.1 KB
--- *mini.map* Window with buffer text overview
--- *MiniMap*
---
--- MIT License Copyright (c) 2022 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Show and manage special floating window displaying automatically updated
--- overview of current buffer text. Window takes up whole height of Neovim
--- instance and is fixed to a left/right side. Map content is computed by
--- taking all current lines, converting it to binary whitespace/non-whitespace
--- mask, rescaling to appropriate dimensions, and converting back to strings
--- consisting from special encoding symbols. All this is done **very fast** and
--- **asynchronously**. See |MiniMap.open()|, |MiniMap.refresh()|, |MiniMap.close()|,
--- |MiniMap.toggle()|, |MiniMap.toggle_side()|.
--- For a general overview and tips, see |mini.map-usage|.
---
--- - Show scrollbar next to map content. It represents current line and view
--- (top and bottom visible lines). Can be the only thing shown, making map
--- window a "pure scrollbar". See "Pure scrollbar config" section in
--- |MiniMap.config|.
---
--- - Highlight map lines representing certain data in current buffer. This is
--- done via extensible set of callables, called integrations (see
--- "Integrations" section in |MiniMap.config|). There are pre-built generators
--- for common integrations:
--- - Builtin search (as result of |/| and similar).
--- - Builtin diagnostic (taken from |vim.diagnostic.get()|).
--- - Git line status (with help of 'lewis6991/gitsigns.nvim', see
--- |gitsigns.get_hunks()|).
--- For more details see |MiniMap.gen_integration|.
---
--- - Focus on map window to quickly browse current (source) buffer. Moving inside
--- map window updates cursor position in source window enabling fast and
--- targeted buffer exploration. To focus back, hit `<CR>` to accept current
--- explored position or `<Esc>` to go back to original position. See
--- |MiniMap.toggle_focus()|.
---
--- - Customizable via |MiniMap.config| and/or `opts` argument of |MiniMap.open()|
--- or |MiniMap.refresh()|:
--- - Encoding symbols used to display binary information of different
--- resolution (default is 3x2). There are pre-built generators for
--- different basic character families and resolutions. See
--- |MiniMap.gen_encode_symbols|.
--- - Scrollbar symbols, separate for line and view. Can have any width
--- (even zero, which virtually disables scrollbar).
--- - Integrations producing map line highlights.
--- - Window options: side (left/right), width, 'winblend', and more.
---
--- What it doesn't do:
--- - Automatically refresh when typing in Insert mode. Although it can be done in
--- non-blocking way, it still might introduce considerable computation overhead
--- (especially in very large files).
--- - Has more flexible window configuration. In case a full height floating
--- window obstructs vision of underlying buffers, use |MiniMap.toggle()| or
--- |MiniMap.toggle_side()|. Works best with global statusline.
--- - Provide autoopen functionality. Due to vast differences in user preference
--- of when map window should be shown, set up of automatic opening is left to
--- user. A common approach would be to call `MiniMap.open()` on |VimEnter| event.
--- If you use |MiniStarter|, you can modify `<CR>` buffer mapping: >
---
--- vim.cmd([[autocmd User MiniStarterOpened
--- \ lua vim.keymap.set(
--- \ 'n',
--- \ '<CR>',
--- \ '<Cmd>lua MiniStarter.eval_current_item(); MiniMap.open()<CR>',
--- \ { buffer = true }
--- \ )]])
--- <
--- # Setup ~
---
--- This module needs a setup with `require('mini.map').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniMap`
--- which you can use for scripting or manually (with `:lua MiniMap.*`).
---
--- See |MiniMap.config| for available config settings.
---
--- You can override runtime config settings (like `config.modifiers`) locally
--- to buffer inside `vim.b.minimap_config` which should have same structure
--- as `MiniMap.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Dependencies ~
---
--- Suggested dependencies (provide extra functionality for integrations):
--- - Plugin 'lewis6991/gitsigns.nvim' for Git status highlighting via
--- |MiniMap.gen_integration.gitsigns()|. If missing, no highlighting is added.
---
--- # Comparisons ~
---
--- - 'wfxr/minimap.vim':
--- - 'mini.map' doesn't have dependencies while being as fast as written
--- in Rust dependency of 'minimap.vim'.
--- - 'mini.map' uses floating window, while 'minimap.vim' uses regular one.
--- - 'mini.map' prov 8000 ides slightly different visual interface with
--- scrollbar and integration counts.
--- - 'mini.map' allows encode symbols customization, 'minimap.vim' does not.
--- - 'mini.map' allows extending highlight integrations, while only
--- builtin search and git status are supported in 'minimap.vim'.
--- - 'mini.map' updates in asynchronous (non-blocking) fashion, 'minimap.vim'
--- does not.
--- - 'mini.map' can be used as a pure scrollbar, 'minimap.vim' can not.
--- - 'dstein64/nvim-scrollview':
--- - 'mini.map' has two-part scrollbar showing current line and view (with
--- variable height), while 'nvim-scrollview' shows only current view
--- (with fixed height).
--- - 'nvim-scrollview' respects folds, i.e. shows view of visible lines,
--- while 'mini.map' by design always shows view based on actual lines.
--- - 'nvim-scrollview' creates scrollbar which can be dragged with mouse,
--- while 'mini.nvim' does not, by design (use |MiniMap.toggle_focus()|).
--- - 'mini.map' can show buffer outline, while 'nvim-scrollview' can not.
--- - 'mini.map' can show highlight integrations, while 'nvim-scrollview'
--- can not.
--- - 'petertriho/nvim-scrollbar':
--- - 'mini.map' has two-part scrollbar showing current line and view (with
--- variable height), while 'nvim-scrollbar' shows only current view.
--- - 'mini.map' can show buffer outline, while 'nvim-scrollbar' can not.
--- - 'mini.map' has fully extendable highlight integrations, while
--- 'nvim-scrollbar' only supports diagnostic and search (with dependency).
--- - 'lewis6991/satellite.nvim':
--- - Almost the same differences as with 'dstein64/nvim-scrollview', except
--- 'satellite.nvim' can display some set of integration highlights.
---
--- # Highlight groups ~
---
--- * `MiniMapNormal` - basic highlight of whole window.
--- * `MiniMapSymbolCount` - counts of per-line integration items.
--- * `MiniMapSymbolLine` - scrollbar part representing current line.
--- * `MiniMapSymbolView` - scrollbar part representing current view.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minimap_disable` (globally) or `vim.b.minimap_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.
--- # Mappings ~
---
--- This module doesn't make mappings, only provides functions for users to map
--- manually. Here is how one |<Leader>| set of mappings can be constructed: >
---
--- vim.keymap.set('n', '<Leader>mc', MiniMap.close)
--- vim.keymap.set('n', '<Leader>mf', MiniMap.toggle_focus)
--- vim.keymap.set('n', '<Leader>mo', MiniMap.open)
--- vim.keymap.set('n', '<Leader>mr', MiniMap.refresh)
--- vim.keymap.set('n', '<Leader>ms', MiniMap.toggle_side)
--- vim.keymap.set('n', '<Leader>mt', MiniMap.toggle)
--- <
--- # How automatic refresh works ~
---
--- Automatic refresh is done by calling |MiniMap.refresh()| when appropriate
--- |events| occur. It is done with specially chosen `parts` argument value (to
--- avoid unnecessary computations). For example, when only cursor has moved
--- (|CursorMoved|), only scrollbar is updated; so no recomputation of integrations
--- or line encoding is done.
---
--- To avoid visual clutter, automatic refresh is done only in normal buffers
--- and help pages (i.e. with |buftype| being empty or "help")
---
--- When you think content is not up to date, try one of these:
--- - Call |MiniMap.refresh()| manually. Make mapping to make it easier.
--- - Save current buffer, for example with |:write|.
--- - Exit and enter Normal mode (if your Neovim version supports |ModeChanged|).
---@tag mini.map-usage
---@alias __map_opts table|nil Options used to define map configuration. Same structure
--- as |MiniMap.config|. Will have effect until at least one tabpage has opened
--- map window. Default values are taken in the following order:
--- - From `opts` field of |MiniMap.current|.
--- - From `vim.b.minimap_config`.
--- - From |MiniMap.config|.
---@diagnostic disable:undefined-field
-- Module definition ==========================================================
local MiniMap = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniMap.config|.
---
---@usage `require('mini.map').setup({})` (replace `{}` with your `config` table)
MiniMap.setup = function(config)
-- Export module
_G.MiniMap = MiniMap
-- 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
--stylua: ignore
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Options ~
---
--- ## Symbols ~
---
--- Options in `config.symbols` define characters used to display various
--- information in map window.
---
--- ### Encode sym 8BD0 bols ~
---
--- The `config.symbols.encode` option defines which characters are used to
--- encode source buffer lines. For details of encode algorithm, see
--- |MiniMap.encode_strings()|.
---
--- This option should be a table with the following structure:
--- - <resolution> field - table containing <row> and <col> elements with row
--- and column resolution of each symbol. This defines encoding structure and
--- number of needed encode symbols.
--- - Numerical fields 1, 2, ..., 2^(row_resolution * col_resolution). Each symbol
--- represents a `(row_resolution, col_resolution)` boolean mask (`true` for
--- non-whitespace, `false` for whitespace), created as (reversed) binary digit:
--- `true` as 1; `false` as 0. Traversing left-right, top-bottom (top-left is
--- lowest bit, bottom-right - highest). So first symbol encodes a complete
--- whitespace, last - complete non-whitespace.
---
--- If `nil` (default), output of |MiniMap.gen_encode_symbols.block()| with `'3x2'`
--- identifier is used.
---
--- Example: { '1', '2', '3', '4', resolution = { row = 1, col = 2 } }. This
--- will encode two characters in each input row. So a string `' a aaa'` will
--- be encoded as `'1234'`.
---
--- There are pre-built generators of encode symbols:
--- - |MiniMap.gen_encode_symbols.block()|
--- - |MiniMap.gen_encode_symbols.dot()|
--- - |MiniMap.gen_encode_symbols.shade()|
---
--- ### Scrollbar symbols ~
---
--- Options `config.symbols.scroll_line` and `config.symbols.scroll_view` define
--- strings used to represent current line and current view inside map window.
--- Can have any length, map window content will adjust.
---
--- If supplied window width is small enough so that only (part of) of
--- scrollbar can be shown, it is called a "pure scrollbar". The behavior differs
--- slightly from normal map window. See "Pure scrollbar config" later section.
---
--- Some suggestions for scrollbar symbols:
--- - View-line pairs: '▒' and '█'.
--- - Line - '🮚', '▶'.
--- - View - '╎', '┋', '┋'.
---
--- ## Integrations ~
---
--- Option `config.integrations` is an array of integrations. Each one is used
--- to define map line highlights representing some important lines in source
--- buffer. If `nil` (default), no integrations are used.
---
--- Each integration should be a callable returning an array with data about
--- **source buffer** lines it wants to highlight. Each array element should be
--- a table with <line> (source buffer line number) and <hl_group> (string with
--- highlight group name) keys. Note: line number outside of source buffer
--- count will be converted to a nearest appropriate one.
---
--- Example output of single integration: >
---
--- {
--- { line = 1, hl_group = 'Search' },
--- { line = 2, hl_group = 'Operator' },
--- { line = 9, hl_group = 'Search'}
--- }
--- <
--- Conversion to map highlights is done on a "first seen" basis: actual
--- highlight group applied to a map line is taken from the first integration
--- output convertible to that map line. Other outputs with same map line
--- (after conversion) contribute to integration count shown between scrollbar
--- and encoded lines (if `config.window.show_integration_count` is `true`).
---
--- Previous example output with default `'3x2'` resolution will add |hl-Search|
--- highlight on map lines 1 and 3, and show integration count 2 on first line.
---
--- Every element of integrations array is called one by one from start to end
--- with their outputs appended to end of single array. This means that more
--- important integrations should be placed in the beginning of an array, as
--- this will make them have higher priority in case other integrations will
--- highlight same map line.
---
--- Example of using `config.integrations`: >
---
--- local map = require('mini.map')
--- map.setup({
--- integrations = {
--- map.gen_integration.builtin_search(),
--- map.gen_integration.gitsigns(),
--- map.gen_integration.diagnostic(),
--- },
--- })
--- <
--- ## Window config ~
---
--- Option `config.window` defines some properties of map window.
---
--- `window.focusable` - whether to allow focusing on map window with other
--- methods beside |MiniMap.toggle_focus()| (like |wincmd|, |CTRL-W|, or
--- mouse). Default: `false`.
---
--- `window.side` - which side to stick map window: `'left'` or `'right'` (default).
---
--- `window.show_integration_count` - whether to show integration count between
--- scrollbar and encoded lines. Integration count is a number of integration
--- outputs which were converted to same map line. When `true`, adds single
--- cell column with numbers from 2 to 9 and character '+' indicating count
--- greater than 9. Count 1 is not shown, because it is redundant to highlighted
--- map line. Default: `true`.
---
--- `window.width` - width of floating window, including scrollbar and
--- integration count column. Default: 10.
---
--- `window.winblend` - value of 'winblend' of floating window. Value 0 makes it
--- completely non-transparent, 100 - completely transparent (content is still
--- visible, but with slightly different highlights).
---
--- `window.zindex` - z-index of floating window. Default: 10.
---
--- # Pure scrollbar config ~
---
--- "Pure scrollbar" is a configuration when window width is not enough to show
--- encoded content. It has following differences from default "map" approach:
--- - It doesn't perform line encoding with |MiniMap.encode_strings()|
--- but instead uses encoding with fixed number of lines (equal to window
--- height).
--- - Integration highlights are not computed.
---
--- Config: >
---
--- require('mini.map').setup({
--- -- Customize `symbols` to your liking
---
--- window = {
--- -- Set this to the maximum width of your scroll symbols
--- width = 1,
---
--- -- Set this to your liking. Try values 0, 25, 50, 75, 100
--- winblend = 100,
---
--- -- Don't need extra column
--- show_integration_count = false,
--- }
--- })
MiniMap.config = {
-- Highlight integrations (none by default)
integrations = nil,
-- Symbols used to display data
symbols = {
-- Encode symbols. See `:h MiniMap.config` for specification and
-- `:h MiniMap.gen_encode_symbols` for pre-built ones.
-- Default: solid blocks with 3x2 resolution.
encode = nil,
-- Scrollbar parts for view and line. Use empty string to disable any.
scroll_line = '█',
scroll_view = '┃',
},
-- Window options
window = {
-- Whether window is focusable in normal way (with `wincmd` or mouse)
focusable = false,
-- Side to stick ('left' or 'right')
side = 'right',
-- Whether to show count of multiple integration highlights
show_integration_count = true,
-- Total width
width = 10,
-- Value of 'winblend' option
winblend = 25,
-- Z-index
zindex = 10,
},
}
--minidoc_afterlines_end
--- Table with information about current state of map
---
--- At least these keys are supported:
--- - <buf_data> - table with buffer identifiers. Field <map> contains
--- identifier of a buffer used to display map. Field <source> - buffer
--- identifier which content map is displaying (i.e. source buffer).
--- - <win_data> - table of window identifiers used to display map in certain
--- tabpage. Keys: tabpage identifier. Values: window identifier.
--- - <opts> - current options used to control map display. Same structure
--- as |MiniMap.config|. Takes precedence over global and buffer-local configs.
--- Is reset when last map window is closed with |MiniMap.close()|.
MiniMap.current = {
buf_data = {},
win_data = {},
opts = {},
}
-- Module functionality =======================================================
--- Encode strings
---
--- This takes arbitrary array of strings and computes its non-whitespace
--- outline. Output is an array of strings with configurable array length, string
--- width, and symbols representing encoding.
---
--- Each encode symbol is assumed to have resolution within which it can convey
--- binary information. For example, resolution `3x2` (row resolution 3,
--- column - 2) means that each symbol can encode 3 rows and 2 columns of
--- binary data. Here it is used to encode non-whitespace mask. See more in
--- "Encode symbols" section of |MiniMap.config|.
---
--- Encoding has the following steps:
--- - Convert strings to boolean mask: 2d boolean array with each row
--- representing a string. Element in every row subarray is `true` if
--- respective (possibly multibyte) character in a string is not a whitespace,
--- `false` otherwise. Note: tabs are expanded into 'tabstop' spaces.
--- - Rescale to appropriate dimensions:
--- - Each output dimension is just enough to encode all input strings, but
--- not more than supplied dimensions (`opts.n_rows * resolution.row` and
--- `opts.n_cols * resolution.col` respectively).
--- - If input dimensions are too big to fit inside output, perform grid
--- downscaling with loss of information. Input boolean mask is divided
--- into 2d-bins with as equal as possible dimensions. Each bin then
--- converted into single boolean value: `true` if bin contains at least
--- one `true` element, `false` otherwise. This leads to a whitespace
--- output meaning that **all** entries in a bin are whitespace, while
--- non-whitespace output means that **some** entry is non-whitespace.
--- - Convert boolean mask to symbol strings:
--- - Input rescaled boolean mask is divided into bins with dimensions of
--- symbol resolution (assuming `false` outer padding).
--- - Each bin with resolution dimensions is transformed into encode symbol.
--- Single convertible `(resolution.row, resolution.col)` boolean
--- mask is treated as (reversed) binary digit: `true` as 1; `false` as 0.
--- Traversing left-right, top-bottom (top-left is lowest bit,
--- bottom-right - highest).
---
--- Example ~
---
--- Assume the output should have 3 rows of symbols each with width 2. Encode
--- symbols are ' ', '▌', '▐', '█' with `1x2` resolution.
---
--- Assume input strings: >
--- aaaaa
--- b b
---
--- d d
--- e e
--- <
--- Steps:
--- - Convert to boolean mask (each row is a boolean array, "t"/"f" ~ `true`/`false`,
--- empty spots are equivalent to being `false`): >
--- ttttt
--- ftft
---
--- ftft
--- tft
--- <
--- - Rescale. Output dimensions are `n_rows * resolution.row = 3 * 1 = 3` rows and
--- `n_cols * resolution.col = 2 * 2 = 4`. It creates as equal as possible grid
--- with 3 rows and 4 columns and converts bins to single booleans. Result: >
--- tttt
--- tftf
--- ttff
--- - Convert to symbols. It makes `1x2` bins, treats their input as (reversed)
--- binary digits (`ff=00=0`, `tf=10=1`, `ft=01=2`, `tt=11=3`) and takes
--- corresponding symbols from supplied options (value plus 1). Result: >
--- ██
--- ▌▌
--- █
--- <
---@param strings table Array of arbitrary strings.
---@param opts table|nil Options. Possible fields:
--- - <n_rows> - number of rows in output encoding. If too big, will be
--- truncated to be maximum needed to encode all input strings (taking into
--- account symbols row resolution). Default: `math.huge`.
--- - <n_cols> - width of every encoding string. If too big, will be truncated
--- to be maximum needed to encode all input strings (taking into account
--- symbols column resolution). Default: `math.huge`.
--- - <symbols> - array of symbols with extra `resolution` field. See "Encode
--- symbols" section of |MiniMap.config| for more details. Default: output
--- of |MiniMap.gen_encode_symbols.block()| with `'3x2'` identifier.
---
---@return table Array of encoded strings.
MiniMap.encode_strings = function(strings, opts)
-- Validate input
if not H.is_array_of(strings, H.is_string) then
H.error('`strings` argument of `encode_strings()` should be array of strings.')
end
opts = vim.tbl_deep_extend(
'force',
{ n_rows = math.huge, n_cols = math.huge, symbols = H.get_config().symbols.encode or H.default_symbols },
opts or {}
)
-- Compute encoding
local mask = H.mask_from_strings(strings, opts)
mask = H.mask_rescale(mask, opts)
return H.mask_to_symbols(mask, opts)
end
--- Open map window
---
--- This creates and shows map window in current tabpage. It basically has
--- two steps:
--- - If not already done, create map buffer (used to set lines and other
--- visual indicators) and map window.
--- - Call |MiniMap.refresh()|.
---
---@param opts __map_opts
MiniMap.open = function(opts)
-- Early returns
if H.is_disabled() then return end
-- Normalize input
opts = H.normalize_opts(opts)
-- Allow execution in case of already opened window
if H.is_window_open() then
MiniMap.refresh(opts)
return
end
-- Open buffer and window
local buf_id = MiniMap.current.buf_data.map
if buf_id == nil or not vim.api.nvim_buf_is_valid(buf_id) then
buf_id = H.create_map_buffer()
MiniMap.current.buf_data.map = buf_id
end
local win_id = vim.api.nvim_open_win(buf_id, false, H.normalize_window_options(opts.window))
H.set_current_map_win(win_id)
-- Set buffer and window options. Other important options are handled by
-- `style = 'minimal'` in `nvim_open_win()`.
vim.api.nvim_win_call(win_id, function()
--stylua: ignore
local options = {
'buftype=nofile', 'foldcolumn=0', 'foldlevel=999', 'matchpairs=', 'nobuflisted',
'nomodeline', 'noreadonly', 'noswapfile', 'synmaxcol&', 'nowrap',
}
-- Vim's `setlocal` is currently more robust compared to `opt_local`
-- Use `noautocmd` to make it more invisible for others
vim.cmd(('silent! noautocmd setlocal %s'):format(table.concat(options, ' ')))
-- Override Normal highlighting locally for map window
vim.cmd('silent! setlocal winhighlight=NormalFloat:MiniMapNormal')
end)
-- Refresh content
MiniMap.refresh(opts)
end
--- Refresh map window
---
--- This function serves two purposes:
--- - Update current map configuration via `opts`.
--- - Update parts of displayed content via `parts`.
---
---@param opts __map_opts
---@param parts table|nil Which parts to update. Recognised keys with boolean
--- values (all `true` by default):
--- - <integrations> - whether to update integration highlights.
--- - <lines> - whether to update map lines.
--- - <scrollbar> - whether to update scrollbar.
MiniMap.refresh = function(opts, parts)
-- Early return
if H.is_disabled() or not H.is_window_open() then return end
-- Normalize input
opts = H.normalize_opts(opts)
parts = vim.tbl_deep_extend('force', { integrations = true, lines = true, scrollbar = true }, parts or {})
-- Update current data
H.cache.scrollbar_data.offset = math.max(H.str_width(opts.symbols.scroll_line), H.str_width(opts.symbols.scroll_view))
+ (opts.window.show_integration_count and 1 or 0)
MiniMap.current.opts = opts
-- Update window options
H.update_window_opts()
-- Possibly update parts in asynchronous fashion
if parts.lines then vim.schedule(H.update_map_lines) end
if parts.scrollbar then vim.schedule(H.update_map_scrollbar) end
if parts.integrations then vim.schedule(H.update_map_integrations) end
end
--- Close map window
---
--- Also resets `opts` field of |MiniMap.current| after closing last map window
--- (among possibly several tabpages).
MiniMap.close = function()
pcall(vim.api.nvim_win_close, H.get_current_map_win(), true)
H.set_current_map_win(nil)
-- Reset current options if closed last window so as to use config during
-- next opening
if vim.tbl_count(MiniMap.current.win_data) == 0 then MiniMap.current.opts = {} end
end
--- Toggle map window
---
--- Open if not shown in current tabpage, close otherwise.
---
---@param opts table|nil Input for |MiniMap.open()|.
MiniMap.toggle = function(opts)
if H.is_window_open() then
MiniMap.close()
else
MiniMap.open(opts)
end
end
--- Toggle focus to/from map window
---
--- When not inside map window, put cursor inside map window; otherwise put
--- cursor in previous window with source buffer.
---
--- When cursor is moving inside map window (but not just after focusing), view of
--- source window is updated to show first line convertible to current map line.
--- This allows quick targeted source buffer exploration.
---
--- There are at least these extra methods to focus back from map window:
--- - Press `<CR>` to accept current explored position in source buffer.
--- Equivalent to calling this function with `false` argument.
--- - Press `<Esc>` to go back to original position prior focusing on map window.
--- Equivalent to calling this function with `true` argument.
---
---@param use_previous_cursor boolean|nil Whether to focus on source window at
--- original cursor position (the one prior focusing on map window).
MiniMap.toggle_focus = function(use_previous_cursor)
if not H.is_window_open() then return end
local cur_win, map_win = vim.api.nvim_get_current_win(), H.get_current_map_win()
if cur_win == map_win then
-- Focus on previous window
vim.api.nvim_set_current_win(H.cache.previous_win.id)
-- Use either previous cursor or first non-whitespace character (if this
-- was the result of cursor movement inside map window)
if use_previous_cursor then
vim.api.nvim_win_set_cursor(0, H.cache.previous_win.cursor)
elseif H.cache.n_map_cursor_moves > 1 then
vim.cmd('normal! ^')
end
else
-- Focus on map window. Cursor is set on `BufEnter` to account for other
-- ways of focusing on buffer (for example, with `<C-w><C-w>`)
vim.api.nvim_set_current_win(map_win)
end
end
--- Toggle side of map window
---
--- A small convenience wrapper for calling |MiniMap.refresh()| to change the
--- side of map window.
MiniMap.toggle_side = function()
if not H.is_window_open() then return end
local cur_side = MiniMap.current.opts.window.side
MiniMap.refresh(
{ window = { side = cur_side == 'left' and 'right' or 'left' } },
{ integrations = false, lines = false, scrollbar = false }
)
end
--- Generate encode symbols
---
--- This is a table with function elements. Call to actually get encode symbols.
---
--- Each element takes a string resolution identifier of a form `'rxc'` (like `'3x2'`)
--- where `r` is a row resolution of each symbol (how many rows of binary data it
--- can encode) and `c` is a column resolution (how many columns it can encode).
MiniMap.gen_encode_symbols = {}
--- Generate block encode symbols
---
--- Outputs use solid block to encode binary data. Example: '🬗', '▟', '█'.
---
---@param id string Resolution identifier.
--- Available values: `'1x2'`, `'2x1'`, `'2x2'`, `'3x2'` (default in 'mini.map').
MiniMap.gen_encode_symbols.block = function(id) return H.block_symbols[id] end
--- Generate dot encode symbols
---
--- Outputs use dots to encode binary data. Example: '⡪', '⣼', '⣿'.
---
---@param id string Resolution identifier. Available values: `'4x2'`, `'3x2'`.
MiniMap.gen_encode_symbols.dot = function(id) return H.dot_symbols[id] end
--- Generate shade encode symbols
---
--- Outputs use whole cell shades to encode binary data. They use same set of
--- characters ('░', '▒', '▒', '▓), but with different resolution.
---
---@param id string Resolution identifier. Available values: `'1x2'`, `'2x1'`.
MiniMap.gen_encode_symbols.shade = function(id) return H.shade_symbols[id] end
--- Generate integrations
---
--- This is a table with function elements. Call to actually get encode symbols.
---
--- Each element takes a table defining highlight groups used for to highlight
--- map lines.
MiniMap.gen_integration = {}
--- Builtin search
---
--- Highlight lines with matches of current builtin search (like with |/|, |?|, etc.).
--- Integration count reflects number of actual matches.
---
--- It prompts integration highlighting update on every change of |hlsearch| option
--- (see |OptionSet|). Note, that it doesn't do that when search is
--- started with |n|, |N|, or similar (there is no good approach for this yet).
--- To enable highlight update on this keys, make custom mappings. Like this: >
---
--- for _, key in ipairs({ 'n', 'N', '*', '#' }) do
--- vim.keymap.set(
--- 'n',
--- key,
--- key ..
--- '<Cmd>lua MiniMap.refresh({}, {lines = false, scrollbar = false})<CR>'
--- )
--- end
--- <
---@param hl_groups table|nil Table defining highlight groups. Can have the
--- following fields:
--- - <search> - highlight group for search matches. Default: |hl-Search|.
MiniMap.gen_integration.builtin_search = function(hl_groups)
if hl_groups == nil then hl_groups = { search = 'Search' } end
-- Update when necessary. Not ideal, because it won't react on `n/N/*`, etc.
-- See https://github.com/neovim/neovim/issues/18879
local augroup = vim.api.nvim_create_augroup('MiniMapBuiltinSearch', {})
vim.api.nvim_create_autocmd(
'OptionSet',
{ group = augroup, pattern = 'hlsearch', callback = H.on_integration_update, desc = "On 'hlsearch' update" }
)
local search_hl = hl_groups.search
return function()
-- Do nothing of search is not active
if vim.v.hlsearch == 0 or not vim.o.hlsearch then return {} end
-- Do nothing if not inside source buffer (can happen in map buffer, for example)
if not H.is_source_buffer() then return {} end
-- Save window view to later restore, as the only way to get positions of
-- search matches seems to be consecutive application of `search()` and
-- retrieving cursor position.
local win_view = vim.fn.winsaveview()
vim.api.nvim_win_set_cursor(0, { 1, 0 })
local search_count = vim.fn.searchcount({ recompute = true, maxcount = 0 })
local search_pattern = vim.fn.getreg('/')
local line_hl = {}
for _ = 1, (search_count.total or 0) do
vim.fn.search(search_pattern)
table.insert(line_hl, { line = vim.fn.line('.'), hl_group = search_hl })
end
vim.fn.winrestview(win_view)
return line_hl
end
end
--- Builtin diagnostic
---
--- Highlight lines with matches of current diagnostic items. Items are computed
--- with |vim.diagnostic.get()| for current (source) buffer.
---
--- It prompts integration highlighting update on every |DiagnosticChanged| event.
--- Diagnostic items with higher severity (see |vim.diagnostic.severity|) have
--- higher highlight priority (errors will be shown over all others, etc.).
---
---@param hl_groups table|nil Table defining highlight groups. Supplied fields
--- also define which diagnostic severity to highlight.
--- Can have the following fields:
--- - <error> - highlight group for error items.
--- Default: |hl-DiagnosticFloatingError|.
--- - <warn> - highlight group for warning items. Default: `nil` (not shown).
--- - <info> - highlight group for info items. Default: `nil` (not shown).
--- - <hint> - highlight group for hint items. Default: `nil` (not shown).
---
---@usage Show all diagnostic levels: >
--- local map = require('mini.map')
--- local diagnostic_integration = map.gen_integration.diagnostic({
--- error = 'DiagnosticFloatingError',
--- warn = 'DiagnosticFloatingWarn',
--- info = 'DiagnosticFloatingInfo',
--- hint = 'DiagnosticFloatingHint',
--- })
--- map.setup({ integrations = { diagnostic_integration } })
MiniMap.gen_integration.diagnostic = function(hl_groups)
if hl_groups == nil then hl_groups = { error = 'DiagnosticFloatingError' } end
-- Precompute ordered array of supported levels. Using keys of
-- `severity_highlights` is not enough because higher severity should be
-- processed later in order to appear on top.
local severity_level_names = vim.tbl_filter(
function(x) return vim.tbl_contains(vim.tbl_keys(hl_groups), x) end,
{ 'error', 'warn', 'info', 'hint' }
)
local severity_data = vim.tbl_map(
function(x) return { severity = vim.diagnostic.severity[x:upper()], hl_group = hl_groups[x] } end,
severity_level_names
)
-- Refresh map when needed
local augroup = vim.api.nvim_create_augroup('MiniMapDiagnostics', {})
vim.api.nvim_create_autocmd(
'DiagnosticChanged',
{ group = augroup, callback = H.on_integration_update, desc = 'On DiagnosticChanged' }
)
return function()
local line_hl = {}
local diagnostic_arr = vim.diagnostic.get(MiniMap.current.buf_data.source)
for _, data in ipairs(severity_data) do
local severity_diagnostic_arr = vim.tbl_filter(function(x) return x.severity == data.severity end, diagnostic_arr)
for _, diag in ipairs(severity_diagnostic_arr) do
-- Add all diagnostic lines to highlight
for i = diag.lnum, diag.end_lnum do
table.insert(line_hl, { line = i + 1, hl_group = data.hl_group })
end
end
end
return line_hl
end
end
--- Git line status
---
--- Highlight lines which have non-trivial Git status. Requires dependency
--- 'lewis6991/gitsigns.nvim' installed and set up. Uses |gitsigns.get_hunks()|
--- and should highlight map lines similarly to how Gitsigns highlights source
--- buffer lines (except dealing with rescaled input on "first seen" bases; see
--- "Integrations" section in |MiniMap.config|).
---
--- It prompts integration highlighting update on every |gitsigns-event|.
---
---@param hl_groups table|nil Table defining highlight groups. If `nil` (not
--- supplied), this status is not highlighted. Can have the following fields:
--- - <add> - highlight group for added lines. Default: "GitSignsAdd".
--- - <change> - highlight group for changed lines. Default: "GitSignsChange".
--- - <delete> - highlight group for deleted lines. Default: "GitSignsDelete".
MiniMap.gen_integration.gitsigns = function(hl_groups)
if hl_groups == nil then hl_groups = { add = 'GitSignsAdd', change = 'GitSignsChange', delete = 'GitSignsDelete' } end
local augroup = vim.api.nvim_create_augroup('MiniMapGitsigns', {})
vim.api.nvim_create_autocmd(
'User',
{ group = augroup, pattern = 'GitSignsUpdate', callback = H.on_integration_update, desc = 'On GitSignsUpdate' }
)
return function()
local has_gitsigns, gitsigns = pcall(require, 'gitsigns')
if not has_gitsigns or gitsigns == nil then return {} end
local has_hunks, hunks = pcall(gitsigns.get_hunks, MiniMap.current.buf_data.source)
if not has_hunks or hunks == nil then return {} end
local line_hl = {}
for _, hunk in ipairs(hunks) do
local from_line = hunk.added.start
local n_added, n_removed = hunk.added.count, hunk.removed.count
local n_lines = math.max(n_added, 1)
-- Highlight similar to 'gitsigns' itself:
-- - Delete - single first line if nothing was added.
-- - Change - added lines that are within first removed lines.
-- - Added - added lines after first removed lines.
for i = 1, n_lines do
local hl_type = (n_added < i and 'delete') or (i <= n_removed and 'change' or 'add')
local hl_group = hl_groups[hl_type]
if hl_group ~= nil then table.insert(line_hl, { line = from_line + i - 1, hl_group = hl_group }) end
end
end
return line_hl
end
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniMap.config)
-- Cache for various operations
H.cache = {
-- Data about previous window. Used for focus related computations.
previous_win = {},
-- Table with information used for latest buffer lines encoding. Used for
-- quick conversion between source and map coordinates.
encode_data = {},
-- Table with information about scrollbar. Used for quick scrollbar related
-- computations.
scrollbar_data = { view = {}, line = nil },
-- Number of cursor movements inside map buffer since focusing. Needed to not
-- update source buffer view just after focusing.
n_map_cursor_moves = 0,
}
H.ns_id = {
integrations = vim.api.nvim_create_namespace('MiniMapIntegrations'),
scroll_view = vim.api.nvim_create_namespace('MiniMapScrollView'),
scroll_line = vim.api.nvim_create_namespace('MiniMapScrollLine'),
}
--stylua: ignore start
H.block_symbols = {}
H.block_symbols['1x2'] = { ' ', '▌', '▐', '█', resolution = { row = 1, col = 2 } }
H.block_symbols['2x1'] = { ' ', '▀', '▄', '█', resolution = { row = 2, col = 1 } }
H.block_symbols['2x2'] = {
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
resolution = { row = 2, col = 2 },
}
H.block_symbols['3x2'] = {
' ', '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎',
'🬏', '🬐', '🬑', '🬒', '🬓', '▌', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝',
'🬞', '🬟', '🬠', '🬡', '🬢', '🬣', '🬤', '🬥', '🬦', '🬧', '▐', '🬨', '🬩', '🬪', '🬫', '🬬',
'🬭', '🬮', '🬯', '🬰', '🬱', '🬲', '🬳', '🬴', '🬵', '🬶', '🬷', '🬸', '🬹', '🬺', '🬻', '█',
resolution = { row = 3, col = 2 },
}
H.dot_symbols = {}
H.dot_symbols['4x2'] = {
'⠀', '⠁', '⠈', '⠉', '⠂', '⠃', '⠊', '⠋', '⠐', '⠑', '⠘', '⠙', '⠒', '⠓', '⠚', '⠛',
'⠄', '⠅', '⠌', '⠍', '⠆', '⠇', '⠎', '⠏', '⠔', '⠕', '⠜', '⠝', '⠖', '⠗', '⠞', '⠟',
'⠠', '⠡', '⠨', '⠩', '⠢', '⠣', '⠪', '⠫', '⠰', '⠱', '⠸', '⠹', '⠲', '⠳', '⠺', '⠻',
'⠤', '⠥', '⠬', '⠭', '⠦', '⠧', '⠮', '⠯', '⠴', '⠵', '⠼', '⠽', '⠶', '⠷', '⠾', '⠿',
'⡀', '⡁', '⡈', '⡉', '⡂', '⡃', '⡊', '⡋', '⡐', '⡑', '⡘', '⡙', '⡒', '⡓', '⡚', '⡛',
'⡄', '⡅', '⡌', '⡍', '⡆', '⡇', '⡎', '⡏', '⡔', '⡕', '⡜', '⡝', '⡖', '⡗', '⡞', '⡟',
'⡠', '⡡', '⡨', '⡩', '⡢', '⡣', '⡪', '⡫', '⡰', '⡱', '⡸', '⡹', '⡲', '⡳', '⡺', '⡻',
'⡤', '⡥', '⡬', '⡭', '⡦', '⡧', '⡮', '⡯', '⡴', '⡵', '⡼', '⡽', '⡶', '⡷', '⡾', '⡿',
'⢀', '⢁', '⢈', '⢉', '⢂', '⢃', '⢊', '⢋', '⢐', '⢑', '⢘', '⢙', '⢒', '⢓', '⢚', '⢛',
'⢄', '⢅', '⢌', '⢍', '⢆', '⢇', '⢎', '⢏', '⢔', '⢕', '⢜', '⢝', '⢖', '⢗', '⢞', '⢟',
'⢠', '⢡', '⢨', '⢩', '⢢', '⢣', '⢪', '⢫', '⢰', '⢱', '⢸', '⢹', '⢲', '⢳', '⢺', '⢻',
'⢤', '⢥', '⢬', '⢭', '⢦', '⢧', '⢮', '⢯', '⢴', '⢵', '⢼', '⢽', '⢶', '⢷', '⢾', '⢿',
'⣀', '⣁', '⣈', '⣉', '⣂', '⣃', '⣊', '⣋', '⣐', '⣑', '⣘', '⣙', '⣒', '⣓', '⣚', '⣛',
'⣄', '⣅', '⣌', '⣍', '⣆', '⣇', '⣎', '⣏', '⣔', '⣕', '⣜', '⣝', '⣖', '⣗', '⣞', '⣟',
'⣠', '⣡', '⣨', '⣩', '⣢', '⣣', '⣪', '⣫', '⣰', '⣱', '⣸', '⣹', '⣲', '⣳', '⣺', '⣻',
'⣤', '⣥', '⣬', '⣭', '⣦', '⣧', '⣮', '⣯', '⣴', '⣵', '⣼', '⣽', '⣶', '⣷', '⣾', '⣿',
resolution = { row = 4, col = 2 },
}
H.dot_symbols['3x2'] = { resolution = { row = 3, col = 2 } }
for i = 1,64 do H.dot_symbols['3x2'][i] = H.dot_symbols['4x2'][i] end
H.shade_symbols = {}
H.shade_symbols['2x1'] = { '░', '▒', '▒', '▓', resolution = { row = 2, col = 1 } }
H.shade_symbols['1x2'] = { '░', '▒', '▒', '▓', resolution = { row = 1, col = 2 } }
H.default_symbols = H.block_symbols['3x2']
--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 {})
vim.validate({
integrations = { config.integrations, H.is_valid_config_integrations },
symbols = { config.symbols, H.is_valid_config_symbols },
window = { config.window, H.is_valid_config_window },
})
return config
end
H.apply_config = function(config) MiniMap.config = config end
0