Merge pull request #16585 from zeertzjq/lua-paste-eol

vim.paste() fixes
This commit is contained in:
zeertzjq
2022-03-15 18:41:03 +08:00
committed by GitHub
3 changed files with 477 additions and 70 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, trailing_nl = 0, 0, false, 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.
@@ -156,44 +156,80 @@ do
--- - 3: ends the paste (exactly once) --- - 3: ends the paste (exactly once)
---@returns false if client should cancel the paste. ---@returns false if client should cancel the paste.
function vim.paste(lines, phase) function vim.paste(lines, phase)
local call = vim.api.nvim_call_function
local now = vim.loop.now() local now = vim.loop.now()
local mode = call('mode', {}):sub(1,1) local is_first_chunk = phase < 2
if phase < 2 then -- Reset flags. local is_last_chunk = phase == -1 or phase == 3
tdots, tick, got_line1 = now, 0, false if is_first_chunk then -- Reset flags.
elseif mode ~= 'c' then tdots, tick, got_line1, undo_started, trailing_nl = now, 0, false, 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
-- 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 not got_line1 then
got_line1 = (#lines > 1)
vim.api.nvim_set_option('paste', true) -- For nvim_input().
-- Escape "<" and control characters
local line1 = lines[1]:gsub('<', '<lt>'):gsub('(%c)', '\022%1')
vim.api.nvim_input(line1)
vim.api.nvim_set_option('paste', false)
end
return true
end
local mode = vim.api.nvim_get_mode().mode
if undo_started then
vim.api.nvim_command('undojoin') vim.api.nvim_command('undojoin')
end end
if mode == 'c' and not got_line1 then -- cmdline-mode: paste only 1 line. if mode:find('^i') or mode:find('^n?t') then -- Insert mode or Terminal buffer
got_line1 = (#lines > 1) vim.api.nvim_put(lines, 'c', false, true)
vim.api.nvim_set_option('paste', true) -- For nvim_input(). elseif phase < 2 and mode:find('^R') and not mode:find('^Rv') then -- Replace mode
local line1 = lines[1]:gsub('<', '<lt>'):gsub('[\r\n\012\027]', ' ') -- Scrub. -- TODO: implement Replace mode streamed pasting
vim.api.nvim_input(line1) -- TODO: support Virtual Replace mode
vim.api.nvim_set_option('paste', false) local nchars = 0
elseif mode ~= 'c' then for _, line in ipairs(lines) do
if phase < 2 and mode:find('^[vV\22sS\19]') then nchars = nchars + line:len()
vim.api.nvim_command([[exe "normal! \<Del>"]])
vim.api.nvim_put(lines, 'c', false, true)
elseif phase < 2 and not mode:find('^[iRt]') then
vim.api.nvim_put(lines, 'c', true, true)
-- XXX: Normal-mode: workaround bad cursor-placement after first chunk.
vim.api.nvim_command('normal! a')
elseif phase < 2 and mode == 'R' then
local nchars = 0
for _, line in ipairs(lines) do
nchars = nchars + line:len()
end
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local bufline = vim.api.nvim_buf_get_lines(0, row-1, row, true)[1]
local firstline = lines[1]
firstline = bufline:sub(1, col)..firstline
lines[1] = firstline
lines[#lines] = lines[#lines]..bufline:sub(col + nchars + 1, bufline:len())
vim.api.nvim_buf_set_lines(0, row-1, row, false, lines)
else
vim.api.nvim_put(lines, 'c', false, true)
end end
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local bufline = vim.api.nvim_buf_get_lines(0, row-1, row, true)[1]
local firstline = lines[1]
firstline = bufline:sub(1, col)..firstline
lines[1] = firstline
lines[#lines] = lines[#lines]..bufline:sub(col + nchars + 1, bufline:len())
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
if mode:find('^n') then -- Normal mode
-- When there was a trailing new line in the previous chunk,
-- the cursor is on the first character of the next line,
-- so paste before the cursor instead of after it.
vim.api.nvim_put(lines, 'c', not trailing_nl, false)
else -- Visual or Select mode
vim.api.nvim_command([[exe "silent normal! \<Del>"]])
local del_start = vim.fn.getpos("'[")
local cursor_pos = vim.fn.getpos('.')
if mode:find('^[VS]') then -- linewise
if cursor_pos[2] < del_start[2] then -- replacing lines at eof
-- create a new line
vim.api.nvim_put({''}, 'l', true, true)
end
vim.api.nvim_put(lines, 'c', false, false)
else
-- paste after cursor when replacing text at eol, otherwise paste before cursor
vim.api.nvim_put(lines, 'c', cursor_pos[3] < del_start[3], false)
end
end
-- put cursor at the end of the text instead of one character after it
vim.fn.setpos('.', vim.fn.getpos("']"))
trailing_nl = lines[#lines] == ''
else -- Don't know what to do in other modes
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
@@ -202,7 +238,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

@@ -636,34 +636,374 @@ describe('API', function()
eq('Invalid phase: 4', eq('Invalid phase: 4',
pcall_err(request, 'nvim_paste', 'foo', true, 4)) pcall_err(request, 'nvim_paste', 'foo', true, 4))
end) end)
it('stream: multiple chunks form one undo-block', function() local function run_streamed_paste_tests()
nvim('paste', '1/chunk 1 (start)\n', true, 1) it('stream: multiple chunks form one undo-block', function()
nvim('paste', '1/chunk 2 (end)\n', true, 3) nvim('paste', '1/chunk 1 (start)\n', true, 1)
local expected1 = [[ nvim('paste', '1/chunk 2 (end)\n', true, 3)
1/chunk 1 (start) local expected1 = [[
1/chunk 2 (end) 1/chunk 1 (start)
]] 1/chunk 2 (end)
expect(expected1) ]]
nvim('paste', '2/chunk 1 (start)\n', true, 1) expect(expected1)
nvim('paste', '2/chunk 2\n', true, 2) nvim('paste', '2/chunk 1 (start)\n', true, 1)
expect([[ nvim('paste', '2/chunk 2\n', true, 2)
1/chunk 1 (start) expect([[
1/chunk 2 (end) 1/chunk 1 (start)
2/chunk 1 (start) 1/chunk 2 (end)
2/chunk 2 2/chunk 1 (start)
]]) 2/chunk 2
nvim('paste', '2/chunk 3\n', true, 2) ]])
nvim('paste', '2/chunk 4 (end)\n', true, 3) nvim('paste', '2/chunk 3\n', true, 2)
expect([[ nvim('paste', '2/chunk 4 (end)\n', true, 3)
1/chunk 1 (start) expect([[
1/chunk 2 (end) 1/chunk 1 (start)
2/chunk 1 (start) 1/chunk 2 (end)
2/chunk 2 2/chunk 1 (start)
2/chunk 3 2/chunk 2
2/chunk 4 (end) 2/chunk 3
]]) 2/chunk 4 (end)
feed('u') -- Undo. ]])
expect(expected1) feed('u') -- Undo.
expect(expected1)
end)
it('stream: Insert mode', function()
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
feed('i')
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('aaaaaabbbbbbccccccdddddd')
feed('<Esc>u')
expect('')
end)
describe('stream: Normal mode', function()
describe('on empty line', function()
before_each(function()
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
end)
after_each(function()
feed('u')
expect('')
end)
it('pasting one line', function()
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('aaaaaabbbbbbccccccdddddd')
end)
it('pasting multiple lines', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
aaaaaa
bbbbbb
cccccc
dddddd]])
end)
end)
describe('not at the end of a line', function()
before_each(function()
feed('i||<Esc>')
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
feed('0')
end)
after_each(function()
feed('u')
expect('||')
end)
it('pasting one line', function()
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('|aaaaaabbbbbbccccccdddddd|')
end)
it('pasting multiple lines', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
|aaaaaa
bbbbbb
cccccc
dddddd|]])
end)
end)
describe('at the end of a line', function()
before_each(function()
feed('i||<Esc>')
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
feed('2|')
end)
after_each(function()
feed('u')
expect('||')
end)
it('pasting one line', function()
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||aaaaaabbbbbbccccccdddddd')
end)
it('pasting multiple lines', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
||aaaaaa
bbbbbb
cccccc
dddddd]])
end)
end)
end)
describe('stream: Visual mode', function()
describe('neither end at the end of a line', function()
before_each(function()
feed('i|xxx<CR>xxx|<Esc>')
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
feed('3|vhk')
end)
after_each(function()
feed('u')
expect([[
|xxx
xxx|]])
end)
it('with non-empty chunks', function()
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('|aaaaaabbbbbbccccccdddddd|')
end)
it('with empty first chunk', function()
nvim('paste', '', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('|bbbbbbccccccdddddd|')
end)
it('with all chunks empty', function()
nvim('paste', '', false, 1)
nvim('paste', '', false, 2)
nvim('paste', '', false, 2)
nvim('paste', '', false, 3)
expect('||')
end)
end)
describe('cursor at the end of a line', function()
before_each(function()
feed('i||xxx<CR>xxx<Esc>')
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
feed('3|vko')
end)
after_each(function()
feed('u')
expect([[
||xxx
xxx]])
end)
it('with non-empty chunks', function()
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||aaaaaabbbbbbccccccdddddd')
end)
it('with empty first chunk', function()
nvim('paste', '', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||bbbbbbccccccdddddd')
end)
end)
describe('other end at the end of a line', function()
before_each(function()
feed('i||xxx<CR>xxx<Esc>')
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
feed('3|vk')
end)
after_each(function()
feed('u')
expect([[
||xxx
xxx]])
end)
it('with non-empty chunks', function()
nvim('paste', 'aaaaaa', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||aaaaaabbbbbbccccccdddddd')
end)
it('with empty first chunk', function()
nvim('paste', '', false, 1)
nvim('paste', 'bbbbbb', false, 2)
nvim('paste', 'cccccc', false, 2)
nvim('paste', 'dddddd', false, 3)
expect('||bbbbbbccccccdddddd')
end)
end)
end)
describe('stream: linewise Visual mode', function()
before_each(function()
feed('i123456789<CR>987654321<CR>123456789<Esc>')
-- If nvim_paste() calls :undojoin without making any changes, this makes it an error.
feed('afoo<Esc>u')
end)
after_each(function()
feed('u')
expect([[
123456789
987654321
123456789]])
end)
describe('selecting the start of a file', function()
before_each(function()
feed('ggV')
end)
it('pasting text without final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
aaaaaa
bbbbbb
cccccc
dddddd987654321
123456789]])
end)
it('pasting text with final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd\n', false, 3)
expect([[
aaaaaa
bbbbbb
cccccc
dddddd
987654321
123456789]])
end)
end)
describe('selecting the middle of a file', function()
before_each(function()
feed('2ggV')
end)
it('pasting text without final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
123456789
aaaaaa
bbbbbb
cccccc
dddddd123456789]])
end)
it('pasting text with final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd\n', false, 3)
expect([[
123456789
aaaaaa
bbbbbb
cccccc
dddddd
123456789]])
end)
end)
describe('selecting the end of a file', function()
before_each(function()
feed('3ggV')
end)
it('pasting text without final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
123456789
987654321
aaaaaa
bbbbbb
cccccc
dddddd]])
end)
it('pasting text with final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd\n', false, 3)
expect([[
123456789
987654321
aaaaaa
bbbbbb
cccccc
dddddd
]])
end)
end)
describe('selecting the whole file', function()
before_each(function()
feed('ggVG')
end)
it('pasting text without final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd', false, 3)
expect([[
aaaaaa
bbbbbb
cccccc
dddddd]])
end)
it('pasting text with final new line', function()
nvim('paste', 'aaaaaa\n', false, 1)
nvim('paste', 'bbbbbb\n', false, 2)
nvim('paste', 'cccccc\n', false, 2)
nvim('paste', 'dddddd\n', false, 3)
expect([[
aaaaaa
bbbbbb
cccccc
dddddd
]])
end)
end)
end)
end
describe('without virtualedit,', function()
run_streamed_paste_tests()
end)
describe('with virtualedit=onemore,', function()
before_each(function()
command('set virtualedit=onemore')
end)
run_streamed_paste_tests()
end) end)
it('non-streaming', function() it('non-streaming', function()
-- With final "\n". -- With final "\n".
@@ -738,6 +1078,37 @@ describe('API', function()
eeffgghh eeffgghh
iijjkkll]]) iijjkkll]])
end) end)
it('when searching in Visual mode', function()
feed('v/')
nvim('paste', 'aabbccdd', true, -1)
eq('aabbccdd', funcs.getcmdline())
expect('')
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('pasting text with control characters in Cmdline mode', function()
local screen = Screen.new(20, 4)
screen:attach()
feed(':')
nvim('paste', 'normal! \023\022\006\027', true, -1)
screen:expect([[
|
~ |
~ |
:normal! ^W^V^F^[^ |
]])
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')

View File

@@ -323,7 +323,7 @@ describe('TUI', function()
feed_data('just paste it™') feed_data('just paste it™')
feed_data('\027[201~') feed_data('\027[201~')
screen:expect{grid=[[ screen:expect{grid=[[
thisjust paste it{1:3} is here | thisjust paste it{1:™}3 is here |
| |
{4:~ }| {4:~ }|
{4:~ }| {4:~ }|
@@ -379,7 +379,7 @@ describe('TUI', function()
end) end)
it('paste: normal-mode (+CRLF #10872)', function() it('paste: normal-mode (+CRLF #10872)', function()
feed_data(':set ruler') feed_data(':set ruler | echo')
wait_for_mode('c') wait_for_mode('c')
feed_data('\n') feed_data('\n')
wait_for_mode('n') wait_for_mode('n')
@@ -423,13 +423,13 @@ describe('TUI', function()
expect_child_buf_lines(expected_crlf) expect_child_buf_lines(expected_crlf)
feed_data('u') feed_data('u')
expect_child_buf_lines({''}) expect_child_buf_lines({''})
feed_data(':echo')
wait_for_mode('c')
feed_data('\n')
wait_for_mode('n')
-- CRLF input -- CRLF input
feed_data('\027[200~'..table.concat(expected_lf,'\r\n')..'\027[201~') feed_data('\027[200~'..table.concat(expected_lf,'\r\n')..'\027[201~')
screen:expect{ screen:expect{grid=expected_grid1, attr_ids=expected_attr}
grid=expected_grid1:gsub(
':set ruler *',
'3 fewer lines; before #1 0 seconds ago '),
attr_ids=expected_attr}
expect_child_buf_lines(expected_crlf) expect_child_buf_lines(expected_crlf)
end) end)