aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/nvim/normal.c29
-rw-r--r--src/nvim/ops.c122
-rw-r--r--test/functional/helpers.lua4
-rw-r--r--test/functional/normal/put_spec.lua936
-rw-r--r--test/helpers.lua20
5 files changed, 1067 insertions, 44 deletions
diff --git a/src/nvim/normal.c b/src/nvim/normal.c
index 227bfbe779..9db02de2a6 100644
--- a/src/nvim/normal.c
+++ b/src/nvim/normal.c
@@ -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,16 +7724,22 @@ static void nv_put(cmdarg_T *cap)
savereg = copy_register(regname);
}
- /* Now delete the selected text. */
- cap->cmdchar = 'd';
- cap->nchar = NUL;
- cap->oap->regname = NUL;
- nv_operator(cap);
- do_pending_operator(cap, 0, false);
- empty = (curbuf->b_ml.ml_flags & ML_EMPTY);
+ // 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;
+ nv_operator(cap);
+ do_pending_operator(cap, 0, false);
+ empty = (curbuf->b_ml.ml_flags & ML_EMPTY);
- /* delete PUT_LINE_BACKWARD; */
- cap->oap->regname = regname;
+ // 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
diff --git a/src/nvim/ops.c b/src/nvim/ops.c
index 10d6be85f8..645dcc0865 100644
--- a/src/nvim/ops.c
+++ b/src/nvim/ops.c
@@ -2637,12 +2637,81 @@ 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)
- stuffcharReadbuff('l');
+ 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 being the affect of 'autoindent' on linewise puts, we 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 end_of_line = false;
+ if (!one_past_line) {
+ end_of_line = (*(cursor_pos + mb_ptr2len(cursor_pos)) == NUL);
+ }
+
+ bool virtualedit_allows = (ve_flags == VE_ALL
+ || ve_flags == VE_ONEMORE);
+ bool end_of_file = (
+ (curbuf->b_ml.ml_line_count == curwin->w_cursor.lnum)
+ && one_past_line);
+ if (virtualedit_allows || !(end_of_line || end_of_file)) {
+ 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 +2900,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 */
- curwin->w_cursor.col += (*mb_ptr2len)(get_cursor_pos_ptr());
- else if (c != TAB || ve_flags != VE_ALL)
- ++curwin->w_cursor.col;
- ++col;
- } else
+ // move to start of next multi-byte character
+ curwin->w_cursor.col += (*mb_ptr2len)(get_cursor_pos_ptr());
+ col++;
+ } else {
getvcol(curwin, &curwin->w_cursor, &col, NULL, &endcol2);
+ }
col += curwin->w_cursor.coladd;
if (ve_flags == VE_ALL
@@ -2892,8 +2959,7 @@ 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);
+ bd.textcol -= (*mb_head_off)(oldp, oldp + bd.textcol);
if (oldp[bd.textcol] != TAB) {
/* Only a Tab can be split into spaces. Other
* characters will have to be moved to after the
@@ -2975,21 +3041,13 @@ 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. */
- 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;
- }
+ int bytelen = (*mb_ptr2len)(get_cursor_pos_ptr());
+
+ // 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;
}
}
curbuf->b_op_start = curwin->w_cursor;
@@ -3027,7 +3085,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--;
diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua
index 2939184d2c..48ed5c256f 100644
--- a/test/functional/helpers.lua
+++ b/test/functional/helpers.lua
@@ -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'
@@ -566,6 +568,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,
diff --git a/test/functional/normal/put_spec.lua b/test/functional/normal/put_spec.lua
new file mode 100644
index 0000000000..36d5e8b43c
--- /dev/null
+++ b/test/functional/normal/put_spec.lua
@@ -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)
+
diff --git a/test/helpers.lua b/test/helpers.lua
index 0bc62da5d7..6f7281db7c 100644
--- a/test/helpers.lua
+++ b/test/helpers.lua
@@ -90,6 +90,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,
@@ -97,4 +115,6 @@ return {
check_logs = check_logs,
uname = uname,
tmpname = tmpname,
+ map = map,
+ filter = filter,
}