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..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 a timed highlight 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 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..f17442912a 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() @@ -184,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) @@ -209,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) @@ -246,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)