mirror of
https://github.com/neovim/neovim.git
synced 2024-12-26 14:11:15 -07:00
06ec1a49d7
Problem: Cannot break undo by setting 'undolevels' to itself in
'inccommand' preview callback.
Solution: Don't set an invalid 'undolevels' value.
Co-authored-by: Michael Henry <drmikehenry@drmikehenry.com>
(cherry picked from commit 7c5e7d831a
)
664 lines
23 KiB
Lua
664 lines
23 KiB
Lua
local helpers = require('test.functional.helpers')(after_each)
|
|
local Screen = require('test.functional.ui.screen')
|
|
local clear = helpers.clear
|
|
local exec_lua = helpers.exec_lua
|
|
local insert = helpers.insert
|
|
local feed = helpers.feed
|
|
local command = helpers.command
|
|
local assert_alive = helpers.assert_alive
|
|
|
|
-- Implements a :Replace command that works like :substitute and has multibuffer support.
|
|
local setup_replace_cmd = [[
|
|
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
|
|
local multibuffer = #matches > 1
|
|
|
|
for _, match in ipairs(matches) do
|
|
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,
|
|
{ bufname .. ':' }
|
|
)
|
|
|
|
preview_buf_line = preview_buf_line + 1
|
|
end
|
|
|
|
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
|
|
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',
|
|
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
|
|
|
|
preview_buf_line = preview_buf_line + 1
|
|
end
|
|
end
|
|
|
|
if use_preview_win then
|
|
return 2
|
|
else
|
|
return 1
|
|
end
|
|
end
|
|
|
|
local function do_replace(opts, preview, preview_ns, preview_buf)
|
|
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 matches = {}
|
|
|
|
-- 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
|
|
|
|
-- 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
|
|
|
|
return false
|
|
end,
|
|
vim.api.nvim_list_bufs()
|
|
)
|
|
|
|
for _, buf in ipairs(buffers) do
|
|
local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false)
|
|
local buf_matches = {}
|
|
|
|
for i, line in ipairs(lines) do
|
|
local startidx, endidx = 0, 0
|
|
local line_matches = {}
|
|
local num = 1
|
|
|
|
while startidx ~= -1 do
|
|
local match = vim.fn.matchstrpos(line, pat1, 0, num)
|
|
startidx, endidx = match[2], match[3]
|
|
|
|
if startidx ~= -1 then
|
|
line_matches[#line_matches+1] = { startidx, endidx }
|
|
end
|
|
|
|
num = num + 1
|
|
end
|
|
|
|
if #line_matches > 0 then
|
|
buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches }
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g')
|
|
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
|
|
end
|
|
|
|
if pat2 ~= '' then
|
|
idx_offset = idx_offset + pat_width_differences[i]
|
|
end
|
|
|
|
line_matches[i] = { repl_startidx, repl_endidx }
|
|
end
|
|
end
|
|
end
|
|
|
|
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(use_preview_win, preview_ns, preview_buf, matches)
|
|
end
|
|
end
|
|
|
|
local function replace(opts)
|
|
do_replace(opts, false)
|
|
end
|
|
|
|
local function replace_preview(opts, preview_ns, preview_buf)
|
|
return do_replace(opts, true, preview_ns, preview_buf)
|
|
end
|
|
|
|
-- ":<range>Replace <pat1> <pat2>"
|
|
-- Replaces all occurrences of <pat1> in <range> with <pat2>
|
|
vim.api.nvim_create_user_command(
|
|
'Replace',
|
|
replace,
|
|
{ nargs = '*', range = '%', addr = 'lines',
|
|
preview = replace_preview }
|
|
)
|
|
]]
|
|
|
|
describe("'inccommand' for user commands", 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},
|
|
[5] = {foreground = Screen.colors.Blue},
|
|
})
|
|
screen:attach()
|
|
exec_lua(setup_replace_cmd)
|
|
command('set cmdwinheight=5')
|
|
insert[[
|
|
text on line 1
|
|
more text on line 2
|
|
oh no, even more text
|
|
will the text ever stop
|
|
oh well
|
|
did the text stop
|
|
why won't it stop
|
|
make the text stop
|
|
]]
|
|
end)
|
|
|
|
it('works with inccommand=nosplit', function()
|
|
command('set inccommand=nosplit')
|
|
feed(':Replace text cats')
|
|
screen:expect([[
|
|
{1:cats} on line 1 |
|
|
more {1:cats} on line 2 |
|
|
oh no, even more {1:cats} |
|
|
will the {1:cats} ever stop |
|
|
oh well |
|
|
did the {1:cats} stop |
|
|
why won't it stop |
|
|
make the {1:cats} stop |
|
|
|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
:Replace text cats^ |
|
|
]])
|
|
end)
|
|
|
|
it('works with inccommand=split', function()
|
|
command('set inccommand=split')
|
|
feed(':Replace text cats')
|
|
screen:expect([[
|
|
{1:cats} on line 1 |
|
|
more {1:cats} on line 2 |
|
|
oh no, even more {1:cats} |
|
|
will the {1:cats} ever stop |
|
|
oh well |
|
|
did the {1:cats} stop |
|
|
why won't it stop |
|
|
make the {1:cats} stop |
|
|
|
|
|
{4:[No Name] [+] }|
|
|
|1| {1:cats} on line 1 |
|
|
|2| more {1:cats} on line 2 |
|
|
|3| oh no, even more {1:cats} |
|
|
|4| will the {1:cats} ever stop |
|
|
|6| did the {1:cats} stop |
|
|
{3:[Preview] }|
|
|
:Replace text cats^ |
|
|
]])
|
|
end)
|
|
|
|
it('properly closes preview when inccommand=split', function()
|
|
command('set inccommand=split')
|
|
feed(':Replace text cats<Esc>')
|
|
screen:expect([[
|
|
text on line 1 |
|
|
more text on line 2 |
|
|
oh no, even more text |
|
|
will the text ever stop |
|
|
oh well |
|
|
did the text stop |
|
|
why won't it stop |
|
|
make the text stop |
|
|
^ |
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
|
|
|
]])
|
|
end)
|
|
|
|
it('properly executes command when inccommand=split', function()
|
|
command('set inccommand=split')
|
|
feed(':Replace text cats<CR>')
|
|
screen:expect([[
|
|
cats on line 1 |
|
|
more cats on line 2 |
|
|
oh no, even more cats |
|
|
will the cats ever stop |
|
|
oh well |
|
|
did the cats stop |
|
|
why won't it stop |
|
|
make the cats stop |
|
|
^ |
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
:Replace text cats |
|
|
]])
|
|
end)
|
|
|
|
it('shows preview window only when range is not current line', function()
|
|
command('set inccommand=split')
|
|
feed('gg:.Replace text cats')
|
|
screen:expect([[
|
|
{1:cats} on line 1 |
|
|
more text on line 2 |
|
|
oh no, even more text |
|
|
will the text ever stop |
|
|
oh well |
|
|
did the text stop |
|
|
why won't it stop |
|
|
make the text stop |
|
|
|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
:.Replace text cats^ |
|
|
]])
|
|
end)
|
|
|
|
it('does not crash on ambiguous command #18825', function()
|
|
command('set inccommand=split')
|
|
command('command Reply echo 1')
|
|
feed(':R')
|
|
assert_alive()
|
|
feed('e')
|
|
assert_alive()
|
|
end)
|
|
|
|
it('no crash if preview callback changes inccommand option', function()
|
|
command('set inccommand=nosplit')
|
|
exec_lua([[
|
|
vim.api.nvim_create_user_command('Replace', function() end, {
|
|
nargs = '*',
|
|
preview = function()
|
|
vim.api.nvim_set_option('inccommand', 'split')
|
|
return 2
|
|
end,
|
|
})
|
|
]])
|
|
feed(':R')
|
|
assert_alive()
|
|
feed('e')
|
|
assert_alive()
|
|
end)
|
|
|
|
it('no crash when adding highlight after :substitute #21495', function()
|
|
command('set inccommand=nosplit')
|
|
exec_lua([[
|
|
vim.api.nvim_create_user_command("Crash", function() end, {
|
|
preview = function(_, preview_ns, _)
|
|
vim.cmd("%s/text/cats/g")
|
|
vim.api.nvim_buf_add_highlight(0, preview_ns, "Search", 0, 0, -1)
|
|
return 1
|
|
end,
|
|
})
|
|
]])
|
|
feed(':C')
|
|
screen:expect([[
|
|
{1: cats on line 1} |
|
|
more cats on line 2 |
|
|
oh no, even more cats |
|
|
will the cats ever stop |
|
|
oh well |
|
|
did the cats stop |
|
|
why won't it stop |
|
|
make the cats stop |
|
|
|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
:C^ |
|
|
]])
|
|
assert_alive()
|
|
end)
|
|
|
|
it('no crash if preview callback executes undo #20036', function()
|
|
command('set inccommand=nosplit')
|
|
exec_lua([[
|
|
vim.api.nvim_create_user_command('Foo', function() end, {
|
|
nargs = '?',
|
|
preview = function(_, _, _)
|
|
vim.cmd.undo()
|
|
end,
|
|
})
|
|
]])
|
|
|
|
-- Clear undo history
|
|
command('set undolevels=-1')
|
|
feed('ggyyp')
|
|
command('set undolevels=1000')
|
|
|
|
feed('yypp:Fo')
|
|
assert_alive()
|
|
feed('<Esc>:Fo')
|
|
assert_alive()
|
|
end)
|
|
|
|
local function test_preview_break_undo()
|
|
command('set inccommand=nosplit')
|
|
exec_lua([[
|
|
vim.api.nvim_create_user_command('Test', function() end, {
|
|
nargs = 1,
|
|
preview = function(opts, _, _)
|
|
vim.cmd('norm i' .. opts.args)
|
|
return 1
|
|
end
|
|
})
|
|
]])
|
|
feed(':Test a.a.a.a.')
|
|
screen:expect([[
|
|
text on line 1 |
|
|
more text on line 2 |
|
|
oh no, even more text |
|
|
will the text ever stop |
|
|
oh well |
|
|
did the text stop |
|
|
why won't it stop |
|
|
make the text stop |
|
|
a.a.a.a. |
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
:Test a.a.a.a.^ |
|
|
]])
|
|
feed('<C-V><Esc>u')
|
|
screen:expect([[
|
|
text on line 1 |
|
|
more text on line 2 |
|
|
oh no, even more text |
|
|
will the text ever stop |
|
|
oh well |
|
|
did the text stop |
|
|
why won't it stop |
|
|
make the text stop |
|
|
a.a.a. |
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
:Test a.a.a.a.{5:^[}u^ |
|
|
]])
|
|
feed('<Esc>')
|
|
screen:expect([[
|
|
text on line 1 |
|
|
more text on line 2 |
|
|
oh no, even more text |
|
|
will the text ever stop |
|
|
oh well |
|
|
did the text stop |
|
|
why won't it stop |
|
|
make the text stop |
|
|
^ |
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
{2:~ }|
|
|
|
|
|
]])
|
|
end
|
|
|
|
describe('breaking undo chain in Insert mode works properly', function()
|
|
it('when using i_CTRL-G_u #20248', function()
|
|
command('inoremap . .<C-G>u')
|
|
test_preview_break_undo()
|
|
end)
|
|
|
|
it('when setting &l:undolevels to itself #24575', function()
|
|
command('inoremap . .<Cmd>let &l:undolevels = &l:undolevels<CR>')
|
|
test_preview_break_undo()
|
|
end)
|
|
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('<CR>')
|
|
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('<CR>')
|
|
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)
|