From 5829b5de0a38c324c3672434d01d55084751bef5 Mon Sep 17 00:00:00 2001 From: Siddhant Agarwal <68201519+siddhantdev@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:56:56 +0530 Subject: [PATCH 1/3] 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 eae2d3b145c724e8c801c7c22a2ccc3bde5534eb) --- runtime/doc/lua.txt | 6 +++ runtime/doc/news.txt | 2 +- runtime/lua/vim/hl.lua | 57 ++++++++++++++----------- test/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 index be118cf790..c580c55a5e 100644 --- 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 index 0afa27046e..a44195f206 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -176,7 +176,7 @@ API aligned text that truncates before covering up buffer text. • `virt_lines_overflow` field accepts value `scroll` to enable horizontal 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 diff --git a/runtime/lua/vim/hl.lua b/runtime/lua/vim/hl.lua index a2b06c9727..d15ee1fd10 100644 --- 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 index 12be01e0a5..e3c6c706c7 100644 --- 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() From 1a2d0484accbef28b97188d355220f1bda458eb2 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 4 Apr 2025 09:25:14 +0800 Subject: [PATCH 2/3] docs: news.txt --- runtime/doc/news.txt | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index a44195f206..fc9c5f7108 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -119,10 +119,6 @@ OPTIONS • Setting |hidden-options| now gives an error. In particular, setting 'noshellslash' is now only allowed on Windows. -PLUGINS - -• TODO - TREESITTER • |Query:iter_matches()| correctly returns all matching nodes in a match @@ -136,12 +132,11 @@ TREESITTER if no languages are explicitly registered. • |vim.treesitter.language.add()| returns `true` if a parser was loaded successfully and `nil,errmsg` otherwise instead of throwing an error. -• |vim.treesitter.get_parser()| and |vim.treesitter.start()| no longer parse - the tree before returning. Scripts must call |LanguageTree:parse()| explicitly. >lua +• |vim.treesitter.get_parser()| and |vim.treesitter.start()| no longer parse the + tree before returning. Scripts must call |LanguageTree:parse()| explicitly. >lua local p = vim.treesitter.get_parser(0, 'c') p:parse() • |vim.treesitter.get_parser()| expects its buffer to be loaded. -< TUI @@ -176,7 +171,6 @@ API aligned text that truncates before covering up buffer text. • `virt_lines_overflow` field accepts value `scroll` to enable horizontal scrolling for virtual lines with 'nowrap'. -• |vim.hl.range()| now has a optional `timeout` field which allows for multiple timed highlights DEFAULTS @@ -242,8 +236,7 @@ EDITOR • |hl-ComplMatchIns| shows matched text of the currently inserted completion. • |hl-PmenuMatch| and |hl-PmenuMatchSel| show matched text in completion popup. • |gO| now works in `help`, `checkhealth`, and `markdown` buffers. -• Jump between sections in `help` and `checkhealth` buffers with `[[` and - `]]`. +• Jump between sections in `help` and `checkhealth` buffers with `[[` and `]]`. EVENTS @@ -302,6 +295,8 @@ LUA • |vim.fs.relpath()| gets relative path compared to base path. • |vim.fs.dir()| and |vim.fs.find()| can now follow symbolic links, the behavior can be turn on using the new `follow` option. +• |vim.hl.range()| now has a optional `timeout` field which allows for multiple + timed highlights. • |vim.text.indent()| indents/dedents text. OPTIONS @@ -312,10 +307,8 @@ OPTIONS • 'messagesopt' configures |:messages| and |hit-enter| prompt. • 'tabclose' controls which tab page to focus when closing a tab page. • 'eventignorewin' to persistently ignore events in a window. -• 'winborder' sets the default border for |floating-windows| - -• 'winborder' add bold style. - +• 'winborder' sets the default border for |floating-windows|. +• 'winborder' "bold" style. • |g:clipboard| accepts a string name to force any builtin clipboard tool. PERFORMANCE From c6ef13dc45fbfa12cbde461fc90cc5d6ca36c563 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 4 Apr 2025 09:19:13 +0800 Subject: [PATCH 3/3] test(lua/hl_spec): fix hang on exit with ASAN (#33298) --- test/functional/lua/hl_spec.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/functional/lua/hl_spec.lua b/test/functional/lua/hl_spec.lua index e3c6c706c7..f17442912a 100644 --- a/test/functional/lua/hl_spec.lua +++ b/test/functional/lua/hl_spec.lua @@ -258,9 +258,9 @@ describe('vim.hl.on_yank', function() eq({ win }, api.nvim__ns_get(ns).wins) command('wincmd w') eq({ win }, api.nvim__ns_get(ns).wins) - -- Use a new vim.hl.range() call to cancel the previous timer + -- Use a new vim.hl.on_yank() call to cancel the previous timer exec_lua(function() - vim.hl.range(0, ns, 'Search', { 0, 0 }, { 0, 0 }, { timeout = 0 }) + vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } }) end) end) @@ -283,9 +283,9 @@ describe('vim.hl.on_yank', function() eq({ win }, api.nvim__ns_get(ns).wins) command('wincmd w') eq({ win }, api.nvim__ns_get(ns).wins) - -- Use a new vim.hl.range() call to cancel the previous timer + -- Use a new vim.hl.on_yank() call to cancel the previous timer exec_lua(function() - vim.hl.range(0, ns, 'Search', { 0, 0 }, { 0, 0 }, { timeout = 0 }) + vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } }) end) end) @@ -320,5 +320,9 @@ describe('vim.hl.on_yank', function() {1:~ }| | ]]) + -- Use a new vim.hl.on_yank() call to cancel the previous timer + exec_lua(function() + vim.hl.on_yank({ timeout = 0, on_macro = true, event = { operator = 'y' } }) + end) end) end)