288 lines
10 KiB
Lua
288 lines
10 KiB
Lua
--- *mini.cursorword* Autohighlight word under cursor
|
|
--- *MiniCursorword*
|
|
---
|
|
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
|
|
---
|
|
--- ==============================================================================
|
|
---
|
|
--- Features:
|
|
--- - Autohighlight word under cursor with customizable delay.
|
|
---
|
|
--- - Current word under cursor can be highlighted differently.
|
|
---
|
|
--- - Highlighting is triggered only if current cursor character is a |[:keyword:]|.
|
|
---
|
|
--- - Highlighting stops in insert and terminal modes.
|
|
---
|
|
--- - "Word under cursor" is meant as in Vim's |<cword>|: something user would
|
|
--- get as 'iw' text object.
|
|
---
|
|
--- # Setup~
|
|
---
|
|
--- This module needs a setup with `require('mini.cursorword').setup({})`
|
|
--- (replace `{}` with your `config` table). It will create global Lua table
|
|
--- `MiniCursorword` which you can use for scripting or manually (with
|
|
--- `:lua MiniCursorword.*`).
|
|
---
|
|
--- See |MiniCursorword.config| for `config` structure and default values.
|
|
---
|
|
--- You can override runtime config settings locally to buffer inside
|
|
--- `vim.b.minicursorword_config` which should have same structure as
|
|
--- `MiniCursorword.config`. See |mini.nvim-buffer-local-config| for more details.
|
|
---
|
|
--- # Highlight groups~
|
|
---
|
|
--- * `MiniCursorword` - highlight group of a non-current cursor word.
|
|
--- Default: plain underline.
|
|
---
|
|
--- * `MiniCursorwordCurrent` - highlight group of a current word under cursor.
|
|
--- Default: links to `MiniCursorword` (so `:hi clear MiniCursorwordCurrent`
|
|
--- will lead to showing `MiniCursorword` highlight group).
|
|
--- Note: To not highlight it, use
|
|
---
|
|
--- `: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('<cword>')
|
|
--- local filetype = vim.bo.filetype
|
|
---
|
|
--- -- Add any disabling global or filetype-specific logic here
|
|
--- local blocklist = {}
|
|
--- if filetype == 'lua' then
|
|
--- blocklist = { 'local', 'require' }
|
|
--- elseif filetype == 'javascript' then
|
|
--- blocklist = { 'import' }
|
|
--- end
|
|
---
|
|
--- vim.b.minicursorword_disable = vim.tbl_contains(blocklist, curword)
|
|
--- end
|
|
---
|
|
--- -- Make sure to add this autocommand *before* calling module's `setup()`.
|
|
--- vim.cmd('au CursorMoved * lua _G.cursorword_blocklist()')
|
|
|
|
-- Module definition ==========================================================
|
|
local MiniCursorword = {}
|
|
local H = {}
|
|
|
|
--- Module setup
|
|
---
|
|
---@param config table|nil Module config table. See |MiniCursorword.config|.
|
|
---
|
|
---@usage `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('<cword>'), [[\/]]) end
|
|
|
|
return MiniCursorword
|