neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit eae2d3b145c724e8c801c7c22a2ccc3bde5534eb
parent 974a3aa2c4c34f97ce015466a7dfc5fd727fccc2
Author: Siddhant Agarwal <68201519+siddhantdev@users.noreply.github.com>
Date:   Thu,  3 Apr 2025 19:56:56 +0530

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.
Diffstat:
Mruntime/doc/lua.txt | 6++++++
Mruntime/doc/news.txt | 2+-
Mruntime/lua/vim/hl.lua | 57++++++++++++++++++++++++++++++++-------------------------
Mtest/functional/lua/hl_spec.lua | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 113 insertions(+), 26 deletions(-)

diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt @@ -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* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt @@ -104,7 +104,7 @@ The following new features were added. API -• todo +• |vim.hl.range()| now allows multiple timed highlights DEFAULTS diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua @@ -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, diff --git a/test/functional/lua/hl_spec.lua b/test/functional/lua/hl_spec.lua @@ -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()