feat(vim.hl): allow multiple timed highlights simultaneously #33283

Problem: Currently vim.hl.range only allows one timed highlight.
Creating another one, removes the old one.

Solution: vim.hl.range now returns a timer and a function. The timer
keeps track of how much time is left in the highlight and the function
allows you to clear it, letting the user decide what to do with old
highlights.

(cherry picked from commit eae2d3b145)
This commit is contained in:
Siddhant Agarwal
2025-04-03 19:56:56 +05:30
committed by siddhantdev
parent dd547ef1ea
commit 5829b5de0a
4 changed files with 113 additions and 26 deletions

View File

@@ -640,6 +640,12 @@ vim.hl.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {opts})
• {timeout}? (`integer`, default: -1 no timeout) Time in ms • {timeout}? (`integer`, default: -1 no timeout) Time in ms
before highlight is cleared before highlight is cleared
Return (multiple): ~
(`uv.uv_timer_t?`) range_timer A timer which manages how much time the
highlight has left
(`fun()?`) range_clear A function which allows clearing the highlight
manually. nil is returned if timeout is not specified
============================================================================== ==============================================================================
VIM.DIFF *vim.diff* VIM.DIFF *vim.diff*

View File

@@ -176,7 +176,7 @@ API
aligned text that truncates before covering up buffer text. aligned text that truncates before covering up buffer text.
• `virt_lines_overflow` field accepts value `scroll` to enable horizontal • `virt_lines_overflow` field accepts value `scroll` to enable horizontal
scrolling for virtual lines with 'nowrap'. scrolling for virtual lines with 'nowrap'.
• |vim.hl.range()| now has a optional `timeout` field which allows for a timed highlight • |vim.hl.range()| now has a optional `timeout` field which allows for multiple timed highlights
DEFAULTS DEFAULTS

View File

@@ -17,9 +17,6 @@ M.priorities = {
user = 200, user = 200,
} }
local range_timer --- @type uv.uv_timer_t?
local range_hl_clear --- @type fun()?
--- @class vim.hl.range.Opts --- @class vim.hl.range.Opts
--- @inlinedoc --- @inlinedoc
--- ---
@@ -47,6 +44,10 @@ local range_hl_clear --- @type fun()?
---@param start integer[]|string Start of region as a (line, column) tuple or string accepted by |getpos()| ---@param start integer[]|string Start of region as a (line, column) tuple or string accepted by |getpos()|
---@param finish integer[]|string End of region as a (line, column) tuple or string accepted by |getpos()| ---@param finish integer[]|string End of region as a (line, column) tuple or string accepted by |getpos()|
---@param opts? vim.hl.range.Opts ---@param opts? vim.hl.range.Opts
--- @return uv.uv_timer_t? range_timer A timer which manages how much time the
--- highlight has left
--- @return fun()? range_clear A function which allows clearing the highlight manually.
--- nil is returned if timeout is not specified
function M.range(bufnr, ns, higroup, start, finish, opts) function M.range(bufnr, ns, higroup, start, finish, opts)
opts = opts or {} opts = opts or {}
local regtype = opts.regtype or 'v' local regtype = opts.regtype or 'v'
@@ -108,38 +109,38 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
end end
end end
if range_timer and not range_timer:is_closing() then local extmarks = {} --- @type integer[]
range_timer:close()
assert(range_hl_clear)
range_hl_clear()
end
range_hl_clear = function()
range_timer = nil
range_hl_clear = nil
pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns, 0, -1)
pcall(vim.api.nvim__ns_set, { wins = {} })
end
for _, res in ipairs(region) do for _, res in ipairs(region) do
local start_row = res[1][2] - 1 local start_row = res[1][2] - 1
local start_col = res[1][3] - 1 local start_col = res[1][3] - 1
local end_row = res[2][2] - 1 local end_row = res[2][2] - 1
local end_col = res[2][3] local end_col = res[2][3]
api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, { table.insert(
hl_group = higroup, extmarks,
end_row = end_row, api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, {
end_col = end_col, hl_group = higroup,
priority = priority, end_row = end_row,
strict = false, end_col = end_col,
}) priority = priority,
strict = false,
})
)
end
local range_hl_clear = function()
for _, mark in ipairs(extmarks) do
api.nvim_buf_del_extmark(bufnr, ns, mark)
end
end end
if timeout ~= -1 then if timeout ~= -1 then
range_timer = vim.defer_fn(range_hl_clear, timeout) local range_timer = vim.defer_fn(range_hl_clear, timeout)
return range_timer, range_hl_clear
end end
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') local yank_ns = api.nvim_create_namespace('nvim.hlyank')
--- Highlight the yanked text during a |TextYankPost| event. --- Highlight the yanked text during a |TextYankPost| event.
@@ -179,8 +180,14 @@ function M.on_yank(opts)
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local winid = vim.api.nvim_get_current_win() local winid = vim.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()
end
vim.api.nvim__ns_set(yank_ns, { wins = { winid } }) vim.api.nvim__ns_set(yank_ns, { wins = { winid } })
M.range(bufnr, yank_ns, higroup, "'[", "']", { yank_timer, yank_hl_clear = M.range(bufnr, yank_ns, higroup, "'[", "']", {
regtype = event.regtype, regtype = event.regtype,
inclusive = true, inclusive = true,
priority = opts.priority or M.priorities.user, priority = opts.priority or M.priorities.user,

View File

@@ -139,6 +139,80 @@ describe('vim.hl.range', function()
| |
]]) ]])
end) end)
it('shows multiple highlights with different timeouts simultaneously', function()
local timeout1 = 300
local timeout2 = 600
exec_lua(function()
local ns = vim.api.nvim_create_namespace('')
vim.hl.range(0, ns, 'Search', { 0, 0 }, { 4, 0 }, { timeout = timeout1 })
vim.hl.range(0, ns, 'Search', { 2, 6 }, { 3, 5 }, { timeout = timeout2 })
end)
screen:expect({
grid = [[
{10:^asdfghjkl}{100:$} |
{10:«口=口»}{100:$} |
{10:qwertyuiop}{100:$} |
{10:口口=口口}{1:$} |
zxcvbnm{1:$} |
|
]],
timeout = timeout1 / 3,
})
screen:expect({
grid = [[
^asdfghjkl{1:$} |
«口=口»{1:$} |
qwerty{10:uiop}{100:$} |
{10:口口}=口口{1:$} |
zxcvbnm{1:$} |
|
]],
timeout = timeout1 + ((timeout2 - timeout1) / 3),
})
screen:expect([[
^asdfghjkl{1:$} |
«口=口»{1:$} |
qwertyuiop{1:$} |
口口=口口{1:$} |
zxcvbnm{1:$} |
|
]])
end)
it('allows cancelling a highlight that has not timed out', function()
exec_lua(function()
local timeout = 3000
local range_timer
local range_hl_clear
local ns = vim.api.nvim_create_namespace('')
range_timer, range_hl_clear = vim.hl.range(
0,
ns,
'Search',
{ 0, 0 },
{ 4, 0 },
{ timeout = timeout }
)
if range_timer and not range_timer:is_closing() then
range_timer:close()
assert(range_hl_clear)
range_hl_clear()
range_hl_clear() -- Exercise redundant call
end
end)
screen:expect({
grid = [[
^asdfghjkl{1:$} |
«口=口»{1:$} |
qwertyuiop{1:$} |
口口=口口{1:$} |
zxcvbnm{1:$} |
|
]],
unchanged = true,
})
end)
end) end)
describe('vim.hl.on_yank', function() describe('vim.hl.on_yank', function()