dotfiles/.config/nvim/lua/mini/hipatterns.lua

1022 lines
39 KiB
Lua

--- *mini.hipatterns* Highlight patterns in text
--- *MiniHipatterns*
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Highlight text with configurable patterns and highlight groups (can be
--- string or callable).
---
--- - Highlighting is updated asynchronously with configurable debounce delay.
---
--- - Function to get matches in a buffer (see |MiniHipatterns.get_matches()|).
---
--- See |MiniHipatterns-examples| for common configuration examples.
---
--- Notes:
--- - It does not define any highlighters by default. Add to `config.highlighters`
--- to have a visible effect.
---
--- - Sometimes (especially during frequent buffer updates on same line numbers)
--- highlighting can be outdated or not applied when it should be. This is due
--- to asynchronous nature of updates reacting to text changes (via
--- `on_lines` of |nvim_buf_attach()|).
--- To make them up to date, use one of the following:
--- - Scroll window (for example, with |CTRL-E| / |CTRL-Y|). This will ensure
--- up to date highlighting inside window view.
--- - Hide and show buffer.
--- - Execute `:edit` (if you enabled highlighting with |MiniHipatterns.setup()|).
--- - Manually call |MiniHipatterns.update()|.
---
--- - If you experience flicker when typing near highlighted pattern in Insert
--- mode, it might be due to `delay` configuration of 'mini.completion' or
--- using built-in completion.
--- For better experience with 'mini.completion', make sure that its
--- `delay.completion` is less than this module's `delay.text_change` (which
--- it is by default).
--- The reason for this is (currently unresolvable) limitations of Neovim's
--- built-in completion implementation.
---
--- # Setup ~
---
--- Setting up highlights can be done in two ways:
--- - Manually for every buffer with `require('mini.hipatterns').enable()`.
--- This will enable highlighting only in one particular buffer until it is
--- unloaded (which also includes calling `:edit` on current file).
---
--- - Globally with `require('mini.hipatterns').setup({})` (replace `{}` with
--- your `config` table). This will auto-enable highlighting in "normal"
--- buffers (see 'buftype'). Use |MiniHipatterns.enable()| to manually enable
--- in other buffers.
--- It will also create global Lua table `MiniHipatterns` which you can use
--- for scripting or manually (with `:lua MiniHipatterns.*`).
---
--- See |MiniHipatterns.config| for `config` structure and default values.
---
--- You can override runtime config settings (like highlighters and delays)
--- locally to buffer inside `vim.b.minihipatterns_config` which should have
--- same structure as `MiniHipatterns.config`.
--- See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - 'folke/todo-comments':
--- - Oriented for "TODO", "NOTE", "FIXME" like patterns, while this module
--- can work with any Lua patterns and computable highlight groups.
--- - Has functionality beyond text highlighting (sign placing,
--- "telescope.nvim" extension, etc.), while this module only focuses on
--- highlighting text.
--- - 'folke/paint.nvim':
--- - Mostly similar to this module, but with slightly less functionality,
--- such as computed pattern and highlight group, asynchronous delay, etc.
--- - 'NvChad/nvim-colorizer.lua':
--- - Oriented for color highlighting, while this module can work with any
--- Lua patterns and computable highlight groups.
--- - Has more built-in color spaces to highlight, while this module out of
--- the box provides only hex color highlighting
--- (see |MiniHipatterns.gen_highlighter.hex_color()|). Other types are
--- also possible to implement.
--- - 'uga-rosa/ccc.nvim':
--- - Has more than color highlighting functionality, which is compared to
--- this module in the same way as 'NvChad/nvim-colorizer.lua'.
---
--- # Highlight groups~
---
--- * `MiniHipatternsFixme` - suggested group to use for `FIXME`-like patterns.
--- * `MiniHipatternsHack` - suggested group to use for `HACK`-like patterns.
--- * `MiniHipatternsTodo` - suggested group to use for `TODO`-like patterns.
--- * `MiniHipatternsNote` - suggested group to use for `NOTE`-like patterns.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- This module can be disabled in three ways:
--- - Globally: set `vim.g.minihipatterns_disable` to `true`.
--- - Locally for buffer permanently: set `vim.b.minihipatterns_disable` to `true`.
--- - Locally for buffer temporarily (until next auto-enabling event if set up
--- with |MiniHipatterns.setup()|): call |MiniHipatterns.disable()|.
---
--- 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.
--- # Common configuration examples ~
---
--- - Special words used to convey different level of attention: >
---
--- require('mini.hipatterns').setup({
--- highlighters = {
--- fixme = { pattern = 'FIXME', group = 'MiniHipatternsFixme' },
--- hack = { pattern = 'HACK', group = 'MiniHipatternsHack' },
--- todo = { pattern = 'TODO', group = 'MiniHipatternsTodo' },
--- note = { pattern = 'NOTE', group = 'MiniHipatternsNote' },
--- }
--- })
--- <
--- - To match only when pattern appears as a standalone word, use frontier
--- patterns `%f`. For example, instead of `'TODO'` pattern use
--- `'%f[%w]()TODO()%f[%W]'`. In this case, for example, 'TODOING' or 'MYTODO'
--- won't match, but 'TODO' and 'TODO:' will.
---
--- - Color hex (like `#rrggbb`) highlighting: >
---
--- local hipatterns = require('mini.hipatterns')
--- hipatterns.setup({
--- highlighters = {
--- hex_color = hipatterns.gen_highlighter.hex_color(),
--- }
--- })
--- <
--- You can customize which part of hex color is highlighted by using `style`
--- field of input options. See |MiniHipatterns.gen_highlighter.hex_color()|.
---
--- - Colored words: >
---
--- local words = { red = '#ff0000', green = '#00ff00', blue = '#0000ff' }
--- local word_color_group = function(_, match)
--- local hex = words[match]
--- if hex == nil then return nil end
--- return MiniHipatterns.compute_hex_color_group(hex, 'bg')
--- end
---
--- local hipatterns = require('mini.hipatterns')
--- hipatterns.setup({
--- highlighters = {
--- word_color = { pattern = '%S+', group = word_color_group },
--- },
--- })
---
--- - Trailing whitespace (if don't want to use more specific 'mini.trailspace'): >
---
--- { pattern = '%f[%s]%s*$', group = 'Error' }
---
--- - Censor certain sensitive information: >
---
--- local censor_extmark_opts = function(_, match, _)
--- local mask = string.rep('x', vim.fn.strchars(match))
--- return {
--- virt_text = { { mask, 'Comment' } }, virt_text_pos = 'overlay',
--- priority = 200, right_gravity = false,
--- }
--- end
---
--- require('mini.hipatterns').setup({
--- highlighters = {
--- censor = {
--- pattern = 'password: ()%S+()',
--- group = '',
--- extmark_opts = censor_extmark_opts,
--- },
--- },
--- })
---
--- - Enable only in certain filetypes. There are at least these ways to do it:
--- - (Suggested) With `vim.b.minihipatterns_config` in |filetype-plugin|.
--- Basically, create "after/ftplugin/<filetype>.lua" file in your config
--- directory (see |$XDG_CONFIG_HOME|) and define `vim.b.minihipatterns_config`
--- there with filetype specific highlighters.
---
--- This assumes `require('mini.hipatterns').setup()` call.
---
--- For example, to highlight keywords in EmmyLua comments in Lua files,
--- create "after/ftplugin/lua.lua" with the following content: >
---
--- vim.b.minihipatterns_config = {
--- highlighters = {
--- emmylua = { pattern = '^%s*%-%-%-()@%w+()', group = 'Special' }
--- }
--- }
--- <
--- - Use callable `pattern` with condition. For example: >
---
--- require('mini.hipatterns').setup({
--- highlighters = {
--- emmylua = {
--- pattern = function(buf_id)
--- if vim.bo[buf_id].filetype ~= 'lua' then return nil end
--- return '^%s*%-%-%-()@%w+()'
--- end,
--- group = 'Special',
--- },
--- },
--- })
--- <
--- - Disable only in certain filetypes. Enable with |MiniHipatterns.setup()|
--- and set `vim.b.minihipatterns_disable` buffer-local variable to `true` for
--- buffer you want disabled. See |mini.nvim-disabling-recipes| for more examples.
---@tag MiniHipatterns-examples
---@alias __hipatterns_buf_id number|nil Buffer identifier in which to enable highlighting.
--- Default: 0 for current buffer.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
-- Module definition ==========================================================
local MiniHipatterns = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniHipatterns.config|.
---
---@usage `require('mini.hipatterns').setup({})` (replace `{}` with your `config` table)
---@text
--- Note: no highlighters is defined by default. Add them for visible effect.
MiniHipatterns.setup = function(config)
-- Export module
_G.MiniHipatterns = MiniHipatterns
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
for _, win_id in ipairs(vim.api.nvim_list_wins()) do
H.auto_enable({ buf = vim.api.nvim_win_get_buf(win_id) })
end
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Options ~
---
--- ## Highlighters ~
---
--- `highlighters` table defines which patterns will be highlighted by placing
--- |extmark| at the match start. It might or might not have explicitly named
--- fields, but having them is recommended and is required for proper use of
--- `vim.b.minihipatterns_config` as buffer-local config. By default it is
--- empty expecting user definition.
---
--- Each entry defines single highlighter as a table with the following fields:
--- - <pattern> `(string|function|table)` - Lua pattern to highlight. Can be
--- either string, callable returning the string, or an array of those.
--- If string:
--- - It can have submatch delimited by placing `()` on start and end, NOT
--- by surrounding with it. Otherwise it will result in error containing
--- `number expected, got string`. Example: `xx()abcd()xx` will match `abcd`
--- only if `xx` is placed before and after it.
---
--- If callable:
--- - It will be called for every enabled buffer with its identifier as input.
---
--- - It can return `nil` meaning this particular highlighter will not work
--- in this particular buffer.
---
--- If array:
--- - Each element is matched and highlighted with the same highlight group.
---
--- - <group> `(string|function)` - name of highlight group to use. Can be either
--- string or callable returning the string.
--- If callable:
--- - It will be called for every pattern match with the following arguments:
--- - `buf_id` - buffer identifier.
--- - `match` - string pattern match to be highlighted.
--- - `data` - extra table with information about the match.
--- It has at least these fields:
--- - <full_match> - string with full pattern match.
--- - <line> - match line number (1-indexed).
--- - <from_col> - match starting byte column (1-indexed).
--- - <to_col> - match ending byte column (1-indexed, inclusive).
---
--- - It can return `nil` meaning this particular match will not be highlighted.
---
--- - <extmark_opts> `(table|function|nil)` - optional extra options
--- for |nvim_buf_set_extmark()|. If callable, will be called in the same way
--- as callable <group> (`data` will also contain `hl_group` key with <group>
--- value) and should return a table with all options for extmark (including
--- `end_row`, `end_col`, `hl_group`, and `priority`).
--- Note: if <extmark_opts> is supplied, <priority> is ignored.
---
--- - <priority> `(number|nil)` - SOFT DEPRECATED in favor
--- of `extmark_opts = { priority = <value> }`.
--- Optional highlighting priority (as in |nvim_buf_set_extmark()|).
--- Default: 200. See also |vim.highlight.priorities|.
---
--- See "Common use cases" section for the examples.
---
--- ## Delay ~
---
--- `delay` is a table defining delays in milliseconds used for asynchronous
--- highlighting process.
---
--- `delay.text_change` is used to delay highlighting updates by accumulating
--- them (in debounce fashion). Smaller values will lead to faster response but
--- more frequent updates. Bigger - slower response but less frequent updates.
---
--- `delay.scroll` is used to delay updating highlights in current window view
--- during scrolling (see |WinScrolled| event). These updates are present to
--- ensure up to date highlighting after scroll.
MiniHipatterns.config = {
-- Table with highlighters (see |MiniHipatterns.config| for more details).
-- Nothing is defined by default. Add manually for visible effect.
highlighters = {},
-- Delays (in ms) defining asynchronous highlighting process
delay = {
-- How much to wait for update after every text change
text_change = 200,
-- How much to wait for update after window scroll
scroll = 50,
},
}
--minidoc_afterlines_end
--- Enable highlighting in buffer
---
--- Notes:
--- - With default config it will highlight nothing, as there are no default
--- highlighters.
---
--- - Buffer highlighting is enabled until buffer is unloaded from memory
--- or |MiniHipatterns.disable()| on this buffer is called.
---
--- - `:edit` disables this, as it is mostly equivalent to closing and opening
--- buffer. In order for highlighting to persist after `:edit`, call
--- |MiniHipatterns.setup()|.
---
---@param buf_id __hipatterns_buf_id
---@param config table|nil Optional buffer-local config. Should have the same
--- structure as |MiniHipatterns.config|. Values will be taken in this order:
--- - From this `config` argument (if supplied).
--- - From buffer-local config in `vim.b.minihipatterns_config` (if present).
--- - From global config (if |MiniHipatterns.setup()| was called).
--- - From default values.
MiniHipatterns.enable = function(buf_id, config)
buf_id = H.validate_buf_id(buf_id)
config = H.validate_config_arg(config)
-- Don't enable more than once
if H.is_buf_enabled(buf_id) then return end
-- Register enabled buffer with cached data for performance
H.update_cache(buf_id, config)
-- Add buffer watchers
vim.api.nvim_buf_attach(buf_id, false, {
-- Called on every text change (`:h nvim_buf_lines_event`)
on_lines = function(_, _, _, from_line, _, to_line)
local buf_cache = H.cache[buf_id]
-- Properly detach if highlighting is disabled
if buf_cache == nil then return true end
H.process_lines(buf_id, from_line + 1, to_line, buf_cache.delay.text_change)
end,
-- Called when buffer content is changed outside of current session
on_reload = function() pcall(MiniHipatterns.update, buf_id) end,
-- Called when buffer is unloaded from memory (`:h nvim_buf_detach_event`),
-- **including** `:edit` command
on_detach = function() MiniHipatterns.disable(buf_id) end,
})
-- Add buffer autocommands
local augroup = vim.api.nvim_create_augroup('MiniHipatternsBuffer' .. buf_id, { clear = true })
H.cache[buf_id].augroup = augroup
local update_buf = vim.schedule_wrap(function()
if not H.is_buf_enabled(buf_id) then return end
H.update_cache(buf_id, config)
local delay_ms = H.cache[buf_id].delay.text_change
H.process_lines(buf_id, 1, vim.api.nvim_buf_line_count(buf_id), delay_ms)
end)
vim.api.nvim_create_autocmd(
{ 'BufWinEnter', 'FileType' },
{ group = augroup, buffer = buf_id, callback = update_buf, desc = 'Update highlighting for whole buffer' }
)
vim.api.nvim_create_autocmd(
'WinScrolled',
{ group = augroup, buffer = buf_id, callback = H.update_view, desc = 'Update highlighting in view' }
)
-- Add highlighting to whole buffer
H.process_lines(buf_id, 1, vim.api.nvim_buf_line_count(buf_id), 0)
end
--- Disable highlighting in buffer
---
--- Note that if |MiniHipatterns.setup()| was called, the effect is present
--- until the next auto-enabling event. To permanently disable highlighting in
--- buffer, set `vim.b.minihipatterns_disable` to `true`
---
---@param buf_id __hipatterns_buf_id
MiniHipatterns.disable = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return end
H.cache[buf_id] = nil
vim.api.nvim_del_augroup_by_id(buf_cache.augroup)
for _, ns in pairs(H.ns_id) do
H.clear_namespace(buf_id, ns, 0, -1)
end
end
--- Toggle highlighting in buffer
---
--- Call |MiniHipatterns.disable()| if enabled; |MiniHipatterns.enable()| otherwise.
---
---@param buf_id __hipatterns_buf_id
---@param config table|nil Forwarded to |MiniHipatterns.enable()|.
MiniHipatterns.toggle = function(buf_id, config)
buf_id = H.validate_buf_id(buf_id)
config = H.validate_config_arg(config)
if H.is_buf_enabled(buf_id) then
MiniHipatterns.disable(buf_id)
else
MiniHipatterns.enable(buf_id, config)
end
end
--- Update highlighting in range
---
--- Works only in buffer with enabled highlighting. Effect takes immediately
--- without delay.
---
---@param buf_id __hipatterns_buf_id
---@param from_line number|nil Start line from which to update (1-indexed).
---@param to_line number|nil End line from which to update (1-indexed, inclusive).
MiniHipatterns.update = function(buf_id, from_line, to_line)
buf_id = H.validate_buf_id(buf_id)
if not H.is_buf_enabled(buf_id) then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
from_line = from_line or 1
if type(from_line) ~= 'number' then H.error('`from_line` should be a number.') end
to_line = to_line or vim.api.nvim_buf_line_count(buf_id)
if type(to_line) ~= 'number' then H.error('`to_line` should be a number.') end
-- Process lines immediately without delay
H.process_lines(buf_id, from_line, to_line, 0)
end
--- Get an array of enabled buffers
---
---@return table Array of buffer identifiers with enabled highlighting.
MiniHipatterns.get_enabled_buffers = function()
local res = {}
for buf_id, _ in pairs(H.cache) do
if vim.api.nvim_buf_is_valid(buf_id) then
table.insert(res, buf_id)
else
-- Clean up if buffer is invalid and for some reason is still enabled
H.cache[buf_id] = nil
end
end
-- Ensure consistent order
table.sort(res)
return res
end
--- Get buffer matches
---
---@param buf_id number|nil Buffer identifier for which to return matches.
--- Default: `nil` for current buffer.
---@param highlighters table|nil Array of highlighter identifiers (as in
--- `highlighters` field of |MiniHipatterns.config|) for which to return matches.
--- Default: all available highlighters (ordered by string representation).
---
---@return table Array of buffer matches which are tables with following fields:
--- - <bufnr> `(number)` - buffer identifier of a match.
--- - <highlighter> `(any)` - highlighter identifier which produced the match.
--- - <lnum> `(number)` - line number of the match start (starts with 1).
--- - <col> `(number)` - column number of the match start (starts with 1).
--- - <end_lnum> `(number|nil)` - line number of the match end (starts with 1).
--- - <end_col> `(number|nil)` - column number next to the match end
--- (implements end-exclusive region; starts with 1).
--- - <hl_group> `(string|nil)` - name of match's highlight group.
---
--- Matches are ordered first by supplied `highlighters`, then by line and
--- column of match start.
MiniHipatterns.get_matches = function(buf_id, highlighters)
buf_id = (buf_id == nil or buf_id == 0) and vim.api.nvim_get_current_buf() or buf_id
if not (type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id)) then
H.error('`buf_id` is not valid buffer identifier.')
end
local all_highlighters = H.get_all_highlighters()
highlighters = highlighters or all_highlighters
if not vim.tbl_islist(highlighters) then H.error('`highlighters` should be an array.') end
highlighters = vim.tbl_filter(function(x) return vim.tbl_contains(all_highlighters, x) end, highlighters)
local position_compare = function(a, b) return a[2] < b[2] or (a[2] == b[2] and a[3] < b[3]) end
local res = {}
for _, hi_id in ipairs(highlighters) do
local extmarks = H.get_extmarks(buf_id, H.ns_id[hi_id], 0, -1, { details = true })
table.sort(extmarks, position_compare)
for _, extmark in ipairs(extmarks) do
local end_lnum, end_col = extmark[4].end_row, extmark[4].end_col
end_lnum = type(end_lnum) == 'number' and (end_lnum + 1) or end_lnum
end_col = type(end_col) == 'number' and (end_col + 1) or end_col
--stylua: ignore
local entry = {
bufnr = buf_id, highlighter = hi_id,
lnum = extmark[2] + 1, col = extmark[3] + 1,
end_lnum = end_lnum, end_col = end_col,
hl_group = extmark[4].hl_group,
}
table.insert(res, entry)
end
end
return res
end
--- Generate builtin highlighters
---
--- This is a table with function elements. Call to actually get highlighter.
MiniHipatterns.gen_highlighter = {}
--- Highlight hex color string
---
--- This will match color hex string in format `#rrggbb` and highlight it
--- according to `opts.style` displaying matched color.
---
--- Highlight group is computed using |MiniHipatterns.compute_hex_color_group()|,
--- so all its usage notes apply here.
---
---@param opts table|nil Options. Possible fields:
--- - <style> `(string)` - one of:
--- - `'full'` - highlight background of whole hex string with it. Default.
--- - `'#'` - highlight background of only `#`.
--- - `'line'` - highlight underline with that color.
--- - `'inline'` - highlight text of <inline_text>.
--- Note: requires Neovim>=0.10.
--- - <priority> `(number)` - priority of highlighting. Default: 200.
--- - <filter> `(function)` - callable object used to filter buffers in which
--- highlighting will take place. It should take buffer identifier as input
--- and return `false` or `nil` to not highlight inside this buffer.
--- - <inline_text> `(string)` - string to be placed and highlighted with color
--- to the right of match in case <style> is "inline". Default: "█".
---
---@return table Highlighter table ready to be used as part of `config.highlighters`.
--- Both `pattern` and `group` are callable.
---
---@usage >
--- local hipatterns = require('mini.hipatterns')
--- hipatterns.setup({
--- highlighters = {
--- hex_color = hipatterns.gen_highlighter.hex_color(),
--- }
--- })
MiniHipatterns.gen_highlighter.hex_color = function(opts)
local default_opts = { style = 'full', priority = 200, filter = H.always_true, inline_text = '' }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
local style = opts.style
if style == 'inline' and vim.fn.has('nvim-0.10') == 0 then
H.error('Style "inline" in `gen_highlighter.hex_color()` requires Neovim>=0.10.')
end
local pattern = style == '#' and '()#()%x%x%x%x%x%x%f[%X]' or '#%x%x%x%x%x%x%f[%X]'
local hl_style = ({ full = 'bg', ['#'] = 'bg', line = 'line', inline = 'fg' })[style] or 'bg'
local extmark_opts = { priority = opts.priority }
if opts.style == 'inline' then
local priority, inline_text = opts.priority, opts.inline_text
---@diagnostic disable:cast-local-type
extmark_opts = function(_, _, data)
local virt_text = { { inline_text, data.hl_group } }
return { virt_text = virt_text, virt_text_pos = 'inline', priority = priority, right_gravity = false }
end
end
return {
pattern = H.wrap_pattern_with_filter(pattern, opts.filter),
group = function(_, _, data) return MiniHipatterns.compute_hex_color_group(data.full_match, hl_style) end,
extmark_opts = extmark_opts,
}
end
--- Compute and create group to highlight hex color string
---
--- Notes:
--- - This works properly only with enabled |termguicolors|.
---
--- - To increase performance, it caches highlight groups per `hex_color`. If
--- you want to try different style in current Neovim session, execute
--- |:colorscheme| command to clear cache. Needs a call to |MiniHipatterns.setup()|.
---
---@param hex_color string Hex color string in format `#rrggbb`.
---@param style string One of:
--- - `'bg'` - highlight background with `hex_color` and foreground with black or
--- white (whichever is more visible). Default.
--- - `'fg'` - highlight foreground with `hex_color`.
--- - `'line'` - highlight underline with `hex_color`.
---
---@return string Name of created highlight group appropriate to show `hex_color`.
MiniHipatterns.compute_hex_color_group = function(hex_color, style)
local hex = hex_color:lower():sub(2)
local group_name = 'MiniHipatterns' .. hex
-- Use manually tracked table instead of `vim.fn.hlexists()` because the
-- latter still returns true for cleared highlights
if H.hex_color_groups[group_name] then return group_name end
-- Define highlight group if it is not already defined
if style == 'bg' then
-- Compute opposite color based on Oklab lightness (for better contrast)
local opposite = H.compute_opposite_color(hex)
vim.api.nvim_set_hl(0, group_name, { fg = opposite, bg = hex_color })
end
if style == 'fg' then vim.api.nvim_set_hl(0, group_name, { fg = hex_color }) end
if style == 'line' then vim.api.nvim_set_hl(0, group_name, { sp = hex_color, underline = true }) end
-- Keep track of created groups to properly react on `:hi clear`
H.hex_color_groups[group_name] = true
return group_name
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniHipatterns.config)
-- Timers
H.timer_debounce = vim.loop.new_timer()
H.timer_view = vim.loop.new_timer()
-- Namespaces per highlighter name
H.ns_id = {}
-- Cache of queued changes used for debounced highlighting
H.change_queue = {}
-- Cache per enabled buffer
H.cache = {}
-- Data about created highlight groups for hex colors
H.hex_color_groups = {}
-- 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({
highlighters = { config.highlighters, 'table' },
delay = { config.delay, 'table' },
})
vim.validate({
['delay.text_change'] = { config.delay.text_change, 'number' },
['delay.scroll'] = { config.delay.scroll, 'number' },
})
return config
end
H.apply_config = function(config) MiniHipatterns.config = config end
H.create_autocommands = function()
local augroup = vim.api.nvim_create_augroup('MiniHipatterns', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
au('BufEnter', '*', H.auto_enable, 'Enable highlighting')
au('ColorScheme', '*', H.on_colorscheme, 'Reload all enabled pattern highlighters')
end
--stylua: ignore
H.create_default_hl = function()
vim.api.nvim_set_hl(0, 'MiniHipatternsFixme', { default = true, link = 'DiagnosticError' })
vim.api.nvim_set_hl(0, 'MiniHipatternsHack', { default = true, link = 'DiagnosticWarn' })
vim.api.nvim_set_hl(0, 'MiniHipatternsTodo', { default = true, link = 'DiagnosticInfo' })
vim.api.nvim_set_hl(0, 'MiniHipatternsNote', { default = true, link = 'DiagnosticHint' })
end
H.is_disabled = function(buf_id)
local buf_disable = H.get_buf_var(buf_id, 'minihipatterns_disable')
return vim.g.minihipatterns_disable == true or buf_disable == true
end
H.get_config = function(config, buf_id)
local buf_config = H.get_buf_var(buf_id, 'minihipatterns_config') or {}
return vim.tbl_deep_extend('force', MiniHipatterns.config, buf_config, config or {})
end
H.get_buf_var = function(buf_id, name)
if not vim.api.nvim_buf_is_valid(buf_id) then return nil end
return vim.b[buf_id or 0][name]
end
-- Autocommands ---------------------------------------------------------------
H.auto_enable = vim.schedule_wrap(function(data)
if H.is_buf_enabled(data.buf) then return end
-- Autoenable only in valid normal buffers. This function is scheduled so as
-- to have the relevant `buftype`.
if vim.api.nvim_buf_is_valid(data.buf) and vim.bo[data.buf].buftype == '' then MiniHipatterns.enable(data.buf) end
end)
H.update_view = vim.schedule_wrap(function(data)
-- Update view only in enabled buffers
local buf_cache = H.cache[data.buf]
if buf_cache == nil then return end
-- NOTE: due to scheduling (which is necessary for better performance),
-- current buffer can be not the target one. But as there is no proper (easy
-- and/or fast) way to get the view of certain buffer (except the current)
-- accept this approach. The main problem of current buffer having not
-- enabled highlighting is solved during processing buffer highlighters.
-- Debounce without aggregating redraws (only last view should be updated)
H.timer_view:stop()
H.timer_view:start(buf_cache.delay.scroll, 0, H.process_view)
end)
H.on_colorscheme = function()
-- Reset created highlight groups for hex colors, as they are probably
-- cleared after `:hi clear`
H.hex_color_groups = {}
-- Reload all currently enabled buffers
for buf_id, _ in pairs(H.cache) do
MiniHipatterns.disable(buf_id)
MiniHipatterns.enable(buf_id)
end
end
-- Validators -----------------------------------------------------------------
H.validate_buf_id = function(x)
if x == nil or x == 0 then return vim.api.nvim_get_current_buf() end
if not (type(x) == 'number' and vim.api.nvim_buf_is_valid(x)) then
H.error('`buf_id` should be `nil` or valid buffer id.')
end
return x
end
H.validate_config_arg = function(x)
if x == nil or type(x) == 'table' then return x or {} end
H.error('`config` should be `nil` or table.')
end
H.validate_string = function(x, name)
if type(x) == 'string' then return x end
H.error(string.format('`%s` should be string.'))
end
-- Enabling -------------------------------------------------------------------
H.is_buf_enabled = function(buf_id) return H.cache[buf_id] ~= nil end
H.update_cache = function(buf_id, config)
local buf_cache = H.cache[buf_id] or {}
local buf_config = H.get_config(config, buf_id)
buf_cache.highlighters = H.normalize_highlighters(buf_config.highlighters)
buf_cache.delay = buf_config.delay
H.cache[buf_id] = buf_cache
end
H.normalize_highlighters = function(highlighters)
local res = {}
for hi_name, hi in pairs(highlighters) do
-- Allow pattern to be string, callable, or array of those. Convert all
-- valid cases into array of callables.
local pattern = type(hi.pattern) == 'string' and function() return hi.pattern end or hi.pattern
if vim.is_callable(pattern) then pattern = { pattern } end
local is_pattern_ok = vim.tbl_islist(pattern)
if is_pattern_ok then
for i, pat in ipairs(pattern) do
pattern[i] = type(pat) == 'string' and function() return pat end or pat
is_pattern_ok = is_pattern_ok and vim.is_callable(pattern[i])
end
end
local group = type(hi.group) == 'string' and function() return hi.group end or hi.group
-- TODO: Remove after Neovim 0.11 is released
local has_raw_priority = type(hi.priority) == 'number'
if has_raw_priority then
local msg = '`priority` field of highlighter is soft deprecated.'
.. ' Use `extmark_opts = { priority = <value> }`.'
.. ' This works for now but will be removed after the next stable release.'
vim.notify_once(msg, vim.log.levels.WARN)
end
local extmark_opts = hi.extmark_opts or { priority = has_raw_priority and hi.priority or 200 }
if type(extmark_opts) == 'table' then
local t = extmark_opts
---@diagnostic disable:cast-local-type
extmark_opts = function(_, _, data)
local opts = vim.deepcopy(t)
opts.hl_group = opts.hl_group or data.hl_group
opts.end_row = opts.end_row or (data.line - 1)
opts.end_col = opts.end_col or data.to_col
return opts
end
end
if is_pattern_ok and vim.is_callable(group) and vim.is_callable(extmark_opts) then
res[hi_name] = { pattern = pattern, group = group, extmark_opts = extmark_opts }
H.ns_id[hi_name] = vim.api.nvim_create_namespace('MiniHipatterns-' .. hi_name)
end
end
return res
end
H.get_all_highlighters = function()
local hi_arr = vim.tbl_map(function(x) return { x, tostring(x) } end, vim.tbl_keys(H.ns_id))
table.sort(hi_arr, function(a, b) return a[2] < b[2] end)
return vim.tbl_map(function(x) return x[1] end, hi_arr)
end
-- Processing -----------------------------------------------------------------
H.process_lines = vim.schedule_wrap(function(buf_id, from_line, to_line, delay_ms)
-- Make sure that that at least one line is processed (important to react
-- after deleting line with extmark non-trivial `extmark_opts`)
table.insert(H.change_queue, { buf_id, math.min(from_line, to_line), math.max(from_line, to_line) })
-- Debounce
H.timer_debounce:stop()
H.timer_debounce:start(delay_ms, 0, H.process_change_queue)
end)
H.process_view = vim.schedule_wrap(function()
table.insert(H.change_queue, { vim.api.nvim_get_current_buf(), vim.fn.line('w0'), vim.fn.line('w$') })
-- Process immediately assuming debouncing should be already done
H.process_change_queue()
end)
H.process_change_queue = vim.schedule_wrap(function()
local queue = H.normalize_change_queue()
for buf_id, lines_to_process in pairs(queue) do
H.process_buffer_changes(buf_id, lines_to_process)
end
H.change_queue = {}
end)
H.normalize_change_queue = function()
local res = {}
for _, change in ipairs(H.change_queue) do
-- `change` is { buf_id, from_line, to_line }; lines are already 1-indexed
local buf_id = change[1]
local buf_lines_to_process = res[buf_id] or {}
for i = change[2], change[3] do
buf_lines_to_process[i] = true
end
res[buf_id] = buf_lines_to_process
end
return res
end
H.process_buffer_changes = vim.schedule_wrap(function(buf_id, lines_to_process)
-- Return early if buffer is not proper.
-- Also check if buffer is enabled here mostly for better resilience. It
-- might be actually needed due to various `schedule_wrap`s leading to change
-- queue entry with not target (and improper) buffer.
local buf_cache = H.cache[buf_id]
if not vim.api.nvim_buf_is_valid(buf_id) or H.is_disabled(buf_id) or buf_cache == nil then return end
-- Optimizations are done assuming small-ish number of highlighters and
-- large-ish number of lines to process
-- Process highlighters
for hi_name, hi in pairs(buf_cache.highlighters) do
-- Remove current highlights
local ns = H.ns_id[hi_name]
for l_num, _ in pairs(lines_to_process) do
H.clear_namespace(buf_id, ns, l_num - 1, l_num)
end
-- Add new highlights
for _, pattern in ipairs(hi.pattern) do
H.apply_highlighter_pattern(pattern(buf_id), hi, buf_id, ns, lines_to_process)
end
end
end)
H.apply_highlighter_pattern = vim.schedule_wrap(function(pattern, hi, buf_id, ns, lines_to_process)
if type(pattern) ~= 'string' then return end
local group, extmark_opts = hi.group, hi.extmark_opts
local pattern_has_line_start = pattern:sub(1, 1) == '^'
-- Apply per proper line
for l_num, _ in pairs(lines_to_process) do
local line = H.get_line(buf_id, l_num)
local from, to, sub_from, sub_to = line:find(pattern)
while from and (from <= to) do
-- Compute full pattern match
local full_match = line:sub(from, to)
-- Compute (possibly inferred) submatch
sub_from, sub_to = sub_from or from, sub_to or (to + 1)
-- - Make last column end-inclusive
sub_to = sub_to - 1
local match = line:sub(sub_from, sub_to)
-- Set extmark based on submatch
local data = { full_match = full_match, line = l_num, from_col = sub_from, to_col = sub_to }
local hl_group = group(buf_id, match, data)
if hl_group ~= nil then
data.hl_group = hl_group
H.set_extmark(buf_id, ns, l_num - 1, sub_from - 1, extmark_opts(buf_id, match, data))
end
-- Overcome an issue that `string.find()` doesn't recognize `^` when
-- `init` is more than 1
if pattern_has_line_start then break end
from, to, sub_from, sub_to = line:find(pattern, to + 1)
end
end
end)
-- Built-in highlighters ------------------------------------------------------
H.wrap_pattern_with_filter = function(pattern, filter)
return function(...)
if not filter(...) then return nil end
return pattern
end
end
H.compute_opposite_color = function(hex)
local dec = tonumber(hex, 16)
local b = H.correct_channel(math.fmod(dec, 256) / 255)
local g = H.correct_channel(math.fmod((dec - b) / 256, 256) / 255)
local r = H.correct_channel(math.floor(dec / 65536) / 255)
local l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
local m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
local s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
local l_, m_, s_ = H.cuberoot(l), H.cuberoot(m), H.cuberoot(s)
local L = H.correct_lightness(0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_)
return L < 0.5 and '#ffffff' or '#000000'
end
-- Function for RGB channel correction. Assumes input in [0; 1] range
-- https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
H.correct_channel = function(x) return 0.04045 < x and math.pow((x + 0.055) / 1.055, 2.4) or (x / 12.92) end
-- Function for lightness correction
-- https://bottosson.github.io/posts/colorpicker/#intermission---a-new-lightness-estimate-for-oklab
H.correct_lightness = function(x)
local k1, k2 = 0.206, 0.03
local k3 = (1 + k1) / (1 + k2)
return 0.5 * (k3 * x - k1 + math.sqrt((k3 * x - k1) ^ 2 + 4 * k2 * k3 * x))
end
-- Utilities ------------------------------------------------------------------
H.error = function(msg) error(string.format('(mini.hipatterns) %s', msg), 0) end
H.get_line =
function(buf_id, line_num) return vim.api.nvim_buf_get_lines(buf_id, line_num - 1, line_num, false)[1] or '' end
H.set_extmark = function(...) pcall(vim.api.nvim_buf_set_extmark, ...) end
H.get_extmarks = function(...)
local ok, res = pcall(vim.api.nvim_buf_get_extmarks, ...)
if not ok then return {} end
return res
end
H.clear_namespace = function(...) pcall(vim.api.nvim_buf_clear_namespace, ...) end
H.always_true = function() return true end
H.cuberoot = function(x) return math.pow(x, 0.333333) end
return MiniHipatterns