From 3ace049c6a4d0580a0bcb485a30e8613c8a6b149 Mon Sep 17 00:00:00 2001 From: "Ayose C." Date: Sun, 17 May 2026 13:56:37 +0000 Subject: [PATCH] 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(). --- runtime/doc/deprecated.txt | 4 +++ runtime/doc/lua.txt | 16 +++++---- runtime/doc/news.txt | 2 ++ runtime/example_init.lua | 4 +-- runtime/lua/vim/hl.lua | 61 ++++++++++++++++++++++++--------- test/functional/lua/hl_spec.lua | 55 ++++++++++++++++++++--------- 6 files changed, 101 insertions(+), 41 deletions(-) diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index 1d9efd91de..d9d165036c 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -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()| diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 2cb8ca1c5a..8314f5a768 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -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}) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index f984be6552..3b20154226 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/runtime/example_init.lua b/runtime/example_init.lua index 3ebe6f7dc7..511194a380 100644 --- a/runtime/example_init.lua +++ b/runtime/example_init.lua @@ -60,11 +60,11 @@ vim.keymap.set({ 'n' }, '', '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, }) diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua index 3a3e3b4edb..861d40402d 100644 --- a/runtime/lua/vim/hl.lua +++ b/runtime/lua/vim/hl.lua @@ -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 diff --git a/test/functional/lua/hl_spec.lua b/test/functional/lua/hl_spec.lua index a63b004456..8eaaa6294e 100644 --- a/test/functional/lua/hl_spec.lua +++ b/test/functional/lua/hl_spec.lua @@ -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') -- 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)