From 523e679ead65e57af8159b7bba49da98251df44e Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Wed, 6 May 2026 17:25:39 +0800 Subject: [PATCH 1/2] refactor(matchparen): rewrite matchparen plugin in Lua AI-assisted: Codex --- runtime/lua/nvim/matchparen.lua | 297 ++++++++++++++++++++++++++++++++ runtime/plugin/matchparen.lua | 5 + runtime/plugin/matchparen.vim | 229 +----------------------- 3 files changed, 303 insertions(+), 228 deletions(-) create mode 100644 runtime/lua/nvim/matchparen.lua create mode 100644 runtime/plugin/matchparen.lua diff --git a/runtime/lua/nvim/matchparen.lua b/runtime/lua/nvim/matchparen.lua new file mode 100644 index 0000000000..67fabf0ef5 --- /dev/null +++ b/runtime/lua/nvim/matchparen.lua @@ -0,0 +1,297 @@ +local api = vim.api +local fn = vim.fn + +local M = {} + +local skip_names = { 'string', 'character', 'singlequote', 'escape', 'symbol', 'comment' } + +---@param text string +---@param byte_col integer +---@return string +local function char_at(text, byte_col) + if byte_col > #text then + return '' + end + return text:sub(byte_col, byte_col + vim.str_utf_end(text, byte_col)) +end + +---@param text string +---@param byte_col integer +---@return string +local function char_before(text, byte_col) + local end_col = math.min(byte_col - 1, #text) + if end_col < 1 then + return '' + end + return text:sub(end_col + vim.str_utf_start(text, end_col), end_col) +end + +function M.enable() + vim.g.loaded_matchparen = 1 + + if vim.g.matchparen_timeout == nil then + vim.g.matchparen_timeout = 300 + end + if vim.g.matchparen_insert_timeout == nil then + vim.g.matchparen_insert_timeout = 60 + end + if vim.g.matchparen_disable_cursor_hl == nil then + vim.g.matchparen_disable_cursor_hl = 0 + end + + local group = api.nvim_create_augroup('matchparen', { clear = true }) + + -- Replace all matchparen autocommands + api.nvim_create_autocmd({ + 'CursorMoved', + 'CursorMovedI', + 'WinEnter', + 'WinScrolled', + 'TextChanged', + 'TextChangedI', + }, { + group = group, + callback = M.highlight_matching_pair, + }) + api.nvim_create_autocmd('BufWinEnter', { + group = group, + callback = function() + api.nvim_create_autocmd('SafeState', { + group = group, + once = true, + callback = M.highlight_matching_pair, + }) + end, + }) + api.nvim_create_autocmd({ + 'WinLeave', + 'BufLeave', + 'TextChangedP', + }, { + group = group, + callback = M.remove_matches, + }) + + -- Define commands that will disable and enable the plugin. + api.nvim_create_user_command('DoMatchParen', M.do_matchparen, { force = true }) + api.nvim_create_user_command('NoMatchParen', M.no_matchparen, { force = true }) +end + +--- The function that is invoked (very often) to define a ":match" highlighting +--- for any matching paren. +function M.highlight_matching_pair() + if vim.w.matchparen_ids == nil then + vim.w.matchparen_ids = {} + end + -- Remove any previous match. + M.remove_matches() + + -- Avoid that we remove the popup menu. + if fn.pumvisible() ~= 0 then + return + end + + -- Get the character under the cursor and check if it's in 'matchpairs'. + local cursor = api.nvim_win_get_cursor(0) + local c_lnum = cursor[1] + local c_col = cursor[2] + 1 + local before = 0 + + local text = api.nvim_buf_get_lines(0, c_lnum - 1, c_lnum, false)[1] or '' + -- Cursor columns are byte indexes, + -- while 'matchpairs' entries may be multibyte characters. + -- Use UTF-8 boundaries to extract the whole character around the byte column. + local c_before = char_before(text, c_col) + local c = char_at(text, c_col) + ---@type string[] + local plist = vim.split(vim.o.matchpairs, '[:,]', { trimempty = true }) + ---@type integer? + local i = vim.iter(ipairs(plist)):find(function(_, item) + return item == c + end) + if i == nil then + -- not found, in Insert mode try character before the cursor + local mode = api.nvim_get_mode().mode + if c_col > 1 and (mode == 'i' or mode == 'R') then + before = #c_before + c = c_before + i = vim.iter(ipairs(plist)):find(function(_, item) + return item == c + end) + end + if i == nil then + -- not found, nothing to do + return + end + end + + -- Figure out the arguments for searchpairpos(). + local flags ---@type string + local c2 ---@type string + if i % 2 == 1 then + flags = 'nW' + c2 = plist[i + 1] + else + flags = 'nbW' + c2 = c + c = plist[i - 1] + end + if c == '[' then + c = [=[\[]=] + c2 = [=[\]]=] + end + + -- Find the match. When it was just before the cursor move it there for a + -- moment. + local save_cursor ---@type [integer, integer, integer, integer, integer]? + if before > 0 then + save_cursor = fn.getcurpos() + api.nvim_win_set_cursor(0, { c_lnum, c_col - before - 1 }) + end + + local skip ---@type fun(): boolean + if vim.g.syntax_on == nil then + skip = function() + return false + end + elseif vim.b.ts_highlight ~= nil and vim.o.syntax ~= 'on' then + skip = function() + for _, capture in ipairs(vim.treesitter.get_captures_at_cursor()) do + for _, skip_name in ipairs(skip_names) do + if capture:find(skip_name, 1, true) ~= nil then + return true + end + end + end + return false + end + else + -- do not attempt to match when the syntax item where the cursor is + -- indicates there does not exist a matching parenthesis, e.g. for shells + -- case statement: "case $var in foobar)" + -- + -- add the check behind a filetype check, so it only needs to be + -- evaluated for certain filetypes + if vim.o.filetype == 'sh' then + local pos = api.nvim_win_get_cursor(0) + for _, id in ipairs(fn.synstack(pos[1], pos[2] + 1)) do + if fn.synIDattr(id, 'name'):lower():find('shsnglcase') ~= nil then + if save_cursor ~= nil then + fn.setpos('.', save_cursor) + end + return + end + end + end + -- Build an expression that detects whether the current cursor position is + -- in certain syntax types (string, comment, etc.), for use as + -- searchpairpos()'s skip argument. + -- We match "escape" for special items, such as lispEscapeSpecial, and + -- match "symbol" for lispBarSymbol. + skip = function() + local pos = api.nvim_win_get_cursor(0) + for _, id in ipairs(fn.synstack(pos[1], pos[2] + 1)) do + local name = fn.synIDattr(id, 'name'):lower() + for _, skip_name in ipairs(skip_names) do + if name:find(skip_name, 1, true) ~= nil then + return true + end + end + end + return false + end + -- If executing the expression determines that the cursor is currently in + -- one of the syntax types, then we want searchpairpos() to find the pair + -- within those syntax types (i.e., not skip). Otherwise, the cursor is + -- outside of the syntax types and skip should keep its value so we skip + -- any matching pair inside the syntax types. + if skip() then + skip = function() + return false + end + end + end + + -- Limit the search to lines visible in the window. + local stoplinebottom = fn.line('w$') + local stoplinetop = fn.line('w0') + local stopline ---@type integer + if i % 2 == 1 then + stopline = stoplinebottom + else + stopline = stoplinetop + end + + -- Limit the search time to 300 msec to avoid a hang on very long lines. + local timeout ---@type integer + local mode = api.nvim_get_mode().mode + if mode == 'i' or mode == 'R' then + timeout = vim.b.matchparen_insert_timeout or vim.g.matchparen_insert_timeout + else + timeout = vim.b.matchparen_timeout or vim.g.matchparen_timeout + end + + ---@type boolean, [integer, integer]|string + local ok, match = pcall(fn.searchpairpos, c, '', c2, flags, skip, stopline, timeout) + if not ok then ---@cast match string + if save_cursor ~= nil then + fn.setpos('.', save_cursor) + end + error(match) + end ---@cast match [integer, integer] + if save_cursor ~= nil then + fn.setpos('.', save_cursor) + end + + local m_lnum = match[1] + local m_col = match[2] + + -- If a match is found setup match highlighting. + if m_lnum > 0 and m_lnum >= stoplinetop and m_lnum <= stoplinebottom then + local ids = vim.w.matchparen_ids + if tonumber(vim.g.matchparen_disable_cursor_hl) == 0 then + table.insert( + ids, + fn.matchaddpos('MatchParen', { { c_lnum, c_col - before }, { m_lnum, m_col } }, 10) + ) + else + table.insert(ids, fn.matchaddpos('MatchParen', { { m_lnum, m_col } }, 10)) + end + vim.w.matchparen_ids = ids + vim.w.paren_hl_on = 1 + end +end + +function M.remove_matches() + if vim.w.paren_hl_on ~= nil and vim.w.paren_hl_on ~= 0 then + local ids = vim.w.matchparen_ids or {} + while #ids > 0 do + pcall(fn.matchdelete, table.remove(ids, 1)) + end + vim.w.matchparen_ids = ids + vim.w.paren_hl_on = 0 + end +end + +function M.no_matchparen() + for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do + vim._with({ win = win, noautocmd = true }, function() + M.remove_matches() + end) + end + vim.g.loaded_matchparen = nil + pcall(api.nvim_clear_autocmds, { group = 'matchparen' }) +end + +function M.do_matchparen() + if vim.g.loaded_matchparen == nil then + M.enable() + end + for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do + vim._with({ win = win, silent = true }, function() + api.nvim_exec_autocmds('CursorMoved', {}) + end) + end +end + +return M diff --git a/runtime/plugin/matchparen.lua b/runtime/plugin/matchparen.lua new file mode 100644 index 0000000000..3c2a3ea96b --- /dev/null +++ b/runtime/plugin/matchparen.lua @@ -0,0 +1,5 @@ +if vim.g.loaded_matchparen ~= nil then + return +end + +require('nvim.matchparen').enable() diff --git a/runtime/plugin/matchparen.vim b/runtime/plugin/matchparen.vim index b05d35179c..8175675a29 100644 --- a/runtime/plugin/matchparen.vim +++ b/runtime/plugin/matchparen.vim @@ -2,231 +2,4 @@ " Maintainer: The Vim Project " Last Change: 2025 Apr 08 " Former Maintainer: Bram Moolenaar - -" Exit quickly when: -" - this plugin was already loaded (or disabled) -" - when 'compatible' is set -" - Vim has no support for :defer -if exists("g:loaded_matchparen") || &cp || - \ exists(":defer") != 2 - finish -endif -let g:loaded_matchparen = 1 - -if !exists("g:matchparen_timeout") - let g:matchparen_timeout = 300 -endif -if !exists("g:matchparen_insert_timeout") - let g:matchparen_insert_timeout = 60 -endif -if !exists("g:matchparen_disable_cursor_hl") - let g:matchparen_disable_cursor_hl = 0 -endif - -augroup matchparen - " Replace all matchparen autocommands - autocmd! CursorMoved,CursorMovedI,WinEnter,WinScrolled * call s:Highlight_Matching_Pair() - autocmd! BufWinEnter * autocmd SafeState * ++once call s:Highlight_Matching_Pair() - autocmd! WinLeave,BufLeave * call s:Remove_Matches() - autocmd! TextChanged,TextChangedI * call s:Highlight_Matching_Pair() - autocmd! TextChangedP * call s:Remove_Matches() -augroup END - -" Skip the rest if it was already done. -if exists("*s:Highlight_Matching_Pair") - finish -endif - -let s:cpo_save = &cpo -set cpo-=C - -" The function that is invoked (very often) to define a ":match" highlighting -" for any matching paren. -func s:Highlight_Matching_Pair() - if !exists("w:matchparen_ids") - let w:matchparen_ids = [] - endif - " Remove any previous match. - call s:Remove_Matches() - - " Avoid that we remove the popup menu. - " Return when there are no colors (looks like the cursor jumps). - if pumvisible() || (&t_Co < 8 && !has("gui_running")) - return - endif - - " Get the character under the cursor and check if it's in 'matchpairs'. - let c_lnum = line('.') - let c_col = col('.') - let before = 0 - - let text = getline(c_lnum) - let c_before = text->strpart(0, c_col - 1)->slice(-1) - let c = text->strpart(c_col - 1)->slice(0, 1) - let plist = split(&matchpairs, '.\zs[:,]') - let i = index(plist, c) - if i < 0 - " not found, in Insert mode try character before the cursor - if c_col > 1 && (mode() == 'i' || mode() == 'R') - let before = strlen(c_before) - let c = c_before - let i = index(plist, c) - endif - if i < 0 - " not found, nothing to do - return - endif - endif - - " Figure out the arguments for searchpairpos(). - if i % 2 == 0 - let s_flags = 'nW' - let c2 = plist[i + 1] - else - let s_flags = 'nbW' - let c2 = c - let c = plist[i - 1] - endif - if c == '[' - let c = '\[' - let c2 = '\]' - endif - - " Find the match. When it was just before the cursor move it there for a - " moment. - if before > 0 - let save_cursor = getcurpos() - call cursor(c_lnum, c_col - before) - defer setpos('.', save_cursor) - endif - - if !has("syntax") || !exists("g:syntax_on") - let s_skip = "0" - elseif exists("b:ts_highlight") && &syntax != 'on' - let s_skip = "match(v:lua.vim.treesitter.get_captures_at_cursor(), '" - \ .. 'string\|character\|singlequote\|escape\|symbol\|comment' - \ .. "') != -1" - else - " do not attempt to match when the syntax item where the cursor is - " indicates there does not exist a matching parenthesis, e.g. for shells - " case statement: "case $var in foobar)" - " - " add the check behind a filetype check, so it only needs to be - " evaluated for certain filetypes - if ['sh']->index(&filetype) >= 0 && - \ synstack(".", col("."))->indexof({_, id -> synIDattr(id, "name") - \ =~? "shSnglCase"}) >= 0 - return - endif - " Build an expression that detects whether the current cursor position is - " in certain syntax types (string, comment, etc.), for use as - " searchpairpos()'s skip argument. - " We match "escape" for special items, such as lispEscapeSpecial, and - " match "symbol" for lispBarSymbol. - let s_skip = 'synstack(".", col("."))' - \ . '->indexof({_, id -> synIDattr(id, "name") =~? ' - \ . '"string\\|character\\|singlequote\\|escape\\|symbol\\|comment"}) >= 0' - " If executing the expression determines that the cursor is currently in - " one of the syntax types, then we want searchpairpos() to find the pair - " within those syntax types (i.e., not skip). Otherwise, the cursor is - " outside of the syntax types and s_skip should keep its value so we skip - " any matching pair inside the syntax types. - " Catch if this throws E363: pattern uses more memory than 'maxmempattern'. - try - execute 'if ' . s_skip . ' | let s_skip = "0" | endif' - catch /^Vim\%((\a\+)\)\=:E363/ - " We won't find anything, so skip searching, should keep Vim responsive. - return - endtry - endif - - " Limit the search to lines visible in the window. - let stoplinebottom = line('w$') - let stoplinetop = line('w0') - if i % 2 == 0 - let stopline = stoplinebottom - else - let stopline = stoplinetop - endif - - " Limit the search time to 300 msec to avoid a hang on very long lines. - " This fails when a timeout is not supported. - if mode() == 'i' || mode() == 'R' - let timeout = exists("b:matchparen_insert_timeout") ? b:matchparen_insert_timeout : g:matchparen_insert_timeout - else - let timeout = exists("b:matchparen_timeout") ? b:matchparen_timeout : g:matchparen_timeout - endif - try - let [m_lnum, m_col] = searchpairpos(c, '', c2, s_flags, s_skip, stopline, timeout) - catch /E118/ - " Can't use the timeout, restrict the stopline a bit more to avoid taking - " a long time on closed folds and long lines. - " The "viewable" variables give a range in which we can scroll while - " keeping the cursor at the same position. - " adjustedScrolloff accounts for very large numbers of scrolloff. - let adjustedScrolloff = min([&scrolloff, (line('w$') - line('w0')) / 2]) - let bottom_viewable = min([line('$'), c_lnum + &lines - adjustedScrolloff - 2]) - let top_viewable = max([1, c_lnum-&lines+adjustedScrolloff + 2]) - " one of these stoplines will be adjusted below, but the current values are - " minimal boundaries within the current window - if i % 2 == 0 - if has("byte_offset") && has("syntax_items") && &smc > 0 - let stopbyte = min([line2byte("$"), line2byte(".") + col(".") + &smc * 2]) - let stopline = min([bottom_viewable, byte2line(stopbyte)]) - else - let stopline = min([bottom_viewable, c_lnum + 100]) - endif - let stoplinebottom = stopline - else - if has("byte_offset") && has("syntax_items") && &smc > 0 - let stopbyte = max([1, line2byte(".") + col(".") - &smc * 2]) - let stopline = max([top_viewable, byte2line(stopbyte)]) - else - let stopline = max([top_viewable, c_lnum - 100]) - endif - let stoplinetop = stopline - endif - let [m_lnum, m_col] = searchpairpos(c, '', c2, s_flags, s_skip, stopline) - endtry - - " If a match is found setup match highlighting. - if m_lnum > 0 && m_lnum >= stoplinetop && m_lnum <= stoplinebottom - if !g:matchparen_disable_cursor_hl - call add(w:matchparen_ids, matchaddpos('MatchParen', [[c_lnum, c_col - before], [m_lnum, m_col]], 10)) - else - call add(w:matchparen_ids, matchaddpos('MatchParen', [[m_lnum, m_col]], 10)) - endif - let w:paren_hl_on = 1 - endif -endfunction - -func s:Remove_Matches() - if exists('w:paren_hl_on') && w:paren_hl_on - while !empty(w:matchparen_ids) - silent! call remove(w:matchparen_ids, 0)->matchdelete() - endwhile - let w:paren_hl_on = 0 - endif -endfunc - -" Define commands that will disable and enable the plugin. -command DoMatchParen call s:DoMatchParen() -command NoMatchParen call s:NoMatchParen() - -func s:NoMatchParen() - let w = winnr() - noau windo call s:Remove_Matches() - unlet! g:loaded_matchparen - exe "noau ". w . "wincmd w" - au! matchparen -endfunc - -func s:DoMatchParen() - runtime plugin/matchparen.vim - let w = winnr() - silent windo doau CursorMoved - exe "noau ". w . "wincmd w" -endfunc - -let &cpo = s:cpo_save -unlet s:cpo_save +runtime plugin/matchparen.lua From f0f6753f30905c221007579487059ad959f14a30 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Thu, 7 May 2026 14:16:15 +0800 Subject: [PATCH 2/2] refactor(matchparen): parameterize `win` instead of always using the current AI-assisted: Codex --- runtime/lua/nvim/matchparen.lua | 140 ++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 50 deletions(-) diff --git a/runtime/lua/nvim/matchparen.lua b/runtime/lua/nvim/matchparen.lua index 67fabf0ef5..e3c3d3d716 100644 --- a/runtime/lua/nvim/matchparen.lua +++ b/runtime/lua/nvim/matchparen.lua @@ -51,7 +51,9 @@ function M.enable() 'TextChangedI', }, { group = group, - callback = M.highlight_matching_pair, + callback = function() + M.highlight_matching_pair() + end, }) api.nvim_create_autocmd('BufWinEnter', { group = group, @@ -59,7 +61,9 @@ function M.enable() api.nvim_create_autocmd('SafeState', { group = group, once = true, - callback = M.highlight_matching_pair, + callback = function() + M.highlight_matching_pair() + end, }) end, }) @@ -69,22 +73,32 @@ function M.enable() 'TextChangedP', }, { group = group, - callback = M.remove_matches, + callback = function() + M.remove_matches() + end, }) -- Define commands that will disable and enable the plugin. - api.nvim_create_user_command('DoMatchParen', M.do_matchparen, { force = true }) - api.nvim_create_user_command('NoMatchParen', M.no_matchparen, { force = true }) + api.nvim_create_user_command('DoMatchParen', function() + M.do_matchparen() + end, { force = true }) + api.nvim_create_user_command('NoMatchParen', function() + M.no_matchparen() + end, { force = true }) end --- The function that is invoked (very often) to define a ":match" highlighting --- for any matching paren. -function M.highlight_matching_pair() - if vim.w.matchparen_ids == nil then - vim.w.matchparen_ids = {} +---@param win? integer +function M.highlight_matching_pair(win) + win = win or api.nvim_get_current_win() + local buf = api.nvim_win_get_buf(win) + + if vim.w[win].matchparen_ids == nil then + vim.w[win].matchparen_ids = {} end -- Remove any previous match. - M.remove_matches() + M.remove_matches(win) -- Avoid that we remove the popup menu. if fn.pumvisible() ~= 0 then @@ -92,19 +106,19 @@ function M.highlight_matching_pair() end -- Get the character under the cursor and check if it's in 'matchpairs'. - local cursor = api.nvim_win_get_cursor(0) + local cursor = api.nvim_win_get_cursor(win) local c_lnum = cursor[1] local c_col = cursor[2] + 1 local before = 0 - local text = api.nvim_buf_get_lines(0, c_lnum - 1, c_lnum, false)[1] or '' + local text = api.nvim_buf_get_lines(buf, c_lnum - 1, c_lnum, false)[1] or '' -- Cursor columns are byte indexes, -- while 'matchpairs' entries may be multibyte characters. -- Use UTF-8 boundaries to extract the whole character around the byte column. local c_before = char_before(text, c_col) local c = char_at(text, c_col) ---@type string[] - local plist = vim.split(vim.o.matchpairs, '[:,]', { trimempty = true }) + local plist = vim.split(vim.bo[buf].matchpairs, '[:,]', { trimempty = true }) ---@type integer? local i = vim.iter(ipairs(plist)):find(function(_, item) return item == c @@ -145,8 +159,8 @@ function M.highlight_matching_pair() -- moment. local save_cursor ---@type [integer, integer, integer, integer, integer]? if before > 0 then - save_cursor = fn.getcurpos() - api.nvim_win_set_cursor(0, { c_lnum, c_col - before - 1 }) + save_cursor = fn.getcurpos(win) + api.nvim_win_set_cursor(win, { c_lnum, c_col - before - 1 }) end local skip ---@type fun(): boolean @@ -154,9 +168,9 @@ function M.highlight_matching_pair() skip = function() return false end - elseif vim.b.ts_highlight ~= nil and vim.o.syntax ~= 'on' then + elseif vim.b[buf].ts_highlight ~= nil and vim.bo[buf].syntax ~= 'on' then skip = function() - for _, capture in ipairs(vim.treesitter.get_captures_at_cursor()) do + for _, capture in ipairs(vim.treesitter.get_captures_at_cursor(win)) do for _, skip_name in ipairs(skip_names) do if capture:find(skip_name, 1, true) ~= nil then return true @@ -172,12 +186,18 @@ function M.highlight_matching_pair() -- -- add the check behind a filetype check, so it only needs to be -- evaluated for certain filetypes - if vim.o.filetype == 'sh' then - local pos = api.nvim_win_get_cursor(0) - for _, id in ipairs(fn.synstack(pos[1], pos[2] + 1)) do + if vim.bo[buf].filetype == 'sh' then + local pos = api.nvim_win_get_cursor(win) + ---@type integer[] + local stack = api.nvim_win_call(win, function() + return fn.synstack(pos[1], pos[2] + 1) + end) + for _, id in ipairs(stack) do if fn.synIDattr(id, 'name'):lower():find('shsnglcase') ~= nil then if save_cursor ~= nil then - fn.setpos('.', save_cursor) + api.nvim_win_call(win, function() + fn.setpos('.', save_cursor) + end) end return end @@ -189,7 +209,7 @@ function M.highlight_matching_pair() -- We match "escape" for special items, such as lispEscapeSpecial, and -- match "symbol" for lispBarSymbol. skip = function() - local pos = api.nvim_win_get_cursor(0) + local pos = api.nvim_win_get_cursor(win) for _, id in ipairs(fn.synstack(pos[1], pos[2] + 1)) do local name = fn.synIDattr(id, 'name'):lower() for _, skip_name in ipairs(skip_names) do @@ -205,7 +225,7 @@ function M.highlight_matching_pair() -- within those syntax types (i.e., not skip). Otherwise, the cursor is -- outside of the syntax types and skip should keep its value so we skip -- any matching pair inside the syntax types. - if skip() then + if api.nvim_win_call(win, skip) then skip = function() return false end @@ -213,8 +233,10 @@ function M.highlight_matching_pair() end -- Limit the search to lines visible in the window. - local stoplinebottom = fn.line('w$') - local stoplinetop = fn.line('w0') + ---@type integer, integer + local stoplinetop, stoplinebottom = unpack(api.nvim_win_call(win, function() + return { fn.line('w0'), fn.line('w$') } + end)) local stopline ---@type integer if i % 2 == 1 then stopline = stoplinebottom @@ -226,21 +248,27 @@ function M.highlight_matching_pair() local timeout ---@type integer local mode = api.nvim_get_mode().mode if mode == 'i' or mode == 'R' then - timeout = vim.b.matchparen_insert_timeout or vim.g.matchparen_insert_timeout + timeout = vim.b[buf].matchparen_insert_timeout or vim.g.matchparen_insert_timeout else - timeout = vim.b.matchparen_timeout or vim.g.matchparen_timeout + timeout = vim.b[buf].matchparen_timeout or vim.g.matchparen_timeout end ---@type boolean, [integer, integer]|string - local ok, match = pcall(fn.searchpairpos, c, '', c2, flags, skip, stopline, timeout) + local ok, match = pcall(api.nvim_win_call, win, function() + return fn.searchpairpos(c, '', c2, flags, skip, stopline, timeout) + end) if not ok then ---@cast match string if save_cursor ~= nil then - fn.setpos('.', save_cursor) + api.nvim_win_call(win, function() + fn.setpos('.', save_cursor) + end) end error(match) end ---@cast match [integer, integer] if save_cursor ~= nil then - fn.setpos('.', save_cursor) + api.nvim_win_call(win, function() + fn.setpos('.', save_cursor) + end) end local m_lnum = match[1] @@ -248,49 +276,61 @@ function M.highlight_matching_pair() -- If a match is found setup match highlighting. if m_lnum > 0 and m_lnum >= stoplinetop and m_lnum <= stoplinebottom then - local ids = vim.w.matchparen_ids + local ids = vim.w[win].matchparen_ids if tonumber(vim.g.matchparen_disable_cursor_hl) == 0 then table.insert( ids, - fn.matchaddpos('MatchParen', { { c_lnum, c_col - before }, { m_lnum, m_col } }, 10) + fn.matchaddpos( + 'MatchParen', + { { c_lnum, c_col - before }, { m_lnum, m_col } }, + 10, + -1, + { window = win } + ) ) else - table.insert(ids, fn.matchaddpos('MatchParen', { { m_lnum, m_col } }, 10)) + table.insert( + ids, + fn.matchaddpos('MatchParen', { { m_lnum, m_col } }, 10, -1, { window = win }) + ) end - vim.w.matchparen_ids = ids - vim.w.paren_hl_on = 1 + vim.w[win].matchparen_ids = ids + vim.w[win].paren_hl_on = 1 end end -function M.remove_matches() - if vim.w.paren_hl_on ~= nil and vim.w.paren_hl_on ~= 0 then - local ids = vim.w.matchparen_ids or {} +---@param win? integer +function M.remove_matches(win) + win = win or api.nvim_get_current_win() + + if vim.w[win].paren_hl_on ~= nil and vim.w[win].paren_hl_on ~= 0 then + local ids = vim.w[win].matchparen_ids or {} while #ids > 0 do - pcall(fn.matchdelete, table.remove(ids, 1)) + pcall(fn.matchdelete, table.remove(ids, 1), win) end - vim.w.matchparen_ids = ids - vim.w.paren_hl_on = 0 + vim.w[win].matchparen_ids = ids + vim.w[win].paren_hl_on = 0 end end -function M.no_matchparen() - for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do - vim._with({ win = win, noautocmd = true }, function() - M.remove_matches() - end) +---@param tab? integer +function M.no_matchparen(tab) + tab = tab or api.nvim_get_current_tabpage() + for _, win in ipairs(api.nvim_tabpage_list_wins(tab)) do + M.remove_matches(win) end vim.g.loaded_matchparen = nil pcall(api.nvim_clear_autocmds, { group = 'matchparen' }) end -function M.do_matchparen() +---@param tab? integer +function M.do_matchparen(tab) if vim.g.loaded_matchparen == nil then M.enable() end - for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do - vim._with({ win = win, silent = true }, function() - api.nvim_exec_autocmds('CursorMoved', {}) - end) + tab = tab or api.nvim_get_current_tabpage() + for _, win in ipairs(api.nvim_tabpage_list_wins(tab)) do + M.highlight_matching_pair(win) end end