mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
Merge #5782 'Visual-mode put from @. register'
This commit is contained in:
commit
043d8ba422
@ -1517,10 +1517,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
|
||||
coladvance(curwin->w_curswant);
|
||||
}
|
||||
cap->count0 = redo_VIsual_count;
|
||||
if (redo_VIsual_count != 0)
|
||||
cap->count1 = redo_VIsual_count;
|
||||
else
|
||||
cap->count1 = 1;
|
||||
cap->count1 = (cap->count0 == 0 ? 1 : cap->count0);
|
||||
} else if (VIsual_active) {
|
||||
if (!gui_yank) {
|
||||
/* Save the current VIsual area for '< and '> marks, and "gv" */
|
||||
@ -7727,7 +7724,12 @@ static void nv_put(cmdarg_T *cap)
|
||||
savereg = copy_register(regname);
|
||||
}
|
||||
|
||||
/* Now delete the selected text. */
|
||||
// To place the cursor correctly after a blockwise put, and to leave the
|
||||
// text in the correct position when putting over a selection with
|
||||
// 'virtualedit' and past the end of the line, we use the 'c' operator in
|
||||
// do_put(), which requires the visual selection to still be active.
|
||||
if (!VIsual_active || VIsual_mode == 'V' || regname != '.') {
|
||||
// Now delete the selected text.
|
||||
cap->cmdchar = 'd';
|
||||
cap->nchar = NUL;
|
||||
cap->oap->regname = NUL;
|
||||
@ -7735,8 +7737,9 @@ static void nv_put(cmdarg_T *cap)
|
||||
do_pending_operator(cap, 0, false);
|
||||
empty = (curbuf->b_ml.ml_flags & ML_EMPTY);
|
||||
|
||||
/* delete PUT_LINE_BACKWARD; */
|
||||
// delete PUT_LINE_BACKWARD;
|
||||
cap->oap->regname = regname;
|
||||
}
|
||||
|
||||
/* When deleted a linewise Visual area, put the register as
|
||||
* lines to avoid it joined with the next line. When deletion was
|
||||
|
102
src/nvim/ops.c
102
src/nvim/ops.c
@ -2637,12 +2637,79 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
|
||||
* special characters (newlines, etc.).
|
||||
*/
|
||||
if (regname == '.') {
|
||||
(void)stuff_inserted((dir == FORWARD ? (count == -1 ? 'o' : 'a') :
|
||||
(count == -1 ? 'O' : 'i')), count, FALSE);
|
||||
/* Putting the text is done later, so can't really move the cursor to
|
||||
* the next character. Use "l" to simulate it. */
|
||||
if ((flags & PUT_CURSEND) && gchar_cursor() != NUL)
|
||||
bool non_linewise_vis = (VIsual_active && VIsual_mode != 'V');
|
||||
|
||||
// PUT_LINE has special handling below which means we use 'i' to start.
|
||||
char command_start_char = non_linewise_vis ? 'c' :
|
||||
(flags & PUT_LINE ? 'i' : (dir == FORWARD ? 'a' : 'i'));
|
||||
|
||||
// To avoid 'autoindent' on linewise puts, create a new line with `:put _`.
|
||||
if (flags & PUT_LINE) {
|
||||
do_put('_', NULL, dir, 1, PUT_LINE);
|
||||
}
|
||||
|
||||
// If given a count when putting linewise, we stuff the readbuf with the
|
||||
// dot register 'count' times split by newlines.
|
||||
if (flags & PUT_LINE) {
|
||||
stuffcharReadbuff(command_start_char);
|
||||
for (; count > 0; count--) {
|
||||
(void)stuff_inserted(NUL, 1, count != 1);
|
||||
if (count != 1) {
|
||||
// To avoid 'autoindent' affecting the text, use Ctrl_U to remove any
|
||||
// whitespace. Can't just insert Ctrl_U into readbuf1, this would go
|
||||
// back to the previous line in the case of 'noautoindent' and
|
||||
// 'backspace' includes "eol". So we insert a dummy space for Ctrl_U
|
||||
// to consume.
|
||||
stuffReadbuff((char_u *)"\n ");
|
||||
stuffcharReadbuff(Ctrl_U);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(void)stuff_inserted(command_start_char, count, false);
|
||||
}
|
||||
|
||||
// Putting the text is done later, so can't move the cursor to the next
|
||||
// character. Simulate it with motion commands after the insert.
|
||||
if (flags & PUT_CURSEND) {
|
||||
if (flags & PUT_LINE) {
|
||||
stuffReadbuff((char_u *)"j0");
|
||||
} else {
|
||||
// Avoid ringing the bell from attempting to move into the space after
|
||||
// the current line. We can stuff the readbuffer with "l" if:
|
||||
// 1) 'virtualedit' is "all" or "onemore"
|
||||
// 2) We are not at the end of the line
|
||||
// 3) We are not (one past the end of the line && on the last line)
|
||||
// This allows a visual put over a selection one past the end of the
|
||||
// line joining the current line with the one below.
|
||||
|
||||
// curwin->w_cursor.col marks the byte position of the cursor in the
|
||||
// currunt line. It increases up to a max of
|
||||
// STRLEN(ml_get(curwin->w_cursor.lnum)). With 'virtualedit' and the
|
||||
// cursor past the end of the line, curwin->w_cursor.coladd is
|
||||
// incremented instead of curwin->w_cursor.col.
|
||||
char_u *cursor_pos = get_cursor_pos_ptr();
|
||||
bool one_past_line = (*cursor_pos == NUL);
|
||||
bool eol = false;
|
||||
if (!one_past_line) {
|
||||
eol = (*(cursor_pos + mb_ptr2len(cursor_pos)) == NUL);
|
||||
}
|
||||
|
||||
bool ve_allows = (ve_flags == VE_ALL || ve_flags == VE_ONEMORE);
|
||||
bool eof = curbuf->b_ml.ml_line_count == curwin->w_cursor.lnum
|
||||
&& one_past_line;
|
||||
if (ve_allows || !(eol || eof)) {
|
||||
stuffcharReadbuff('l');
|
||||
}
|
||||
}
|
||||
} else if (flags & PUT_LINE) {
|
||||
stuffReadbuff((char_u *)"g'[");
|
||||
}
|
||||
|
||||
// So the 'u' command restores cursor position after ".p, save the cursor
|
||||
// position now (though not saving any text).
|
||||
if (command_start_char == 'a') {
|
||||
u_save(curwin->w_cursor.lnum, curwin->w_cursor.lnum + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2831,14 +2898,12 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
|
||||
else
|
||||
getvcol(curwin, &curwin->w_cursor, NULL, NULL, &col);
|
||||
|
||||
if (has_mbyte)
|
||||
/* move to start of next multi-byte character */
|
||||
// move to start of next multi-byte character
|
||||
curwin->w_cursor.col += (*mb_ptr2len)(get_cursor_pos_ptr());
|
||||
else if (c != TAB || ve_flags != VE_ALL)
|
||||
++curwin->w_cursor.col;
|
||||
++col;
|
||||
} else
|
||||
col++;
|
||||
} else {
|
||||
getvcol(curwin, &curwin->w_cursor, &col, NULL, &endcol2);
|
||||
}
|
||||
|
||||
col += curwin->w_cursor.coladd;
|
||||
if (ve_flags == VE_ALL
|
||||
@ -2892,7 +2957,6 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
|
||||
bd.startspaces = incr - bd.endspaces;
|
||||
--bd.textcol;
|
||||
delcount = 1;
|
||||
if (has_mbyte)
|
||||
bd.textcol -= (*mb_head_off)(oldp, oldp + bd.textcol);
|
||||
if (oldp[bd.textcol] != TAB) {
|
||||
/* Only a Tab can be split into spaces. Other
|
||||
@ -2975,22 +3039,14 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
|
||||
// if type is kMTCharWise, FORWARD is the same as BACKWARD on the next
|
||||
// char
|
||||
if (dir == FORWARD && gchar_cursor() != NUL) {
|
||||
if (has_mbyte) {
|
||||
int bytelen = (*mb_ptr2len)(get_cursor_pos_ptr());
|
||||
|
||||
/* put it on the next of the multi-byte character. */
|
||||
// put it on the next of the multi-byte character.
|
||||
col += bytelen;
|
||||
if (yanklen) {
|
||||
curwin->w_cursor.col += bytelen;
|
||||
curbuf->b_op_end.col += bytelen;
|
||||
}
|
||||
} else {
|
||||
++col;
|
||||
if (yanklen) {
|
||||
++curwin->w_cursor.col;
|
||||
++curbuf->b_op_end.col;
|
||||
}
|
||||
}
|
||||
}
|
||||
curbuf->b_op_start = curwin->w_cursor;
|
||||
}
|
||||
@ -3027,7 +3083,9 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
|
||||
}
|
||||
if (VIsual_active)
|
||||
lnum++;
|
||||
} while (VIsual_active && lnum <= curbuf->b_visual.vi_end.lnum);
|
||||
} while (VIsual_active
|
||||
&& (lnum <= curbuf->b_visual.vi_end.lnum
|
||||
|| lnum <= curbuf->b_visual.vi_start.lnum));
|
||||
|
||||
if (VIsual_active) { /* reset lnum to the last visual line */
|
||||
lnum--;
|
||||
|
@ -13,6 +13,8 @@ local check_logs = global_helpers.check_logs
|
||||
local neq = global_helpers.neq
|
||||
local eq = global_helpers.eq
|
||||
local ok = global_helpers.ok
|
||||
local map = global_helpers.map
|
||||
local filter = global_helpers.filter
|
||||
|
||||
local start_dir = lfs.currentdir()
|
||||
local nvim_prog = os.getenv('NVIM_PROG') or 'build/bin/nvim'
|
||||
@ -570,6 +572,8 @@ return function(after_each)
|
||||
neq = neq,
|
||||
expect = expect,
|
||||
ok = ok,
|
||||
map = map,
|
||||
filter = filter,
|
||||
nvim = nvim,
|
||||
nvim_async = nvim_async,
|
||||
nvim_prog = nvim_prog,
|
||||
|
936
test/functional/normal/put_spec.lua
Normal file
936
test/functional/normal/put_spec.lua
Normal file
@ -0,0 +1,936 @@
|
||||
local Screen = require('test.functional.ui.screen')
|
||||
local helpers = require('test.functional.helpers')(after_each)
|
||||
|
||||
local clear = helpers.clear
|
||||
local insert = helpers.insert
|
||||
local feed = helpers.feed
|
||||
local expect = helpers.expect
|
||||
local eq = helpers.eq
|
||||
local map = helpers.map
|
||||
local filter = helpers.filter
|
||||
local execute = helpers.execute
|
||||
local curbuf_contents = helpers.curbuf_contents
|
||||
local funcs = helpers.funcs
|
||||
local dedent = helpers.dedent
|
||||
local getreg = funcs.getreg
|
||||
|
||||
local function reset()
|
||||
clear()
|
||||
insert([[
|
||||
Line of words 1
|
||||
Line of words 2]])
|
||||
execute('goto 1')
|
||||
feed('itest_string.<esc>u')
|
||||
funcs.setreg('a', 'test_stringa', 'V')
|
||||
funcs.setreg('b', 'test_stringb\ntest_stringb\ntest_stringb', 'b')
|
||||
funcs.setreg('"', 'test_string"', 'v')
|
||||
end
|
||||
|
||||
-- We check the last inserted register ". in each of these tests because it is
|
||||
-- implemented completely differently in do_put().
|
||||
-- It is implemented differently so that control characters and imap'ped
|
||||
-- characters work in the same manner when pasted as when inserted.
|
||||
describe('put command', function()
|
||||
-- Put a call to clear() here to force the connection to the server.
|
||||
-- This means we can use the funcs.*() functions while mangling text before
|
||||
-- the actual tests are run.
|
||||
clear()
|
||||
before_each(reset)
|
||||
|
||||
local function visual_marks_zero()
|
||||
for _,v in pairs(funcs.getpos("'<")) do
|
||||
if v ~= 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for _,v in pairs(funcs.getpos("'>")) do
|
||||
if v ~= 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- {{{ Where test definitions are run
|
||||
local function run_test_variations(test_variations, extra_setup)
|
||||
reset()
|
||||
if extra_setup then extra_setup() end
|
||||
local init_contents = curbuf_contents()
|
||||
local init_cursorpos = funcs.getcurpos()
|
||||
local assert_no_change = function (exception_table, after_undo)
|
||||
expect(init_contents)
|
||||
-- When putting the ". register forwards, undo doesn't move
|
||||
-- the cursor back to where it was before.
|
||||
-- This is because it uses the command character 'a' to
|
||||
-- start the insert, and undo after that leaves the cursor
|
||||
-- one place to the right (unless we were at the end of the
|
||||
-- line when we pasted).
|
||||
if not (exception_table.undo_position and after_undo) then
|
||||
eq(funcs.getcurpos(), init_cursorpos)
|
||||
end
|
||||
end
|
||||
|
||||
for _, test in pairs(test_variations) do
|
||||
it(test.description, function()
|
||||
if extra_setup then extra_setup() end
|
||||
local orig_dotstr = funcs.getreg('.')
|
||||
helpers.ok(visual_marks_zero())
|
||||
-- Make sure every test starts from the same conditions
|
||||
assert_no_change(test.exception_table, false)
|
||||
local was_cli = test.test_action()
|
||||
test.test_assertions(test.exception_table, false)
|
||||
-- Check that undo twice puts us back to the original conditions
|
||||
-- (i.e. puts the cursor and text back to before)
|
||||
feed('u')
|
||||
assert_no_change(test.exception_table, true)
|
||||
|
||||
-- Should not have changed the ". register
|
||||
-- If we paste the ". register with a count we can't avoid
|
||||
-- changing this register, hence avoid this check.
|
||||
if not test.exception_table.dot_reg_changed then
|
||||
eq(funcs.getreg('.'), orig_dotstr)
|
||||
end
|
||||
|
||||
-- Doing something, undoing it, and then redoing it should
|
||||
-- leave us in the same state as just doing it once.
|
||||
-- For :ex actions we want '@:', for normal actions we want '.'
|
||||
|
||||
-- The '.' redo doesn't work for visual put so just exit if
|
||||
-- it was tested.
|
||||
-- We check that visual put was used by checking if the '< and
|
||||
-- '> marks were changed.
|
||||
if not visual_marks_zero() then
|
||||
return
|
||||
end
|
||||
|
||||
if test.exception_table.undo_position then
|
||||
funcs.setpos('.', init_cursorpos)
|
||||
end
|
||||
if was_cli then
|
||||
feed('@:')
|
||||
else
|
||||
feed('.')
|
||||
end
|
||||
|
||||
test.test_assertions(test.exception_table, true)
|
||||
end)
|
||||
end
|
||||
end -- run_test_variations()
|
||||
-- }}}
|
||||
|
||||
local function create_test_defs(test_defs, command_base, command_creator, -- {{{
|
||||
expect_base, expect_creator)
|
||||
local rettab = {}
|
||||
local exceptions
|
||||
for _, v in pairs(test_defs) do
|
||||
if v[4] then
|
||||
exceptions = v[4]
|
||||
else
|
||||
exceptions = {}
|
||||
end
|
||||
table.insert(rettab,
|
||||
{
|
||||
test_action = command_creator(command_base, v[1]),
|
||||
test_assertions = expect_creator(expect_base, v[2]),
|
||||
description = v[3],
|
||||
exception_table = exceptions,
|
||||
})
|
||||
end
|
||||
return rettab
|
||||
end -- create_test_defs() }}}
|
||||
|
||||
local function find_cursor_position(expect_string) -- {{{
|
||||
-- There must only be one occurance of the character 'x' in
|
||||
-- expect_string.
|
||||
-- This function removes that occurance, and returns the position that
|
||||
-- it was in.
|
||||
-- This returns the cursor position that would leave the 'x' in that
|
||||
-- place if we feed 'ix<esc>' and the string existed before it.
|
||||
for linenum, line in pairs(funcs.split(expect_string, '\n', 1)) do
|
||||
local column = line:find('x')
|
||||
if column then
|
||||
return {linenum, column}, expect_string:gsub('x', '')
|
||||
end
|
||||
end
|
||||
end -- find_cursor_position() }}}
|
||||
|
||||
-- Action function creators {{{
|
||||
local function create_p_action(test_map, substitution)
|
||||
local temp_val = test_map:gsub('p', substitution)
|
||||
return function()
|
||||
feed(temp_val)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function create_put_action(command_base, substitution)
|
||||
local temp_val = command_base:gsub('put', substitution)
|
||||
return function()
|
||||
execute(temp_val)
|
||||
return true
|
||||
end
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- Expect function creator {{{
|
||||
local function expect_creator(conversion_function, expect_base, conversion_table)
|
||||
local temp_expect_string = conversion_function(expect_base, conversion_table)
|
||||
local cursor_position, expect_string = find_cursor_position(temp_expect_string)
|
||||
return function(exception_table, after_redo)
|
||||
expect(expect_string)
|
||||
|
||||
-- Have to use getcurpos() instead of curwinmeths.get_cursor() in
|
||||
-- order to account for virtualedit.
|
||||
-- We always want the curswant element in getcurpos(), which is
|
||||
-- sometimes different to the column element in
|
||||
-- curwinmeths.get_cursor().
|
||||
-- NOTE: The ".gp command leaves the cursor after the pasted text
|
||||
-- when running, but does not when the command is redone with the
|
||||
-- '.' command.
|
||||
if not (exception_table.redo_position and after_redo) then
|
||||
local actual_position = funcs.getcurpos()
|
||||
eq(cursor_position, {actual_position[2], actual_position[5]})
|
||||
end
|
||||
end
|
||||
end -- expect_creator() }}}
|
||||
|
||||
-- Test definitions {{{
|
||||
local function copy_def(def)
|
||||
local rettab = { '', {}, '', nil }
|
||||
rettab[1] = def[1]
|
||||
for k,v in pairs(def[2]) do
|
||||
rettab[2][k] = v
|
||||
end
|
||||
rettab[3] = def[3]
|
||||
if def[4] then
|
||||
rettab[4] = {}
|
||||
for k,v in pairs(def[4]) do
|
||||
rettab[4][k] = v
|
||||
end
|
||||
end
|
||||
return rettab
|
||||
end
|
||||
|
||||
local normal_command_defs = {
|
||||
{
|
||||
'p',
|
||||
{cursor_after = false, put_backwards = false, dot_register = false},
|
||||
'pastes after cursor with p',
|
||||
},
|
||||
{
|
||||
'gp',
|
||||
{cursor_after = true, put_backwards = false, dot_register = false},
|
||||
'leaves cursor after text with gp',
|
||||
},
|
||||
{
|
||||
'".p',
|
||||
{cursor_after = false, put_backwards = false, dot_register = true},
|
||||
'works with the ". register',
|
||||
},
|
||||
{
|
||||
'".gp',
|
||||
{cursor_after = true, put_backwards = false, dot_register = true},
|
||||
'gp works with the ". register',
|
||||
{redo_position = true},
|
||||
},
|
||||
{
|
||||
'P',
|
||||
{cursor_after = false, put_backwards = true, dot_register = false},
|
||||
'pastes before cursor with P',
|
||||
},
|
||||
{
|
||||
'gP',
|
||||
{cursor_after = true, put_backwards = true, dot_register = false},
|
||||
'gP pastes before cursor and leaves cursor after text',
|
||||
},
|
||||
{
|
||||
'".P',
|
||||
{cursor_after = false, put_backwards = true, dot_register = true},
|
||||
'P works with ". register',
|
||||
},
|
||||
{
|
||||
'".gP',
|
||||
{cursor_after = true, put_backwards = true, dot_register = true},
|
||||
'gP works with ". register',
|
||||
{redo_position = true},
|
||||
},
|
||||
}
|
||||
|
||||
-- Add a definition applying a count for each definition above.
|
||||
-- Could do this for each transformation (p -> P, p -> gp etc), but I think
|
||||
-- it's neater this way (balance between being explicit and too verbose).
|
||||
for i = 1,#normal_command_defs do
|
||||
local cur = normal_command_defs[i]
|
||||
|
||||
-- Make modified copy of current definition that includes a count.
|
||||
local newdef = copy_def(cur)
|
||||
newdef[2].count = 2
|
||||
cur[2].count = 1
|
||||
newdef[1] = '2' .. newdef[1]
|
||||
newdef[3] = 'double ' .. newdef[3]
|
||||
|
||||
if cur[2].dot_register then
|
||||
if not cur[4] then
|
||||
newdef[4] = {}
|
||||
end
|
||||
newdef[4].dot_reg_changed = true
|
||||
end
|
||||
|
||||
normal_command_defs[#normal_command_defs + 1] = newdef
|
||||
end
|
||||
|
||||
local ex_command_defs = {
|
||||
{
|
||||
'put',
|
||||
{put_backwards = false, dot_register = false},
|
||||
'pastes linewise forwards with :put',
|
||||
},
|
||||
{
|
||||
'put!',
|
||||
{put_backwards = true, dot_register = false},
|
||||
'pastes linewise backwards with :put!',
|
||||
},
|
||||
{
|
||||
'put .',
|
||||
{put_backwards = false, dot_register = true},
|
||||
'pastes linewise with the dot register',
|
||||
},
|
||||
{
|
||||
'put! .',
|
||||
{put_backwards = true, dot_register = true},
|
||||
'pastes linewise backwards with the dot register',
|
||||
},
|
||||
}
|
||||
|
||||
local function non_dotdefs(def_table)
|
||||
return filter(function(d) return not d[2].dot_register end, def_table)
|
||||
end
|
||||
|
||||
-- }}}
|
||||
|
||||
-- Conversion functions {{{
|
||||
local function convert_characterwise(expect_base, conversion_table,
|
||||
virtualedit_end, visual_put)
|
||||
expect_base = dedent(expect_base)
|
||||
-- There is no difference between 'P' and 'p' when VIsual_active
|
||||
if not visual_put then
|
||||
if conversion_table.put_backwards then
|
||||
-- Special case for virtualedit at the end of a line.
|
||||
local replace_string
|
||||
if not virtualedit_end then
|
||||
replace_string = 'test_stringx"%1'
|
||||
else
|
||||
replace_string = 'test_stringx"'
|
||||
end
|
||||
expect_base = expect_base:gsub('(.)test_stringx"', replace_string)
|
||||
end
|
||||
end
|
||||
if conversion_table.count > 1 then
|
||||
local rep_string = 'test_string"'
|
||||
local extra_puts = rep_string:rep(conversion_table.count - 1)
|
||||
expect_base = expect_base:gsub('test_stringx"', extra_puts .. 'test_stringx"')
|
||||
end
|
||||
if conversion_table.cursor_after then
|
||||
expect_base = expect_base:gsub('test_stringx"', 'test_string"x')
|
||||
end
|
||||
if conversion_table.dot_register then
|
||||
expect_base = expect_base:gsub('(test_stringx?)"', '%1.')
|
||||
end
|
||||
return expect_base
|
||||
end -- convert_characterwise()
|
||||
|
||||
local function make_back(string)
|
||||
local prev_line
|
||||
local rettab = {}
|
||||
local string_found = false
|
||||
for _, line in pairs(funcs.split(string, '\n', 1)) do
|
||||
if line:find('test_string') then
|
||||
string_found = true
|
||||
table.insert(rettab, line)
|
||||
else
|
||||
if string_found then
|
||||
if prev_line then
|
||||
table.insert(rettab, prev_line)
|
||||
prev_line = nil
|
||||
end
|
||||
table.insert(rettab, line)
|
||||
else
|
||||
table.insert(rettab, prev_line)
|
||||
prev_line = line
|
||||
end
|
||||
end
|
||||
end
|
||||
-- In case there are no lines after the text that was put.
|
||||
if prev_line and string_found then
|
||||
table.insert(rettab, prev_line)
|
||||
end
|
||||
return table.concat(rettab, '\n')
|
||||
end -- make_back()
|
||||
|
||||
local function convert_linewise(expect_base, conversion_table, _, use_a, indent)
|
||||
expect_base = dedent(expect_base)
|
||||
if conversion_table.put_backwards then
|
||||
expect_base = make_back(expect_base)
|
||||
end
|
||||
local p_str = 'test_string"'
|
||||
if use_a then
|
||||
p_str = 'test_stringa'
|
||||
end
|
||||
|
||||
if conversion_table.dot_register then
|
||||
expect_base = expect_base:gsub('x' .. p_str, 'xtest_string.')
|
||||
p_str = 'test_string.'
|
||||
end
|
||||
|
||||
if conversion_table.cursor_after then
|
||||
expect_base = expect_base:gsub('x' .. p_str .. '\n', p_str .. '\nx')
|
||||
end
|
||||
|
||||
-- The 'indent' argument is only used here because a single put with an
|
||||
-- indent doesn't require special handling. It doesn't require special
|
||||
-- handling because the cursor is never put before the indent, hence
|
||||
-- the modification of 'test_stringx"' gives the same overall answer as
|
||||
-- modifying ' test_stringx"'.
|
||||
|
||||
-- Only happens when using normal mode command actions.
|
||||
if conversion_table.count and conversion_table.count > 1 then
|
||||
if not indent then
|
||||
indent = ''
|
||||
end
|
||||
local rep_string = indent .. p_str .. '\n'
|
||||
local extra_puts = rep_string:rep(conversion_table.count - 1)
|
||||
local orig_string, new_string
|
||||
if conversion_table.cursor_after then
|
||||
orig_string = indent .. p_str .. '\nx'
|
||||
new_string = extra_puts .. orig_string
|
||||
else
|
||||
orig_string = indent .. 'x' .. p_str .. '\n'
|
||||
new_string = orig_string .. extra_puts
|
||||
end
|
||||
expect_base = expect_base:gsub(orig_string, new_string)
|
||||
end
|
||||
return expect_base
|
||||
end
|
||||
|
||||
local function put_x_last(orig_line, p_str)
|
||||
local prev_end, cur_end, cur_start = 0, 0, 0
|
||||
while cur_start do
|
||||
prev_end = cur_end
|
||||
cur_start, cur_end = orig_line:find(p_str, prev_end)
|
||||
end
|
||||
-- Assume (because that is the only way I call it) that p_str matches
|
||||
-- the pattern 'test_string.'
|
||||
return orig_line:sub(1, prev_end - 1) .. 'x' .. orig_line:sub(prev_end)
|
||||
end
|
||||
|
||||
local function convert_blockwise(expect_base, conversion_table, visual,
|
||||
use_b, trailing_whitespace)
|
||||
expect_base = dedent(expect_base)
|
||||
local p_str = 'test_string"'
|
||||
if use_b then
|
||||
p_str = 'test_stringb'
|
||||
end
|
||||
|
||||
if conversion_table.dot_register then
|
||||
expect_base = expect_base:gsub('(x?)' .. p_str, '%1test_string.')
|
||||
-- Looks strange, but the dot is a special character in the pattern
|
||||
-- and a literal character in the replacement.
|
||||
expect_base = expect_base:gsub('test_stringx.', 'test_stringx.')
|
||||
p_str = 'test_string.'
|
||||
end
|
||||
|
||||
-- No difference between 'p' and 'P' in visual mode.
|
||||
if not visual then
|
||||
if conversion_table.put_backwards then
|
||||
-- One for the line where the cursor is left, one for all other
|
||||
-- lines.
|
||||
expect_base = expect_base:gsub('([^x])' .. p_str, p_str .. '%1')
|
||||
expect_base = expect_base:gsub('([^x])x' .. p_str, 'x' .. p_str .. '%1')
|
||||
if not trailing_whitespace then
|
||||
expect_base = expect_base:gsub(' \n', '\n')
|
||||
expect_base = expect_base:gsub(' $', '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if conversion_table.count and conversion_table.count > 1 then
|
||||
local p_pattern = p_str:gsub('%.', '%%.')
|
||||
expect_base = expect_base:gsub(p_pattern,
|
||||
p_str:rep(conversion_table.count))
|
||||
expect_base = expect_base:gsub('test_stringx([b".])',
|
||||
p_str:rep(conversion_table.count - 1)
|
||||
.. '%0')
|
||||
end
|
||||
|
||||
if conversion_table.cursor_after then
|
||||
if not visual then
|
||||
local prev_line
|
||||
local rettab = {}
|
||||
local prev_in_block = false
|
||||
for _, line in pairs(funcs.split(expect_base, '\n', 1)) do
|
||||
if line:find('test_string') then
|
||||
if prev_line then
|
||||
prev_line = prev_line:gsub('x', '')
|
||||
table.insert(rettab, prev_line)
|
||||
end
|
||||
prev_line = line
|
||||
prev_in_block = true
|
||||
else
|
||||
if prev_in_block then
|
||||
prev_line = put_x_last(prev_line, p_str)
|
||||
table.insert(rettab, prev_line)
|
||||
prev_in_block = false
|
||||
end
|
||||
table.insert(rettab, line)
|
||||
end
|
||||
end
|
||||
if prev_line and prev_in_block then
|
||||
table.insert(rettab, put_x_last(prev_line, p_str))
|
||||
end
|
||||
|
||||
expect_base = table.concat(rettab, '\n')
|
||||
else
|
||||
expect_base = expect_base:gsub('x(.)', '%1x')
|
||||
end
|
||||
end
|
||||
|
||||
return expect_base
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- Convenience functions {{{
|
||||
local function run_normal_mode_tests(test_string, base_map, extra_setup,
|
||||
virtualedit_end, selection_string)
|
||||
local function convert_closure(e, c)
|
||||
return convert_characterwise(e, c, virtualedit_end, selection_string)
|
||||
end
|
||||
local function expect_normal_creator(expect_base, conversion_table)
|
||||
local test_expect = expect_creator(convert_closure, expect_base, conversion_table)
|
||||
return function(exception_table, after_redo)
|
||||
test_expect(exception_table, after_redo)
|
||||
if selection_string then
|
||||
eq(getreg('"'), selection_string)
|
||||
else
|
||||
eq(getreg('"'), 'test_string"')
|
||||
end
|
||||
end
|
||||
end
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
normal_command_defs,
|
||||
base_map,
|
||||
create_p_action,
|
||||
test_string,
|
||||
expect_normal_creator
|
||||
),
|
||||
extra_setup
|
||||
)
|
||||
end -- run_normal_mode_tests()
|
||||
|
||||
local function convert_linewiseer(expect_base, conversion_table)
|
||||
return expect_creator(convert_linewise, expect_base, conversion_table)
|
||||
end
|
||||
|
||||
local function run_linewise_tests(expect_base, base_command, extra_setup)
|
||||
local linewise_test_defs = create_test_defs(
|
||||
ex_command_defs, base_command,
|
||||
create_put_action, expect_base, convert_linewiseer)
|
||||
run_test_variations(linewise_test_defs, extra_setup)
|
||||
end -- run_linewise_tests()
|
||||
-- }}}
|
||||
|
||||
-- Actual tests
|
||||
describe('default pasting', function()
|
||||
local expect_string = [[
|
||||
Ltest_stringx"ine of words 1
|
||||
Line of words 2]]
|
||||
run_normal_mode_tests(expect_string, 'p')
|
||||
|
||||
run_linewise_tests([[
|
||||
Line of words 1
|
||||
xtest_string"
|
||||
Line of words 2]],
|
||||
'put'
|
||||
)
|
||||
end)
|
||||
|
||||
describe('linewise register', function()
|
||||
-- put with 'p'
|
||||
local local_ex_command_defs = non_dotdefs(normal_command_defs)
|
||||
local base_expect_string = [[
|
||||
Line of words 1
|
||||
xtest_stringa
|
||||
Line of words 2]]
|
||||
local function local_convert_linewise(expect_base, conversion_table)
|
||||
return convert_linewise(expect_base, conversion_table, nil, true)
|
||||
end
|
||||
local function expect_lineput(expect_base, conversion_table)
|
||||
return expect_creator(local_convert_linewise, expect_base, conversion_table)
|
||||
end
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
local_ex_command_defs,
|
||||
'"ap',
|
||||
create_p_action,
|
||||
base_expect_string,
|
||||
expect_lineput
|
||||
)
|
||||
)
|
||||
|
||||
-- put with :put
|
||||
local linewise_put_defs = non_dotdefs(ex_command_defs)
|
||||
base_expect_string = [[
|
||||
Line of words 1
|
||||
xtest_stringa
|
||||
Line of words 2]]
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
linewise_put_defs,
|
||||
'put a', create_put_action,
|
||||
base_expect_string, convert_linewiseer
|
||||
)
|
||||
)
|
||||
|
||||
end)
|
||||
|
||||
describe('blockwise register', function()
|
||||
local blockwise_put_defs = non_dotdefs(normal_command_defs)
|
||||
local test_base = [[
|
||||
Lxtest_stringbine of words 1
|
||||
Ltest_stringbine of words 2
|
||||
test_stringb]]
|
||||
|
||||
local function expect_block_creator(expect_base, conversion_table)
|
||||
return expect_creator(function(e,c) return convert_blockwise(e,c,nil,true) end,
|
||||
expect_base, conversion_table)
|
||||
end
|
||||
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
blockwise_put_defs,
|
||||
'"bp',
|
||||
create_p_action,
|
||||
test_base,
|
||||
expect_block_creator
|
||||
)
|
||||
)
|
||||
end)
|
||||
|
||||
it('adds correct indentation when put with [p and ]p', function()
|
||||
feed('G>>"a]pix<esc>')
|
||||
-- luacheck: ignore
|
||||
expect([[
|
||||
Line of words 1
|
||||
Line of words 2
|
||||
xtest_stringa]])
|
||||
feed('uu"a[pix<esc>')
|
||||
-- luacheck: ignore
|
||||
expect([[
|
||||
Line of words 1
|
||||
xtest_stringa
|
||||
Line of words 2]])
|
||||
end)
|
||||
|
||||
describe('linewise paste with autoindent', function()
|
||||
-- luacheck: ignore
|
||||
run_linewise_tests([[
|
||||
Line of words 1
|
||||
Line of words 2
|
||||
xtest_string"]],
|
||||
'put'
|
||||
,
|
||||
function()
|
||||
funcs.setline('$', ' Line of words 2')
|
||||
-- Set curswant to '8' to be at the end of the tab character
|
||||
-- This is where the cursor is put back after the 'u' command.
|
||||
funcs.setpos('.', {0, 2, 1, 0, 8})
|
||||
execute('set autoindent')
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
describe('put inside tabs with virtualedit', function()
|
||||
local test_string = [[
|
||||
Line of words 1
|
||||
test_stringx" Line of words 2]]
|
||||
run_normal_mode_tests(test_string, 'p', function()
|
||||
funcs.setline('$', ' Line of words 2')
|
||||
execute('set virtualedit=all')
|
||||
funcs.setpos('.', {0, 2, 1, 2, 3})
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('put after the line with virtualedit', function()
|
||||
local test_string = [[
|
||||
Line of words 1 test_stringx"
|
||||
Line of words 2]]
|
||||
run_normal_mode_tests(test_string, 'p', function()
|
||||
funcs.setline('$', ' Line of words 2')
|
||||
execute('set virtualedit=all')
|
||||
funcs.setpos('.', {0, 1, 16, 1, 17})
|
||||
end, true)
|
||||
end)
|
||||
|
||||
describe('Visual put', function()
|
||||
describe('basic put', function()
|
||||
local test_string = [[
|
||||
test_stringx" words 1
|
||||
Line of words 2]]
|
||||
run_normal_mode_tests(test_string, 'v2ep', nil, nil, 'Line of')
|
||||
end)
|
||||
describe('over trailing newline', function()
|
||||
local test_string = 'Line of test_stringx"Line of words 2'
|
||||
run_normal_mode_tests(test_string, 'v$p', function()
|
||||
funcs.setpos('.', {0, 1, 9, 0, 9})
|
||||
end,
|
||||
nil,
|
||||
'words 1\n')
|
||||
end)
|
||||
describe('linewise mode', function()
|
||||
local test_string = [[
|
||||
xtest_string"
|
||||
Line of words 2]]
|
||||
local function expect_vis_linewise(expect_base, conversion_table)
|
||||
return expect_creator(function(e, c)
|
||||
return convert_linewise(e, c, nil, nil)
|
||||
end,
|
||||
expect_base, conversion_table)
|
||||
end
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
normal_command_defs,
|
||||
'Vp',
|
||||
create_p_action,
|
||||
test_string,
|
||||
expect_vis_linewise
|
||||
),
|
||||
function() funcs.setpos('.', {0, 1, 1, 0, 1}) end
|
||||
)
|
||||
|
||||
describe('with whitespace at bol', function()
|
||||
local function expect_vis_lineindented(expect_base, conversion_table)
|
||||
local test_expect = expect_creator(function(e, c)
|
||||
return convert_linewise(e, c, nil, nil, ' ')
|
||||
end,
|
||||
expect_base, conversion_table)
|
||||
return function(exception_table, after_redo)
|
||||
test_expect(exception_table, after_redo)
|
||||
eq(getreg('"'), 'Line of words 1\n')
|
||||
end
|
||||
end
|
||||
local base_expect_string = [[
|
||||
xtest_string"
|
||||
Line of words 2]]
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
normal_command_defs,
|
||||
'Vp',
|
||||
create_p_action,
|
||||
base_expect_string,
|
||||
expect_vis_lineindented
|
||||
),
|
||||
function()
|
||||
feed('i test_string.<esc>u')
|
||||
funcs.setreg('"', ' test_string"', 'v')
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
describe('blockwise visual mode', function()
|
||||
local test_base = [[
|
||||
test_stringx"e of words 1
|
||||
test_string"e of words 2]]
|
||||
|
||||
local function expect_block_creator(expect_base, conversion_table)
|
||||
local test_expect = expect_creator(function(e, c)
|
||||
return convert_blockwise(e, c, true)
|
||||
end, expect_base, conversion_table)
|
||||
return function(e,c)
|
||||
test_expect(e,c)
|
||||
eq(getreg('"'), 'Lin\nLin')
|
||||
end
|
||||
end
|
||||
|
||||
local select_down_test_defs = create_test_defs(
|
||||
normal_command_defs,
|
||||
'<C-v>jllp',
|
||||
create_p_action,
|
||||
test_base,
|
||||
expect_block_creator
|
||||
)
|
||||
run_test_variations(select_down_test_defs)
|
||||
|
||||
|
||||
-- Undo and redo of a visual block put leave the cursor in the top
|
||||
-- left of the visual block area no matter where the cursor was
|
||||
-- when it started.
|
||||
local undo_redo_no = map(function(table)
|
||||
local rettab = copy_def(table)
|
||||
if not rettab[4] then
|
||||
rettab[4] = {}
|
||||
end
|
||||
rettab[4].undo_position = true
|
||||
rettab[4].redo_position = true
|
||||
return rettab
|
||||
end,
|
||||
normal_command_defs)
|
||||
|
||||
-- Selection direction doesn't matter
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
undo_redo_no,
|
||||
'<C-v>kllp',
|
||||
create_p_action,
|
||||
test_base,
|
||||
expect_block_creator
|
||||
),
|
||||
function() funcs.setpos('.', {0, 2, 1, 0, 1}) end
|
||||
)
|
||||
|
||||
describe('blockwise cursor after undo', function()
|
||||
-- A bit of a hack of the reset above.
|
||||
-- In the tests that selection direction doesn't matter, we
|
||||
-- don't check the undo/redo position because it doesn't fit
|
||||
-- the same pattern as everything else.
|
||||
-- Here we fix this by directly checking the undo/redo position
|
||||
-- in the test_assertions of our test definitions.
|
||||
local function assertion_creator(_,_)
|
||||
return function(_,_)
|
||||
feed('u')
|
||||
-- Have to use feed('u') here to set curswant, because
|
||||
-- ex_undo() doesn't do that.
|
||||
eq(funcs.getcurpos(), {0, 1, 1, 0, 1})
|
||||
feed('<C-r>')
|
||||
eq(funcs.getcurpos(), {0, 1, 1, 0, 1})
|
||||
end
|
||||
end
|
||||
|
||||
run_test_variations(
|
||||
create_test_defs(
|
||||
undo_redo_no,
|
||||
'<C-v>kllp',
|
||||
create_p_action,
|
||||
test_base,
|
||||
assertion_creator
|
||||
),
|
||||
function() funcs.setpos('.', {0, 2, 1, 0, 1}) end
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("with 'virtualedit'", function()
|
||||
describe('splitting a tab character', function()
|
||||
local base_expect_string = [[
|
||||
Line of words 1
|
||||
test_stringx" Line of words 2]]
|
||||
run_normal_mode_tests(
|
||||
base_expect_string,
|
||||
'vp',
|
||||
function()
|
||||
funcs.setline('$', ' Line of words 2')
|
||||
execute('set virtualedit=all')
|
||||
funcs.setpos('.', {0, 2, 1, 2, 3})
|
||||
end,
|
||||
nil,
|
||||
' '
|
||||
)
|
||||
end)
|
||||
describe('after end of line', function()
|
||||
local base_expect_string = [[
|
||||
Line of words 1 test_stringx"
|
||||
Line of words 2]]
|
||||
run_normal_mode_tests(
|
||||
base_expect_string,
|
||||
'vp',
|
||||
function()
|
||||
execute('set virtualedit=all')
|
||||
funcs.setpos('.', {0, 1, 16, 2, 18})
|
||||
end,
|
||||
true,
|
||||
' '
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('. register special tests', function()
|
||||
before_each(reset)
|
||||
it('applies control character actions', function()
|
||||
feed('i<C-t><esc>u')
|
||||
expect([[
|
||||
Line of words 1
|
||||
Line of words 2]])
|
||||
feed('".p')
|
||||
expect([[
|
||||
Line of words 1
|
||||
Line of words 2]])
|
||||
feed('u1go<C-v>j".p')
|
||||
eq([[
|
||||
ine of words 1
|
||||
ine of words 2]], curbuf_contents())
|
||||
end)
|
||||
|
||||
local function bell_test(actions, should_ring)
|
||||
local screen = Screen.new()
|
||||
screen:attach()
|
||||
helpers.ok(not screen.bell and not screen.visualbell)
|
||||
actions()
|
||||
helpers.wait()
|
||||
screen:wait(function()
|
||||
if should_ring then
|
||||
if not screen.bell and not screen.visualbell then
|
||||
return 'Bell was not rung after action'
|
||||
end
|
||||
else
|
||||
if screen.bell or screen.visualbell then
|
||||
return 'Bell was rung after action'
|
||||
end
|
||||
end
|
||||
end)
|
||||
screen:detach()
|
||||
end
|
||||
|
||||
it('should not ring the bell with gp at end of line', function()
|
||||
bell_test(function() feed('$".gp') end)
|
||||
|
||||
-- Even if the last character is a multibyte character.
|
||||
reset()
|
||||
funcs.setline(1, 'helloม')
|
||||
bell_test(function() feed('$".gp') end)
|
||||
end)
|
||||
|
||||
it('should not ring the bell with gp and end of file', function()
|
||||
funcs.setpos('.', {0, 2, 1, 0})
|
||||
bell_test(function() feed('$vl".gp') end)
|
||||
end)
|
||||
|
||||
it('should ring the bell when deleting if not appropriate', function()
|
||||
execute('goto 2')
|
||||
feed('i<bs><esc>')
|
||||
expect([[
|
||||
ine of words 1
|
||||
Line of words 2]])
|
||||
bell_test(function() feed('".P') end, true)
|
||||
end)
|
||||
|
||||
it('should restore cursor position after undo of ".p', function()
|
||||
local origpos = funcs.getcurpos()
|
||||
feed('".pu')
|
||||
eq(origpos, funcs.getcurpos())
|
||||
end)
|
||||
|
||||
it("should be unaffected by 'autoindent' with V\".2p", function()
|
||||
execute('set autoindent')
|
||||
feed('i test_string.<esc>u')
|
||||
feed('V".2p')
|
||||
expect([[
|
||||
test_string.
|
||||
test_string.
|
||||
Line of words 2]])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
@ -91,6 +91,24 @@ local function tmpname()
|
||||
end
|
||||
end
|
||||
|
||||
local function map(func, tab)
|
||||
local rettab = {}
|
||||
for k, v in pairs(tab) do
|
||||
rettab[k] = func(v)
|
||||
end
|
||||
return rettab
|
||||
end
|
||||
|
||||
local function filter(filter_func, tab)
|
||||
local rettab = {}
|
||||
for _, entry in pairs(tab) do
|
||||
if filter_func(entry) then
|
||||
table.insert(rettab, entry)
|
||||
end
|
||||
end
|
||||
return rettab
|
||||
end
|
||||
|
||||
return {
|
||||
eq = eq,
|
||||
neq = neq,
|
||||
@ -98,4 +116,6 @@ return {
|
||||
check_logs = check_logs,
|
||||
uname = uname,
|
||||
tmpname = tmpname,
|
||||
map = map,
|
||||
filter = filter,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user