feat(vim.hl): vim.hl.hl_op() #39777

Problem:
vim.hl.on_yank() only works for TextYankPost, not TextPutPost.

Solution:
Introduce hl_op().
Deprecate on_yank().
This commit is contained in:
Ayose C.
2026-05-17 13:56:37 +00:00
committed by GitHub
parent 8e1d6dfada
commit 3ace049c6a
6 changed files with 101 additions and 41 deletions

View File

@@ -27,6 +27,10 @@ API
EVENTS
• *BufModifiedSet* Use |OptionSet| with pattern "modified" instead.
HIGHLIGHTS
• vim.hl.on_yank() Use |vim.hl.hl_op()| instead.
LUA
• vim.F.if_nil() Renamed to |vim.nonnil()|

View File

@@ -2954,21 +2954,23 @@ vim.glob.to_lpeg({pattern}) *vim.glob.to_lpeg()*
==============================================================================
Lua module: vim.hl *vim.hl*
vim.hl.on_yank({opts}) *vim.hl.on_yank()*
Highlight the yanked text during a |TextYankPost| event.
vim.hl.hl_op({opts}) *vim.hl.hl_op()*
Highlight the related text region during a |TextYankPost| or |TextPutPost|
event.
Add the following to your `init.vim`: >vim
autocmd TextYankPost * silent! lua vim.hl.on_yank {higroup='Visual', timeout=300}
autocmd TextYankPost * silent! lua vim.hl.hl_op {higroup='Visual', timeout=300}
autocmd TextPutPost * silent! lua vim.hl.hl_op {higroup='Visual', timeout=300}
<
Parameters: ~
• {opts} (`table?`) Optional parameters
• event event structure (default vim.v.event)
• higroup highlight group for yanked region (default
• higroup highlight group for the text region (default
"IncSearch")
• on_macro highlight when executing macro (default false)
• on_visual highlight when yanking visual selection (default
true)
• on_visual highlight when the event is in |Visual| mode
(default true)
• priority integer priority (default
|vim.hl.priorities|`.user`)
• timeout time in ms before highlight is cleared (default 150)
@@ -2980,7 +2982,7 @@ vim.hl.priorities *vim.hl.priorities*
• `semantic_tokens`: `125`, used for LSP semantic token highlighting
• `diagnostics`: `150`, used for code analysis such as diagnostics
• `user`: `200`, used for user-triggered highlights such as LSP document
symbols or `on_yank` autocommands
symbols or `hl_op` autocommands
*vim.hl.range()*
vim.hl.range({buf}, {ns}, {higroup}, {start}, {finish}, {opts})

View File

@@ -153,6 +153,8 @@ EVENTS
HIGHLIGHTS
• `Dimmed` for text that should be de-emphasized.
• |vim.hl.hl_op()| highlights text regions for |TextYankPost| and
|TextPutPost| events. It replaces the now deprecated `vim.hl.on_yank()`.
LSP

View File

@@ -60,11 +60,11 @@ vim.keymap.set({ 'n' }, '<A-l>', '<C-w>l')
-- See `:h lua-guide-autocommands`, `:h autocmd`, `:h nvim_create_autocmd()`
-- Highlight when yanking (copying) text.
-- Try it with `yap` in normal mode. See `:h vim.hl.on_yank()`
-- Try it with `yap` in normal mode. See `:h vim.hl.hl_op()`
vim.api.nvim_create_autocmd('TextYankPost', {
desc = 'Highlight when yanking (copying) text',
callback = function()
vim.hl.on_yank()
vim.hl.hl_op()
end,
})

View File

@@ -8,7 +8,7 @@ local M = {}
--- - `semantic_tokens`: `125`, used for LSP semantic token highlighting
--- - `diagnostics`: `150`, used for code analysis such as diagnostics
--- - `user`: `200`, used for user-triggered highlights such as LSP document
--- symbols or `on_yank` autocommands
--- symbols or `hl_op` autocommands
M.priorities = {
syntax = 50,
treesitter = 100,
@@ -144,26 +144,34 @@ function M.range(buf, ns, higroup, start, finish, opts)
end
end
local yank_timer --- @type uv.uv_timer_t?
local yank_hl_clear --- @type fun()?
local yank_ns = api.nvim_create_namespace('nvim.hlyank')
---@private
---@class (private) vim.hl.OnEventState
---@field timer? uv.uv_timer_t Timer to clear the highlight.
---@field clear? fun() Function to clear the highlight immediately.
--- Highlight the yanked text during a |TextYankPost| event.
--- @type { [string]: vim.hl.OnEventState }
local hl_op_state = {}
local events_ns = api.nvim_create_namespace('nvim.hl.events')
--- Highlight the related text region during a |TextYankPost| or |TextPutPost|
--- event.
---
--- Add the following to your `init.vim`:
---
--- ```vim
--- autocmd TextYankPost * silent! lua vim.hl.on_yank {higroup='Visual', timeout=300}
--- autocmd TextYankPost * silent! lua vim.hl.hl_op {higroup='Visual', timeout=300}
--- autocmd TextPutPost * silent! lua vim.hl.hl_op {higroup='Visual', timeout=300}
--- ```
---
--- @param opts table|nil Optional parameters
--- - event event structure (default vim.v.event)
--- - higroup highlight group for yanked region (default "IncSearch")
--- - higroup highlight group for the text region (default "IncSearch")
--- - on_macro highlight when executing macro (default false)
--- - on_visual highlight when yanking visual selection (default true)
--- - on_visual highlight when the event is in |Visual| mode (default true)
--- - priority integer priority (default |vim.hl.priorities|`.user`)
--- - timeout time in ms before highlight is cleared (default 150)
function M.on_yank(opts)
function M.hl_op(opts)
vim.validate('opts', opts, 'table', true)
opts = opts or {}
local event = opts.event or vim.v.event
@@ -173,31 +181,52 @@ function M.on_yank(opts)
if not on_macro and vim.fn.reg_executing() ~= '' then
return
end
if event.operator ~= 'y' or event.regtype == '' then
if event.regtype == '' then
return
end
if not on_visual and event.visual then
return
end
local state_key --- @type string
if event.operator == 'y' then
state_key = 'yank'
elseif event.operator == 'p' or event.operator == 'P' then
state_key = 'put'
else
return
end
local higroup = opts.higroup or 'IncSearch'
local bufnr = api.nvim_get_current_buf()
local winid = api.nvim_get_current_win()
if yank_timer and not yank_timer:is_closing() then
yank_timer:close()
assert(yank_hl_clear)
yank_hl_clear()
local state = hl_op_state[state_key]
if state ~= nil and state.timer and not state.timer:is_closing() then
state.timer:close()
assert(state.clear)
state.clear()
end
api.nvim__ns_set(yank_ns, { wins = { winid } })
yank_timer, yank_hl_clear = M.range(bufnr, yank_ns, higroup, "'[", "']", {
api.nvim__ns_set(events_ns, { wins = { winid } })
local timer, clear = M.range(bufnr, events_ns, higroup, "'[", "']", {
regtype = event.regtype,
inclusive = true,
priority = opts.priority or M.priorities.user,
timeout = opts.timeout or 150,
})
hl_op_state[state_key] = {
timer = timer,
clear = clear,
}
end
--- @deprecated Please use |vim.hl.hl_op()| instead.
function M.on_yank(opts)
vim.deprecate('vim.hl.on_yank', 'vim.hl.hl_op', '0.13')
return M.hl_op(opts)
end
return M

View File

@@ -215,7 +215,7 @@ describe('vim.hl.range', function()
end)
end)
describe('vim.hl.on_yank', function()
describe('vim.hl.hl_op', function()
before_each(function()
clear()
end)
@@ -224,7 +224,7 @@ describe('vim.hl.on_yank', function()
command('new')
n.feed('ifoo<esc>') -- set '[, ']
exec_lua(function()
vim.hl.on_yank({
vim.hl.hl_op({
timeout = 10,
on_macro = true,
event = { operator = 'y', regtype = 'v' },
@@ -238,10 +238,10 @@ describe('vim.hl.on_yank', function()
it('does not close timer twice', function()
exec_lua(function()
vim.hl.on_yank({ timeout = 10, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = 10, on_macro = true, event = { operator = 'y' } })
vim.uv.sleep(10)
vim.schedule(function()
vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = 0, on_macro = true, event = { operator = 'y' } })
end)
end)
eq('', eval('v:errmsg'))
@@ -252,16 +252,16 @@ describe('vim.hl.on_yank', function()
exec_lua(function()
vim.api.nvim_buf_set_mark(0, '[', 1, 1, {})
vim.api.nvim_buf_set_mark(0, ']', 1, 1, {})
vim.hl.on_yank({ timeout = math.huge, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = math.huge, on_macro = true, event = { operator = 'y' } })
end)
local ns = api.nvim_create_namespace('nvim.hlyank')
local ns = api.nvim_create_namespace('nvim.hl.events')
local win = api.nvim_get_current_win()
eq({ win }, api.nvim__ns_get(ns).wins)
command('wincmd w')
eq({ win }, api.nvim__ns_get(ns).wins)
-- Use a new vim.hl.on_yank() call to cancel the previous timer
-- Use a new vim.hl.hl_op() call to cancel the previous timer
exec_lua(function()
vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = 0, on_macro = true, event = { operator = 'y' } })
end)
end)
@@ -270,23 +270,23 @@ describe('vim.hl.on_yank', function()
exec_lua(function()
vim.api.nvim_buf_set_mark(0, '[', 1, 1, {})
vim.api.nvim_buf_set_mark(0, ']', 1, 1, {})
vim.hl.on_yank({ timeout = math.huge, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = math.huge, on_macro = true, event = { operator = 'y' } })
end)
local ns = api.nvim_create_namespace('nvim.hlyank')
local ns = api.nvim_create_namespace('nvim.hl.events')
eq(api.nvim_get_current_win(), api.nvim__ns_get(ns).wins[1])
command('wincmd w')
exec_lua(function()
vim.api.nvim_buf_set_mark(0, '[', 1, 1, {})
vim.api.nvim_buf_set_mark(0, ']', 1, 1, {})
vim.hl.on_yank({ timeout = math.huge, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = math.huge, on_macro = true, event = { operator = 'y' } })
end)
local win = api.nvim_get_current_win()
eq({ win }, api.nvim__ns_get(ns).wins)
command('wincmd w')
eq({ win }, api.nvim__ns_get(ns).wins)
-- Use a new vim.hl.on_yank() call to cancel the previous timer
-- Use a new vim.hl.hl_op() call to cancel the previous timer
exec_lua(function()
vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = 0, on_macro = true, event = { operator = 'y' } })
end)
end)
@@ -295,7 +295,7 @@ describe('vim.hl.on_yank', function()
screen:add_extra_attr_ids({
[100] = { foreground = Screen.colors.Blue, background = Screen.colors.Yellow, bold = true },
})
command('autocmd TextYankPost * lua vim.hl.on_yank{timeout=100000}')
command('autocmd TextYankPost * lua vim.hl.hl_op{timeout=100000}')
api.nvim_buf_set_lines(0, 0, -1, true, {
[[foo(bar) 'baz']],
[[foo(bar) 'baz']],
@@ -321,9 +321,32 @@ describe('vim.hl.on_yank', function()
{1:~ }|
|
]])
-- Use a new vim.hl.on_yank() call to cancel the previous timer
-- Use a new vim.hl.hl_op() call to cancel the previous timer
exec_lua(function()
vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } })
vim.hl.hl_op({ timeout = 0, on_macro = true, event = { operator = 'y' } })
end)
end)
it('highlights TextPutPost', function()
local screen = Screen.new(60, 4)
screen:add_extra_attr_ids({
[100] = { foreground = Screen.colors.Blue, background = Screen.colors.Yellow, bold = true },
})
command('autocmd TextPutPost * lua vim.hl.hl_op{timeout=100000}')
api.nvim_buf_set_lines(0, 0, -1, true, {
[[foo(bar) '1']],
[[foo(bar) '2']],
})
n.feed('ggyyp')
screen:expect([[
foo(bar) '1' |
{2:^foo(bar) '1'} |
foo(bar) '2' |
|
]])
-- Use a new vim.hl.hl_op() call to cancel the previous timer
exec_lua(function()
vim.hl.hl_op({ timeout = 0, on_macro = true, event = { operator = 'p' } })
end)
end)
end)