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.
This commit is contained in:
Siddhant Agarwal
2025-04-03 19:56:56 +05:30
committed by GitHub
parent 974a3aa2c4
commit eae2d3b145
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
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*

View File

@ -104,7 +104,7 @@ The following new features were added.
API
todo
|vim.hl.range()| now allows multiple timed highlights
DEFAULTS

View File

@ -17,9 +17,6 @@ M.priorities = {
user = 200,
}
local range_timer --- @type uv.uv_timer_t?
local range_hl_clear --- @type fun()?
--- @class vim.hl.range.Opts
--- @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 finish integer[]|string End of region as a (line, column) tuple or string accepted by |getpos()|
---@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)
opts = opts or {}
local regtype = opts.regtype or 'v'
@ -108,38 +109,38 @@ function M.range(bufnr, ns, higroup, start, finish, opts)
end
end
if range_timer and not range_timer:is_closing() then
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
local extmarks = {} --- @type integer[]
for _, res in ipairs(region) do
local start_row = res[1][2] - 1
local start_col = res[1][3] - 1
local end_row = res[2][2] - 1
local end_col = res[2][3]
api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, {
hl_group = higroup,
end_row = end_row,
end_col = end_col,
priority = priority,
strict = false,
})
table.insert(
extmarks,
api.nvim_buf_set_extmark(bufnr, ns, start_row, start_col, {
hl_group = higroup,
end_row = end_row,
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
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
local yank_timer --- @type uv.uv_timer_t?
local yank_hl_clear --- @type fun()?
local yank_ns = api.nvim_create_namespace('nvim.hlyank')
--- 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 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 } })
M.range(bufnr, yank_ns, higroup, "'[", "']", {
yank_timer, yank_hl_clear = M.range(bufnr, yank_ns, higroup, "'[", "']", {
regtype = event.regtype,
inclusive = true,
priority = opts.priority or M.priorities.user,

View File

@ -139,6 +139,80 @@ describe('vim.hl.range', function()
|
]])
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)
describe('vim.hl.on_yank', function()