diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua index cc9a734..c13031a 100644 --- a/.config/nvim/init.lua +++ b/.config/nvim/init.lua @@ -199,3 +199,24 @@ require('onedark').setup { require('onedark').load() require('func.puppet') +require('mini.cursorword').setup() + +local hipatterns = require('mini.hipatterns') +hipatterns.setup({ + highlighters = { + -- Highlight standalone 'FIXME', 'HACK', 'TODO', 'NOTE' + fixme = { pattern = '%f[%w]()FIXME()%f[%W]', group = 'MiniHipatternsFixme' }, + hack = { pattern = '%f[%w]()HACK()%f[%W]', group = 'MiniHipatternsHack' }, + todo = { pattern = '%f[%w]()TODO()%f[%W]', group = 'MiniHipatternsTodo' }, + note = { pattern = '%f[%w]()NOTE()%f[%W]', group = 'MiniHipatternsNote' }, + + -- Highlight hex color strings (`#rrggbb`) using that color + hex_color = hipatterns.gen_highlighter.hex_color(), + }, +}) + +require('mini.indentscope').setup() +--require('mini.pairs').setup() +require('mini.splitjoin').setup() +require('mini.surround').setup() +require('mini.trailspace').setup() diff --git a/.config/nvim/lua/mini/cursorword.lua b/.config/nvim/lua/mini/cursorword.lua new file mode 100644 index 0000000..9568ff4 --- /dev/null +++ b/.config/nvim/lua/mini/cursorword.lua @@ -0,0 +1,287 @@ +--- *mini.cursorword* Autohighlight word under cursor +--- *MiniCursorword* +--- +--- MIT License Copyright (c) 2021 Evgeni Chasnovski +--- +--- ============================================================================== +--- +--- Features: +--- - Autohighlight word under cursor with customizable delay. +--- +--- - Current word under cursor can be highlighted differently. +--- +--- - Highlighting is triggered only if current cursor character is a |[:keyword:]|. +--- +--- - Highlighting stops in insert and terminal modes. +--- +--- - "Word under cursor" is meant as in Vim's ||: something user would +--- get as 'iw' text object. +--- +--- # Setup~ +--- +--- This module needs a setup with `require('mini.cursorword').setup({})` +--- (replace `{}` with your `config` table). It will create global Lua table +--- `MiniCursorword` which you can use for scripting or manually (with +--- `:lua MiniCursorword.*`). +--- +--- See |MiniCursorword.config| for `config` structure and default values. +--- +--- You can override runtime config settings locally to buffer inside +--- `vim.b.minicursorword_config` which should have same structure as +--- `MiniCursorword.config`. See |mini.nvim-buffer-local-config| for more details. +--- +--- # Highlight groups~ +--- +--- * `MiniCursorword` - highlight group of a non-current cursor word. +--- Default: plain underline. +--- +--- * `MiniCursorwordCurrent` - highlight group of a current word under cursor. +--- Default: links to `MiniCursorword` (so `:hi clear MiniCursorwordCurrent` +--- will lead to showing `MiniCursorword` highlight group). +--- Note: To not highlight it, use +--- +--- `:hi! MiniCursorwordCurrent guifg=NONE guibg=NONE gui=NONE cterm=NONE` +--- +--- To change any highlight group, modify it directly with |:highlight|. +--- +--- # Disabling~ +--- +--- To disable core functionality, set `vim.g.minicursorword_disable` (globally) or +--- `vim.b.minicursorword_disable` (for a buffer) to `true`. Considering high +--- number of different scenarios and customization intentions, writing exact +--- rules for disabling module's functionality is left to user. See +--- |mini.nvim-disabling-recipes| for common recipes. Note: after disabling +--- there might be highlighting left; it will be removed after next +--- highlighting update. +--- +--- Module-specific disabling: +--- - Don't show highlighting if cursor is on the word that is in a blocklist +--- of current filetype. In this example, blocklist for "lua" is "local" and +--- "require" words, for "javascript" - "import": +--- > +--- _G.cursorword_blocklist = function() +--- local curword = vim.fn.expand('') +--- local filetype = vim.bo.filetype +--- +--- -- Add any disabling global or filetype-specific logic here +--- local blocklist = {} +--- if filetype == 'lua' then +--- blocklist = { 'local', 'require' } +--- elseif filetype == 'javascript' then +--- blocklist = { 'import' } +--- end +--- +--- vim.b.minicursorword_disable = vim.tbl_contains(blocklist, curword) +--- end +--- +--- -- Make sure to add this autocommand *before* calling module's `setup()`. +--- vim.cmd('au CursorMoved * lua _G.cursorword_blocklist()') + +-- Module definition ========================================================== +local MiniCursorword = {} +local H = {} + +--- Module setup +--- +---@param config table|nil Module config table. See |MiniCursorword.config|. +--- +---@usage `require('mini.cursorword').setup({})` (replace `{}` with your `config` table) +MiniCursorword.setup = function(config) + -- Export module + _G.MiniCursorword = MiniCursorword + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Define behavior + H.create_autocommands() + + -- Create default highlighting + H.create_default_hl() +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +MiniCursorword.config = { + -- Delay (in ms) between when cursor moved and when highlighting appeared + delay = 100, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= + +-- Helper data ================================================================ +-- Module default config +H.default_config = vim.deepcopy(MiniCursorword.config) + +-- Delay timer +H.timer = vim.loop.new_timer() + +-- Information about last match highlighting (stored *per window*): +-- - Key: windows' unique buffer identifiers. +-- - Value: table with: +-- - `id` field for match id (from `vim.fn.matchadd()`). +-- - `word` field for matched word. +H.window_matches = {} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {}) + + vim.validate({ delay = { config.delay, 'number' } }) + + return config +end + +H.apply_config = function(config) MiniCursorword.config = config end + +H.create_autocommands = function() + local augroup = vim.api.nvim_create_augroup('MiniCursorword', {}) + + local au = function(event, pattern, callback, desc) + vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc }) + end + + au('CursorMoved', '*', H.auto_highlight, 'Auto highlight cursorword') + au({ 'InsertEnter', 'TermEnter', 'QuitPre' }, '*', H.auto_unhighlight, 'Auto unhighlight cursorword') + au('ModeChanged', '*:[^i]', H.auto_highlight, 'Auto highlight cursorword') + + au('ColorScheme', '*', H.create_default_hl, 'Ensure proper colors') + au('FileType', 'TelescopePrompt', function() vim.b.minicursorword_disable = true end, 'Disable locally') +end + +--stylua: ignore +H.create_default_hl = function() + vim.api.nvim_set_hl(0, 'MiniCursorword', { default = true, underline = true }) + vim.api.nvim_set_hl(0, 'MiniCursorwordCurrent', { default = true, link = 'MiniCursorword' }) +end + +H.is_disabled = function() return vim.g.minicursorword_disable == true or vim.b.minicursorword_disable == true end + +H.get_config = function(config) + return vim.tbl_deep_extend('force', MiniCursorword.config, vim.b.minicursorword_config or {}, config or {}) +end + +-- Autocommands --------------------------------------------------------------- +H.auto_highlight = function() + -- Stop any possible previous delayed highlighting + H.timer:stop() + + -- Stop highlighting immediately if module is disabled when cursor is not on + -- 'keyword' + if not H.should_highlight() then return H.unhighlight() end + + -- Get current information + local win_id = vim.api.nvim_get_current_win() + local win_match = H.window_matches[win_id] or {} + local curword = H.get_cursor_word() + + -- Only immediately update highlighting of current word under cursor if + -- currently highlighted word equals one under cursor + if win_match.word == curword then + H.unhighlight(true) + H.highlight(true) + return + end + + -- Stop highlighting previous match (if it exists) + H.unhighlight() + + -- Delay highlighting + H.timer:start( + H.get_config().delay, + 0, + vim.schedule_wrap(function() + -- Ensure that always only one word is highlighted + H.unhighlight() + H.highlight() + end) + ) +end + +H.auto_unhighlight = function() + -- Stop any possible previous delayed highlighting + H.timer:stop() + H.unhighlight() +end + +-- Highlighting --------------------------------------------------------------- +---@param only_current boolean|nil Whether to forcefully highlight only current word +--- under cursor. +---@private +H.highlight = function(only_current) + -- A modified version of https://stackoverflow.com/a/25233145 + -- Using `matchadd()` instead of a simpler `:match` to tweak priority of + -- 'current word' highlighting: with `:match` it is higher than for + -- `incsearch` which is not convenient. + local win_id = vim.api.nvim_get_current_win() + if not vim.api.nvim_win_is_valid(win_id) then return end + + if not H.should_highlight() then return end + + H.window_matches[win_id] = H.window_matches[win_id] or {} + + -- Add match highlight for current word under cursor + local current_word_pattern = [[\k*\%#\k*]] + local match_id_current = vim.fn.matchadd('MiniCursorwordCurrent', current_word_pattern, -1) + H.window_matches[win_id].id_current = match_id_current + + -- Don't add main match id if not needed or if one is already present + if only_current or H.window_matches[win_id].id ~= nil then return end + + -- Add match highlight for non-current word under cursor. NOTEs: + -- - Using `\(...\)\@!` allows to not match current word. + -- - Using 'very nomagic' ('\V') allows not escaping. + -- - Using `\<` and `\>` matches whole word (and not as part). + local curword = H.get_cursor_word() + local pattern = string.format([[\(%s\)\@!\&\V\<%s\>]], current_word_pattern, curword) + local match_id = vim.fn.matchadd('MiniCursorword', pattern, -1) + + -- Store information about highlight + H.window_matches[win_id].id = match_id + H.window_matches[win_id].word = curword +end + +---@param only_current boolean|nil Whether to remove highlighting only of current +--- word under cursor. +---@private +H.unhighlight = function(only_current) + -- Don't do anything if there is no valid information to act upon + local win_id = vim.api.nvim_get_current_win() + local win_match = H.window_matches[win_id] + if not vim.api.nvim_win_is_valid(win_id) or win_match == nil then return end + + -- Use `pcall` because there is an error if match id is not present. It can + -- happen if something else called `clearmatches`. + pcall(vim.fn.matchdelete, win_match.id_current) + H.window_matches[win_id].id_current = nil + + if not only_current then + pcall(vim.fn.matchdelete, win_match.id) + H.window_matches[win_id] = nil + end +end + +H.should_highlight = function() return not H.is_disabled() and H.is_cursor_on_keyword() end + +H.is_cursor_on_keyword = function() + local col = vim.fn.col('.') + local curchar = vim.api.nvim_get_current_line():sub(col, col) + + -- Use `pcall()` to catch `E5108` (can happen in binary files, see #112) + local ok, match_res = pcall(vim.fn.match, curchar, '[[:keyword:]]') + return ok and match_res >= 0 +end + +H.get_cursor_word = function() return vim.fn.escape(vim.fn.expand(''), [[\/]]) end + +return MiniCursorword diff --git a/.config/nvim/lua/mini/hipatterns.lua b/.config/nvim/lua/mini/hipatterns.lua new file mode 100644 index 0000000..73b88b7 --- /dev/null +++ b/.config/nvim/lua/mini/hipatterns.lua @@ -0,0 +1,1021 @@ +--- *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: +--- -