From c2f7a2a18daa7e1d80285818065b2376683dcb21 Mon Sep 17 00:00:00 2001 From: Famiu Haque Date: Thu, 14 Jul 2022 14:37:00 +0600 Subject: feat: multibuffer preview support for inccommand Allows preview callbacks to modify multiple buffers in order to show the preview. Previously, if multiple buffers were modified, only the current buffer would have its state restored. After this change, all buffers have their state restored after preview. Closes #19103. --- test/functional/ui/inccommand_user_spec.lua | 347 ++++++++++++++++++++-------- 1 file changed, 256 insertions(+), 91 deletions(-) (limited to 'test') diff --git a/test/functional/ui/inccommand_user_spec.lua b/test/functional/ui/inccommand_user_spec.lua index b5816f6fe6..0b25d4f8d2 100644 --- a/test/functional/ui/inccommand_user_spec.lua +++ b/test/functional/ui/inccommand_user_spec.lua @@ -7,61 +7,82 @@ local feed = helpers.feed local command = helpers.command local assert_alive = helpers.assert_alive --- Implements a :Replace command that works like :substitute. +-- Implements a :Replace command that works like :substitute and has multibuffer support. local setup_replace_cmd = [[ - local function show_replace_preview(buf, use_preview_win, preview_ns, preview_buf, matches) + local function show_replace_preview(use_preview_win, preview_ns, preview_buf, matches) -- Find the width taken by the largest line number, used for padding the line numbers local highest_lnum = math.max(matches[#matches][1], 1) local highest_lnum_width = math.floor(math.log10(highest_lnum)) local preview_buf_line = 0 - - vim.g.prevns = preview_ns - vim.g.prevbuf = preview_buf + local multibuffer = #matches > 1 for _, match in ipairs(matches) do - local lnum = match[1] - local line_matches = match[2] - local prefix - - if use_preview_win then - prefix = string.format( - '|%s%d| ', - string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))), - lnum - ) + local buf = match[1] + local buf_matches = match[2] + + if multibuffer and #buf_matches > 0 and use_preview_win then + local bufname = vim.api.nvim_buf_get_name(buf) + + if bufname == "" then + bufname = string.format("Buffer #%d", buf) + end vim.api.nvim_buf_set_lines( preview_buf, preview_buf_line, preview_buf_line, 0, - { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] } + { bufname .. ':' } ) + + preview_buf_line = preview_buf_line + 1 end - for _, line_match in ipairs(line_matches) do - vim.api.nvim_buf_add_highlight( - buf, - preview_ns, - 'Substitute', - lnum - 1, - line_match[1], - line_match[2] - ) + for _, buf_match in ipairs(buf_matches) do + local lnum = buf_match[1] + local line_matches = buf_match[2] + local prefix if use_preview_win then - vim.api.nvim_buf_add_highlight( + prefix = string.format( + '|%s%d| ', + string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))), + lnum + ) + + vim.api.nvim_buf_set_lines( preview_buf, + preview_buf_line, + preview_buf_line, + 0, + { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] } + ) + end + + for _, line_match in ipairs(line_matches) do + vim.api.nvim_buf_add_highlight( + buf, preview_ns, 'Substitute', - preview_buf_line, - #prefix + line_match[1], - #prefix + line_match[2] + lnum - 1, + line_match[1], + line_match[2] ) + + if use_preview_win then + vim.api.nvim_buf_add_highlight( + preview_buf, + preview_ns, + 'Substitute', + preview_buf_line, + #prefix + line_match[1], + #prefix + line_match[2] + ) + end end - end - preview_buf_line = preview_buf_line + 1 + preview_buf_line = preview_buf_line + 1 + end end if use_preview_win then @@ -72,94 +93,121 @@ local setup_replace_cmd = [[ end local function do_replace(opts, preview, preview_ns, preview_buf) - local pat1 = opts.fargs[1] or '' + local pat1 = opts.fargs[1] + + if not pat1 then return end + local pat2 = opts.fargs[2] or '' local line1 = opts.line1 local line2 = opts.line2 - - local buf = vim.api.nvim_get_current_buf() - local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, 0) local matches = {} - for i, line in ipairs(lines) do - local startidx, endidx = 0, 0 - local line_matches = {} - local num = 1 + -- Get list of valid and listed buffers + local buffers = vim.tbl_filter( + function(buf) + if not (vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted and buf ~= preview_buf) + then + return false + end - while startidx ~= -1 do - local match = vim.fn.matchstrpos(line, pat1, 0, num) - startidx, endidx = match[2], match[3] + -- Check if there's at least one window using the buffer + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + if vim.api.nvim_win_get_buf(win) == buf then + return true + end + end - if startidx ~= -1 then - line_matches[#line_matches+1] = { startidx, endidx } - end + return false + end, + vim.api.nvim_list_bufs() + ) - num = num + 1 - end + for _, buf in ipairs(buffers) do + local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false) + local buf_matches = {} - if #line_matches > 0 then - matches[#matches+1] = { line1 + i - 1, line_matches } - end - end + for i, line in ipairs(lines) do + local startidx, endidx = 0, 0 + local line_matches = {} + local num = 1 - local new_lines = {} + while startidx ~= -1 do + local match = vim.fn.matchstrpos(line, pat1, 0, num) + startidx, endidx = match[2], match[3] - for _, match in ipairs(matches) do - local lnum = match[1] - local line_matches = match[2] - local line = lines[lnum - line1 + 1] - local pat_width_differences = {} - - -- If previewing, only replace the text in current buffer if pat2 isn't empty - -- Otherwise, always replace the text - if pat2 ~= '' or not preview then - if preview then - for _, line_match in ipairs(line_matches) do - local startidx, endidx = unpack(line_match) - local pat_match = line:sub(startidx + 1, endidx) - - pat_width_differences[#pat_width_differences+1] = - #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match + if startidx ~= -1 then + line_matches[#line_matches+1] = { startidx, endidx } end + + num = num + 1 end - new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g') + if #line_matches > 0 then + buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches } + end end - -- Highlight the matches if previewing - if preview then - local idx_offset = 0 - for i, line_match in ipairs(line_matches) do - local startidx, endidx = unpack(line_match) - -- Starting index of replacement text - local repl_startidx = startidx + idx_offset - -- Ending index of the replacement text (if pat2 isn't empty) - local repl_endidx - - if pat2 ~= '' then - repl_endidx = endidx + idx_offset + pat_width_differences[i] - else - repl_endidx = endidx + idx_offset + local new_lines = {} + + for _, buf_match in ipairs(buf_matches) do + local lnum = buf_match[1] + local line_matches = buf_match[2] + local line = lines[lnum - line1 + 1] + local pat_width_differences = {} + + -- If previewing, only replace the text in current buffer if pat2 isn't empty + -- Otherwise, always replace the text + if pat2 ~= '' or not preview then + if preview then + for _, line_match in ipairs(line_matches) do + local startidx, endidx = unpack(line_match) + local pat_match = line:sub(startidx + 1, endidx) + + pat_width_differences[#pat_width_differences+1] = + #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match + end end - if pat2 ~= '' then - idx_offset = idx_offset + pat_width_differences[i] - end + new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g') + end - line_matches[i] = { repl_startidx, repl_endidx } + -- Highlight the matches if previewing + if preview then + local idx_offset = 0 + for i, line_match in ipairs(line_matches) do + local startidx, endidx = unpack(line_match) + -- Starting index of replacement text + local repl_startidx = startidx + idx_offset + -- Ending index of the replacement text (if pat2 isn't empty) + local repl_endidx + + if pat2 ~= '' then + repl_endidx = endidx + idx_offset + pat_width_differences[i] + else + repl_endidx = endidx + idx_offset + end + + if pat2 ~= '' then + idx_offset = idx_offset + pat_width_differences[i] + end + + line_matches[i] = { repl_startidx, repl_endidx } + end end end - end - for lnum, line in pairs(new_lines) do - vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line }) + for lnum, line in pairs(new_lines) do + vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line }) + end + + matches[#matches+1] = { buf, buf_matches } end if preview then local lnum = vim.api.nvim_win_get_cursor(0)[1] -- Use preview window only if preview buffer is provided and range isn't just the current line local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum) - return show_replace_preview(buf, use_preview_win, preview_ns, preview_buf, matches) + return show_replace_preview(use_preview_win, preview_ns, preview_buf, matches) end end @@ -354,3 +402,120 @@ describe("'inccommand' for user commands", function() assert_alive() end) end) + +describe("'inccommand' with multiple buffers", function() + local screen + + before_each(function() + clear() + screen = Screen.new(40, 17) + screen:set_default_attr_ids({ + [1] = {background = Screen.colors.Yellow1}, + [2] = {foreground = Screen.colors.Blue1, bold = true}, + [3] = {reverse = true}, + [4] = {reverse = true, bold = true} + }) + screen:attach() + exec_lua(setup_replace_cmd) + command('set cmdwinheight=10') + insert[[ + foo bar baz + bar baz foo + baz foo bar + ]] + command('vsplit | enew') + insert[[ + bar baz foo + baz foo bar + foo bar baz + ]] + end) + + it('works', function() + command('set inccommand=nosplit') + feed(':Replace foo bar') + screen:expect([[ + bar baz {1:bar} │ {1:bar} bar baz | + baz {1:bar} bar │ bar baz {1:bar} | + {1:bar} bar baz │ baz {1:bar} bar | + │ | + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {4:[No Name] [+] }{3:[No Name] [+] }| + :Replace foo bar^ | + ]]) + feed('') + screen:expect([[ + bar baz bar │ bar bar baz | + baz bar bar │ bar baz bar | + bar bar baz │ baz bar bar | + ^ │ | + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {4:[No Name] [+] }{3:[No Name] [+] }| + :Replace foo bar | + ]]) + end) + + it('works with inccommand=split', function() + command('set inccommand=split') + feed(':Replace foo bar') + screen:expect([[ + bar baz {1:bar} │ {1:bar} bar baz | + baz {1:bar} bar │ bar baz {1:bar} | + {1:bar} bar baz │ baz {1:bar} bar | + │ | + {4:[No Name] [+] }{3:[No Name] [+] }| + Buffer #1: | + |1| {1:bar} bar baz | + |2| bar baz {1:bar} | + |3| baz {1:bar} bar | + Buffer #2: | + |1| bar baz {1:bar} | + |2| baz {1:bar} bar | + |3| {1:bar} bar baz | + | + {2:~ }| + {3:[Preview] }| + :Replace foo bar^ | + ]]) + feed('') + screen:expect([[ + bar baz bar │ bar bar baz | + baz bar bar │ bar baz bar | + bar bar baz │ baz bar bar | + ^ │ | + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {2:~ }│{2:~ }| + {4:[No Name] [+] }{3:[No Name] [+] }| + :Replace foo bar | + ]]) + end) +end) -- cgit