--- *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/.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: --- - `(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. --- --- - `(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: --- - - string with full pattern match. --- - - match line number (1-indexed). --- - - match starting byte column (1-indexed). --- - - match ending byte column (1-indexed, inclusive). --- --- - It can return `nil` meaning this particular match will not be highlighted. --- --- - `(table|function|nil)` - optional extra options --- for |nvim_buf_set_extmark()|. If callable, will be called in the same way --- as callable (`data` will also contain `hl_group` key with --- value) and should return a table with all options for extmark (including --- `end_row`, `end_col`, `hl_group`, and `priority`). --- Note: if is supplied, is ignored. --- --- - `(number|nil)` - SOFT DEPRECATED in favor --- of `extmark_opts = { priority = }`. --- 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: --- - `(number)` - buffer identifier of a match. --- - `(any)` - highlighter identifier which produced the match. --- - `(number)` - line number of the match start (starts with 1). --- - `(number)` - column number of the match start (starts with 1). --- - `(number|nil)` - line number of the match end (starts with 1). --- - `(number|nil)` - column number next to the match end --- (implements end-exclusive region; starts with 1). --- - `(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: --- -