diff options
-rw-r--r-- | src/nvim/normal.c | 29 | ||||
-rw-r--r-- | src/nvim/ops.c | 122 | ||||
-rw-r--r-- | test/functional/helpers.lua | 4 | ||||
-rw-r--r-- | test/functional/normal/put_spec.lua | 936 | ||||
-rw-r--r-- | test/helpers.lua | 20 |
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, } |