mirror of
https://github.com/neovim/neovim.git
synced 2024-12-20 03:05:11 -07:00
e61228a214
Before calling "attach" a screen object is just a dummy container for (row, col) values whose purpose is to be sent as part of the "attach" function call anyway. Just create the screen in an attached state directly. Keep the complete (row, col, options) config together. It is still completely valid to later detach and re-attach as needed, including to another session.
624 lines
20 KiB
Lua
624 lines
20 KiB
Lua
local t = require('test.testutil')
|
|
local n = require('test.functional.testnvim')()
|
|
local Screen = require('test.functional.ui.screen')
|
|
|
|
local api = n.api
|
|
local clear = n.clear
|
|
local eq = t.eq
|
|
local exec_lua = n.exec_lua
|
|
local insert = n.insert
|
|
local feed = n.feed
|
|
local command = n.command
|
|
local assert_alive = n.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)
|
|
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([[
|
|
{10:cats} on line 1 |
|
|
more {10:cats} on line 2 |
|
|
oh no, even more {10:cats} |
|
|
will the {10:cats} ever stop |
|
|
oh well |
|
|
did the {10:cats} stop |
|
|
why won't it stop |
|
|
make the {10:cats} stop |
|
|
|
|
|
{1:~ }|*7
|
|
:Replace text cats^ |
|
|
]])
|
|
end)
|
|
|
|
it('works with inccommand=split', function()
|
|
command('set inccommand=split')
|
|
feed(':Replace text cats')
|
|
screen:expect([[
|
|
{10:cats} on line 1 |
|
|
more {10:cats} on line 2 |
|
|
oh no, even more {10:cats} |
|
|
will the {10:cats} ever stop |
|
|
oh well |
|
|
did the {10:cats} stop |
|
|
why won't it stop |
|
|
make the {10:cats} stop |
|
|
|
|
|
{3:[No Name] [+] }|
|
|
|1| {10:cats} on line 1 |
|
|
|2| more {10:cats} on line 2 |
|
|
|3| oh no, even more {10:cats} |
|
|
|4| will the {10:cats} ever stop |
|
|
|6| did the {10:cats} stop |
|
|
{2:[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 |
|
|
^ |
|
|
{1:~ }|*7
|
|
|
|
|
]])
|
|
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 |
|
|
^ |
|
|
{1:~ }|*7
|
|
: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([[
|
|
{10: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 |
|
|
|
|
|
{1:~ }|*7
|
|
:.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_value('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([[
|
|
{10: 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 |
|
|
|
|
|
{1:~ }|*7
|
|
: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. |
|
|
{1:~ }|*7
|
|
: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. |
|
|
{1:~ }|*7
|
|
:Test a.a.a.a.{18:^[}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 |
|
|
^ |
|
|
{1:~ }|*7
|
|
|
|
|
]])
|
|
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)
|
|
|
|
it('disables preview if preview buffer cannot be created #27086', function()
|
|
command('set inccommand=split')
|
|
api.nvim_buf_set_name(0, '[Preview]')
|
|
exec_lua([[
|
|
vim.api.nvim_create_user_command('Test', function() end, {
|
|
nargs = '*',
|
|
preview = function(_, _, _)
|
|
return 2
|
|
end
|
|
})
|
|
]])
|
|
eq('split', api.nvim_get_option_value('inccommand', {}))
|
|
feed(':Test')
|
|
eq('nosplit', api.nvim_get_option_value('inccommand', {}))
|
|
end)
|
|
|
|
it('does not flush intermediate cursor position at end of message grid', function()
|
|
exec_lua([[
|
|
vim.api.nvim_create_user_command('Test', function() end, {
|
|
nargs = '*',
|
|
preview = function(_, _, _)
|
|
vim.api.nvim_buf_set_text(0, 0, 0, 1, -1, { "Preview" })
|
|
vim.cmd.sleep("1m")
|
|
return 1
|
|
end
|
|
})
|
|
]])
|
|
local cursor_goto = screen._handle_grid_cursor_goto
|
|
screen._handle_grid_cursor_goto = function(...)
|
|
cursor_goto(...)
|
|
assert(screen._cursor.col < 12)
|
|
end
|
|
feed(':Test baz<Left><Left>arb')
|
|
screen:expect({
|
|
grid = [[
|
|
Preview |
|
|
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 |
|
|
|
|
|
{1:~ }|*8
|
|
:Test barb^az |
|
|
]],
|
|
})
|
|
end)
|
|
end)
|
|
|
|
describe("'inccommand' with multiple buffers", function()
|
|
local screen
|
|
|
|
before_each(function()
|
|
clear()
|
|
screen = Screen.new(40, 17)
|
|
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 {10:bar} │ {10:bar} bar baz |
|
|
baz {10:bar} bar │ bar baz {10:bar} |
|
|
{10:bar} bar baz │ baz {10:bar} bar |
|
|
│ |
|
|
{1:~ }│{1:~ }|*11
|
|
{3:[No Name] [+] }{2:[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 |
|
|
^ │ |
|
|
{1:~ }│{1:~ }|*11
|
|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
|
:Replace foo bar |
|
|
]])
|
|
end)
|
|
|
|
it('works with inccommand=split', function()
|
|
command('set inccommand=split')
|
|
feed(':Replace foo bar')
|
|
screen:expect([[
|
|
bar baz {10:bar} │ {10:bar} bar baz |
|
|
baz {10:bar} bar │ bar baz {10:bar} |
|
|
{10:bar} bar baz │ baz {10:bar} bar |
|
|
│ |
|
|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
|
Buffer #1: |
|
|
|1| {10:bar} bar baz |
|
|
|2| bar baz {10:bar} |
|
|
|3| baz {10:bar} bar |
|
|
Buffer #2: |
|
|
|1| bar baz {10:bar} |
|
|
|2| baz {10:bar} bar |
|
|
|3| {10:bar} bar baz |
|
|
|
|
|
{1:~ }|
|
|
{2:[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 |
|
|
^ │ |
|
|
{1:~ }│{1:~ }|*11
|
|
{3:[No Name] [+] }{2:[No Name] [+] }|
|
|
:Replace foo bar |
|
|
]])
|
|
end)
|
|
end)
|