fix(paste): avoid edges cases caused by empty chunk

This commit is contained in:
zeertzjq 2022-03-06 06:56:24 +08:00
parent 21ba2d81a8
commit fcc6f66cf2
2 changed files with 80 additions and 6 deletions

View File

@ -128,7 +128,7 @@ local function inspect(object, options) -- luacheck: no unused
end end
do do
local tdots, tick, got_line1 = 0, 0, false local tdots, tick, got_line1, undo_started = 0, 0, false, false
--- Paste handler, invoked by |nvim_paste()| when a conforming UI --- Paste handler, invoked by |nvim_paste()| when a conforming UI
--- (such as the |TUI|) pastes text into the editor. --- (such as the |TUI|) pastes text into the editor.
@ -158,8 +158,17 @@ do
function vim.paste(lines, phase) function vim.paste(lines, phase)
local now = vim.loop.now() local now = vim.loop.now()
local is_first_chunk = phase < 2 local is_first_chunk = phase < 2
local is_last_chunk = phase == -1 or phase == 3
if is_first_chunk then -- Reset flags. if is_first_chunk then -- Reset flags.
tdots, tick, got_line1 = now, 0, false tdots, tick, got_line1, undo_started = now, 0, false, false
end
if #lines == 0 then
lines = {''}
end
if #lines == 1 and lines[1] == '' and not is_last_chunk then
-- An empty chunk can cause some edge cases in streamed pasting,
-- so don't do anything unless it is the last chunk.
return true
end end
-- Note: mode doesn't always start with "c" in cmdline mode, so use getcmdtype() instead. -- Note: mode doesn't always start with "c" in cmdline mode, so use getcmdtype() instead.
if vim.fn.getcmdtype() ~= '' then -- cmdline-mode: paste only 1 line. if vim.fn.getcmdtype() ~= '' then -- cmdline-mode: paste only 1 line.
@ -173,7 +182,7 @@ do
return true return true
end end
local mode = vim.api.nvim_get_mode().mode local mode = vim.api.nvim_get_mode().mode
if not is_first_chunk then if undo_started then
vim.api.nvim_command('undojoin') vim.api.nvim_command('undojoin')
end end
if mode:find('^i') or mode:find('^n?t') then -- Insert mode or Terminal buffer if mode:find('^i') or mode:find('^n?t') then -- Insert mode or Terminal buffer
@ -190,7 +199,6 @@ do
local firstline = lines[1] local firstline = lines[1]
firstline = bufline:sub(1, col)..firstline firstline = bufline:sub(1, col)..firstline
lines[1] = firstline lines[1] = firstline
-- FIXME: #lines can be 0
lines[#lines] = lines[#lines]..bufline:sub(col + nchars + 1, bufline:len()) lines[#lines] = lines[#lines]..bufline:sub(col + nchars + 1, bufline:len())
vim.api.nvim_buf_set_lines(0, row-1, row, false, lines) vim.api.nvim_buf_set_lines(0, row-1, row, false, lines)
elseif mode:find('^[nvV\22sS\19]') then -- Normal or Visual or Select mode elseif mode:find('^[nvV\22sS\19]') then -- Normal or Visual or Select mode
@ -216,6 +224,7 @@ do
else -- Don't know what to do in other modes else -- Don't know what to do in other modes
return false return false
end end
undo_started = true
if phase ~= -1 and (now - tdots >= 100) then if phase ~= -1 and (now - tdots >= 100) then
local dots = ('.'):rep(tick % 4) local dots = ('.'):rep(tick % 4)
tdots = now tdots = now
@ -224,7 +233,7 @@ do
-- message when there are zero dots. -- message when there are zero dots.
vim.api.nvim_command(('echo "%s"'):format(dots)) vim.api.nvim_command(('echo "%s"'):format(dots))
end end
if phase == -1 or phase == 3 then if is_last_chunk then
vim.api.nvim_command('redraw'..(tick > 1 and '|echo ""' or '')) vim.api.nvim_command('redraw'..(tick > 1 and '|echo ""' or ''))
end end
return true -- Paste will not continue if not returning `true`. return true -- Paste will not continue if not returning `true`.

View File

@ -630,6 +630,10 @@ describe('API', function()
end) end)
describe('nvim_paste', function() describe('nvim_paste', function()
before_each(function()
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('ifoo<Esc>u')
end)
it('validates args', function() it('validates args', function()
eq('Invalid phase: -2', eq('Invalid phase: -2',
pcall_err(request, 'nvim_paste', 'foo', true, -2)) pcall_err(request, 'nvim_paste', 'foo', true, -2))
@ -702,7 +706,7 @@ describe('API', function()
feed('u') feed('u')
expect('||') expect('||')
end) end)
it('stream: Visual mode either end not at the end of a line', function() it('stream: Visual mode neither end at the end of a line', function()
feed('i|xxx<CR>xxx|<Esc>hvhk') feed('i|xxx<CR>xxx|<Esc>hvhk')
nvim('paste', 'aaaaaa', false, 1) nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2) nvim('paste', 'bbbbbb', false, 2)
@ -714,6 +718,30 @@ describe('API', function()
|xxx |xxx
xxx|]]) xxx|]])
end) end)
it('stream: Visual mode neither end at the end of a line with empty first chunk', function()
feed('i|xxx<CR>xxx|<Esc>hvhk')
nvim('paste', '', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('|bbbbbbccccccdddddd|')
feed('u')
expect([[
|xxx
xxx|]])
end)
it('stream: Visual mode neither end at the end of a line with all chunks empty', function()
feed('i|xxx<CR>xxx|<Esc>hvhk')
nvim('paste', '', false, 1)
nvim('paste', '', false, 2)
nvim('paste', '', false, 2)
nvim('paste', '', false, 3)
expect('||')
feed('u')
expect([[
|xxx
xxx|]])
end)
it('stream: Visual mode cursor at the end of a line', function() it('stream: Visual mode cursor at the end of a line', function()
feed('i||xxx<CR>xxx<Esc>vko') feed('i||xxx<CR>xxx<Esc>vko')
nvim('paste', 'aaaaaa', false, 1) nvim('paste', 'aaaaaa', false, 1)
@ -726,6 +754,18 @@ describe('API', function()
||xxx ||xxx
xxx]]) xxx]])
end) end)
it('stream: Visual mode cursor at the end of a line with empty first chunk', function()
feed('i||xxx<CR>xxx<Esc>vko')
nvim('paste', '', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||bbbbbbccccccdddddd')
feed('u')
expect([[
||xxx
xxx]])
end)
it('stream: Visual mode other end at the end of a line', function() it('stream: Visual mode other end at the end of a line', function()
feed('i||xxx<CR>xxx<Esc>vk') feed('i||xxx<CR>xxx<Esc>vk')
nvim('paste', 'aaaaaa', false, 1) nvim('paste', 'aaaaaa', false, 1)
@ -738,6 +778,18 @@ describe('API', function()
||xxx ||xxx
xxx]]) xxx]])
end) end)
it('stream: Visual mode other end at the end of a line with empty first chunk', function()
feed('i||xxx<CR>xxx<Esc>vk')
nvim('paste', '', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||bbbbbbccccccdddddd')
feed('u')
expect([[
||xxx
xxx]])
end)
it('non-streaming', function() it('non-streaming', function()
-- With final "\n". -- With final "\n".
nvim('paste', 'line 1\nline 2\nline 3\n', true, -1) nvim('paste', 'line 1\nline 2\nline 3\n', true, -1)
@ -817,6 +869,19 @@ describe('API', function()
eq('aabbccdd', funcs.getcmdline()) eq('aabbccdd', funcs.getcmdline())
expect('') expect('')
end) end)
it('pasting with empty last chunk in Cmdline mode', function()
local screen = Screen.new(20, 4)
screen:attach()
feed(':')
nvim('paste', 'Foo', true, 1)
nvim('paste', '', true, 3)
screen:expect([[
|
~ |
~ |
:Foo^ |
]])
end)
it('crlf=false does not break lines at CR, CRLF', function() it('crlf=false does not break lines at CR, CRLF', function()
nvim('paste', 'line 1\r\n\r\rline 2\nline 3\rline 4\r', false, -1) nvim('paste', 'line 1\r\n\r\rline 2\nline 3\rline 4\r', false, -1)
expect('line 1\r\n\r\rline 2\nline 3\rline 4\r') expect('line 1\r\n\r\rline 2\nline 3\rline 4\r')