From 6a3f8d48d0c96330511565aef5e10ad965e986b4 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 7 Nov 2016 00:59:32 +0100 Subject: 'inccommand': rename 'incsubstitute' 'inccommand' allows us to expand the feature to other commands, such as: :cdo :cfdo :global Also rename "IncSubstitute" highlight group to "Substitute". --- runtime/doc/options.txt | 17 +- runtime/doc/syntax.txt | 5 +- runtime/doc/vim_diff.txt | 2 +- src/nvim/ex_cmds.c | 72 +- src/nvim/ex_getln.c | 2 +- src/nvim/option.c | 8 +- src/nvim/option_defs.h | 2 +- src/nvim/options.lua | 16 +- src/nvim/syntax.c | 2 +- test/functional/ui/inccommand_spec.lua | 1302 +++++++++++++++++++++++++++++ test/functional/ui/incsubstitute_spec.lua | 1302 ----------------------------- 11 files changed, 1365 insertions(+), 1365 deletions(-) create mode 100644 test/functional/ui/inccommand_spec.lua delete mode 100644 test/functional/ui/incsubstitute_spec.lua diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index f41983cf07..79e5ff090f 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -3466,6 +3466,15 @@ A jump table for the options with a short description can be found at |Q_op|. The value is set to 1 when it is not -1 and setting the 'keymap' option to a valid keymap name. + *'inccommand'* *'icm'* +'inccommand' 'icm' string (default "") + global + + "nosplit" : Shows the effects of a command incrementally, as you type. + "split" : Also shows partial off-screen results in a preview window. + + Currently only works for |:substitute|. |hl-Substitute| + *'include'* *'inc'* 'include' 'inc' string (default "^\s*#\s*include") global or local to buffer |global-local| @@ -3526,14 +3535,6 @@ A jump table for the options with a short description can be found at |Q_op|. CTRL-R CTRL-W can be used to add the word at the end of the current match, excluding the characters that were already typed. - *'incsubstitute'* *'ics'* -'incsubstitute' 'ics' string (default "") - global - - If "split" or "nosplit" then |:substitute| updates the buffer - as-you-type. If "split", also show partial off-screen results in - a window. Replacement text is hightlighted with |hl-IncSubstitute|. - *'indentexpr'* *'inde'* 'indentexpr' 'inde' string (default "") local to buffer diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt index a942c5de03..a9762a7121 100644 --- a/runtime/doc/syntax.txt +++ b/runtime/doc/syntax.txt @@ -4852,8 +4852,9 @@ SignColumn column where |signs| are displayed *hl-IncSearch* IncSearch 'incsearch' highlighting; also used for the text replaced with ":s///c" - *hl-IncSubstitute* -IncSubstitute 'incsubstitute' replacement text + *hl-Substitute* +Substitute |:substitute| replacement text highlighting + *hl-LineNr* LineNr Line number for ":number" and ":#" commands, and when 'number' or 'relativenumber' option is set. diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 80db8eefb0..79381183a0 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -107,7 +107,7 @@ Some `CTRL-SHIFT-...` key chords are distinguished from `CTRL-...` variants , , , , , Options: - 'incsubstitute' shows results while typing a |:substitute| command + 'inccommand' shows results while typing a |:substitute| command 'statusline' supports unlimited alignment sections 'tabline' %@Func@foo%X can call any function on mouse-click diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 3dc190ddbf..66c4089ec7 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -87,7 +87,7 @@ typedef struct { SubIgnoreType do_ic; ///< ignore case flag } subflags_T; -/// Lines matched during 'incsubstitute'. +/// Lines matched during :substitute. typedef struct { linenr_T lnum; long nmatch; @@ -3110,7 +3110,7 @@ static char_u *sub_parse_flags(char_u *cmd, subflags_T *subflags, /// /// The usual escapes are supported as described in the regexp docs. /// -/// @return buffer used for 'incsubstitute' +/// @return buffer used for 'inccommand' preview buf_T *do_sub(exarg_T *eap) { long i = 0; @@ -3627,8 +3627,8 @@ buf_T *do_sub(exarg_T *eap) * use "\=col("."). */ curwin->w_cursor.col = regmatch.startpos[0].col; - // 3. Substitute the string. During 'incsubstitute' only do this if - // there is a replace pattern. + // 3. Substitute the string. During 'inccommand' only do this if there + // is a replace pattern. if (!eap->is_live || has_second_delim) { if (subflags.do_count) { // prevent accidentally changing the buffer by a function @@ -3944,9 +3944,9 @@ skip: subflags.do_all = save_do_all; subflags.do_ask = save_do_ask; - // Show 'incsubstitute' preview if there are matched lines. - buf_T *incsub_buf = NULL; - if (eap->is_live && matched_lines.size != 0 && pat != NULL && *p_ics != NUL) { + // Show 'inccommand' preview if there are matched lines. + buf_T *preview_buf = NULL; + if (eap->is_live && matched_lines.size != 0 && pat != NULL && *p_icm != NUL) { // Place cursor on the first match after the cursor. (If all matches are // above, then do_sub already placed cursor on the last match.) colnr_T cur_col = -1; @@ -3971,10 +3971,9 @@ skip: } } - incsub_buf = incsub_display(pat, sub, eap->line1, eap->line2, - &matched_lines); + preview_buf = show_sub(pat, sub, eap->line1, eap->line2, &matched_lines); - } else if (*p_ics != NUL && eap->is_live) { + } else if (*p_icm != NUL && eap->is_live) { curwin->w_cursor = old_cursor; // don't move the cursor } @@ -3985,7 +3984,7 @@ skip: } kv_destroy(matched_lines); - return incsub_buf; + return preview_buf; } // NOLINT(readability/fn_size) /* @@ -6015,12 +6014,11 @@ void set_context_in_sign_cmd(expand_T *xp, char_u *arg) } } -/// Shows a preview of :substitute (for 'incsubstitute'). -/// With incsubstitute=split, shows a special buffer in a window, draws the -/// screen, then restores the layout. -static buf_T *incsub_display(char_u *pat, char_u *sub, - linenr_T line1, linenr_T line2, - MatchedLineVec *matched_lines) +/// Shows the effects of the current :substitute command being typed +/// ('inccommand'). If inccommand=split, shows a preview window then later +/// restores the layout. +static buf_T *show_sub(char_u *pat, char_u *sub, linenr_T line1, linenr_T line2, + MatchedLineVec *matched_lines) FUNC_ATTR_NONNULL_ALL { static handle_T bufnr = 0; // special buffer, re-used on each visit @@ -6033,27 +6031,27 @@ static buf_T *incsub_display(char_u *pat, char_u *sub, size_t pat_size = mb_string2cells(pat); // We keep a special-purpose buffer around, but don't assume it exists. - buf_T *incsub_buf = bufnr ? buflist_findnr(bufnr) : 0; + buf_T *preview_buf = bufnr ? buflist_findnr(bufnr) : 0; win_size_save(&save_winsizes); // Save current window sizes. cmdmod.tab = 0; // disable :tab modifier - cmdmod.noswapfile = true; // disable swap for 'incsubstitute' buffer + cmdmod.noswapfile = true; // disable swap for preview buffer // disable file info message set_option_value((char_u *)"shm", 0L, (char_u *)"F", 0); bool outside_curline = (line1 != curwin->w_cursor.lnum || line2 != curwin->w_cursor.lnum); - bool split = outside_curline && (*p_ics != 'n') && (sub_size || pat_size); - if (incsub_buf == curbuf) { // Preview buffer cannot preview itself! + bool split = outside_curline && (*p_icm != 'n') && (sub_size || pat_size); + if (preview_buf == curbuf) { // Preview buffer cannot preview itself! split = false; - incsub_buf = NULL; + preview_buf = NULL; } if (split && win_split((int)p_cwh, WSP_BOT) != FAIL) { - buf_open_special(incsub_buf ? bufnr : 0, "[Preview]", "incsub"); + buf_open_special(preview_buf ? bufnr : 0, "[Preview]", "incsub"); buf_clear(); - incsub_buf = curbuf; + preview_buf = curbuf; set_option_value((char_u *)"bh", 0L, (char_u *)"hide", OPT_LOCAL); - bufnr = incsub_buf->handle; + bufnr = preview_buf->handle; curbuf->b_p_bl = false; curbuf->b_p_ma = true; curbuf->b_p_ul = -1; @@ -6070,9 +6068,9 @@ static buf_T *incsub_display(char_u *pat, char_u *sub, size_t old_line_size = 0; size_t line_size; int src_id_highlight = 0; - int hl_id = syn_check_group((char_u *)"IncSubstitute", 13); + int hl_id = syn_check_group((char_u *)"Substitute", 13); - // Dump the lines into the incsub buffer. + // Dump the lines into the preview buffer. for (size_t line = 0; line < matched_lines->size; line++) { MatchedLine mat = matched_lines->items[line]; line_size = mb_string2cells(mat.line) + col_width + 1; @@ -6083,7 +6081,7 @@ static buf_T *incsub_display(char_u *pat, char_u *sub, old_line_size = line_size; } - // put " | lnum|line" into str and append it to the incsubstitute buffer + // put " | lnum|line" into str and append it to the preview buffer snprintf(str, line_size, "|%*ld| %s", col_width - 3, mat.lnum, mat.line); ml_append(line, (char_u *)str, (colnr_T)line_size, false); @@ -6118,17 +6116,17 @@ static buf_T *incsub_display(char_u *pat, char_u *sub, cmdmod = save_cmdmod; - return incsub_buf; + return preview_buf; } /// :substitute command /// -/// If 'incsubstitute' is empty, this just calls do_sub(). -/// If 'incsubstitute' is set, substitutes as-you-type ("live"). -/// If the command is cancelled the changes are removed from undo history. +/// If 'inccommand' is empty this just calls do_sub(). +/// If 'inccommand' is set, shows a "live" preview then removes the changes +/// from undo history. void ex_substitute(exarg_T *eap) { - if (*p_ics == NUL || !eap->is_live) { // 'incsubstitute' is disabled + if (*p_icm == NUL || !eap->is_live) { // 'inccommand' is disabled (void)do_sub(eap); return; } @@ -6138,18 +6136,18 @@ void ex_substitute(exarg_T *eap) int save_changedtick = curbuf->b_changedtick; long save_b_p_ul = curbuf->b_p_ul; curbuf->b_p_ul = LONG_MAX; // make sure we can undo all changes - block_autocmds(); // disable events before incsub opening window/buffer + block_autocmds(); // disable events before show_sub() opens window/buffer emsg_off++; // No error messages for live commands - buf_T *incsub_buf = do_sub(eap); + buf_T *preview_buf = do_sub(eap); if (save_changedtick != curbuf->b_changedtick && !u_undo_and_forget(1)) { abort(); } - if (buf_valid(incsub_buf)) { + if (buf_valid(preview_buf)) { // XXX: Must do this *after* u_undo_and_forget(), why? - close_windows(incsub_buf, false); + close_windows(preview_buf, false); } curbuf->b_changedtick = save_changedtick; curbuf->b_p_ul = save_b_p_ul; diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index dba7a107e2..07b50a8056 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1592,7 +1592,7 @@ static int command_line_changed(CommandLineState *s) redrawcmdline(); s->did_incsearch = true; } else if (s->firstc == ':' - && *p_ics != NUL // 'incsubstitute' is set + && *p_icm != NUL // 'inccommand' is set && cmdline_star == 0 // not typing a password && cmd_is_live(ccline.cmdbuff)) { // process a "live" command diff --git a/src/nvim/option.c b/src/nvim/option.c index 761e4451b9..ca66f84a70 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -288,7 +288,7 @@ static char *(p_fdm_values[]) = { "manual", "expr", "marker", "indent", static char *(p_fcl_values[]) = { "all", NULL }; static char *(p_cot_values[]) = { "menu", "menuone", "longest", "preview", "noinsert", "noselect", NULL }; -static char *(p_ics_values[]) = { "nosplit", "split", NULL }; +static char *(p_icm_values[]) = { "nosplit", "split", NULL }; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "option.c.generated.h" @@ -3111,9 +3111,9 @@ did_set_string_option ( else if (gvarp == &p_cino) { /* TODO: recognize errors */ parse_cino(curbuf); - // incsubstitute - } else if (varp == &p_ics) { - if (check_opt_strings(p_ics, p_ics_values, false) != OK) { + // inccommand + } else if (varp == &p_icm) { + if (check_opt_strings(p_icm, p_icm_values, false) != OK) { errmsg = e_invarg; } // Options that are a list of flags. diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index e0711c7c8f..57ad5f5d1a 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -470,6 +470,7 @@ EXTERN int p_icon; // 'icon' EXTERN char_u *p_iconstring; // 'iconstring' EXTERN int p_ic; // 'ignorecase' EXTERN int p_is; // 'incsearch' +EXTERN char_u *p_icm; // 'inccommand' EXTERN int p_im; // 'insertmode' EXTERN char_u *p_isf; // 'isfname' EXTERN char_u *p_isi; // 'isident' @@ -589,7 +590,6 @@ EXTERN int p_spr; // 'splitright' EXTERN int p_sol; // 'startofline' EXTERN char_u *p_su; // 'suffixes' EXTERN char_u *p_swb; // 'switchbuf' -EXTERN char_u *p_ics; // 'incsubstitute' EXTERN unsigned swb_flags; #ifdef IN_OPTION_C static char *(p_swb_values[]) = diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 359bf3fcee..14707aaa6c 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1187,6 +1187,14 @@ return { if_false={vi=macros('B_IMODE_NONE')}, } }, + { + full_name='inccommand', abbreviation='icm', + type='string', scope={'global'}, + vi_def=true, + redraw={'everything'}, + varname='p_icm', + defaults={if_true={vi=""}} + }, { full_name='include', abbreviation='inc', type='string', scope={'global', 'buffer'}, @@ -1210,14 +1218,6 @@ return { varname='p_is', defaults={if_true={vi=false, vim=true}} }, - { - full_name='incsubstitute', abbreviation='ics', - type='string', scope={'global'}, - vi_def=true, - redraw={'everything'}, - varname='p_ics', - defaults={if_true={vi=""}} - }, { full_name='indentexpr', abbreviation='inde', type='string', scope={'buffer'}, diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index 68d0e3da0f..e57965ac2c 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -5902,7 +5902,7 @@ static char *highlight_init_both[] = "WildMenu ctermbg=Yellow ctermfg=Black guibg=Yellow guifg=Black", "default link EndOfBuffer NonText", "default link QuickFixLine Search", - "default link IncSubstitute Search", + "default link Substitute Search", NULL }; diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua new file mode 100644 index 0000000000..25ddfdf2a1 --- /dev/null +++ b/test/functional/ui/inccommand_spec.lua @@ -0,0 +1,1302 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear = helpers.clear +local curbufmeths = helpers.curbufmeths +local eq = helpers.eq +local eval = helpers.eval +local execute = helpers.execute +local expect = helpers.expect +local feed = helpers.feed +local insert = helpers.insert +local meths = helpers.meths +local neq = helpers.neq +local ok = helpers.ok +local source = helpers.source +local wait = helpers.wait + +local default_text = [[ + Inc substitution on + two lines +]] + +local function common_setup(screen, inccommand, text) + if screen then + execute("syntax on") + execute("set nohlsearch") + execute("hi Substitute guifg=red guibg=yellow") + screen:attach() + screen:set_default_attr_ids({ + [1] = {foreground = Screen.colors.Fuchsia}, + [2] = {foreground = Screen.colors.Brown, bold = true}, + [3] = {foreground = Screen.colors.SlateBlue}, + [4] = {bold = true, foreground = Screen.colors.SlateBlue}, + [5] = {foreground = Screen.colors.DarkCyan}, + [6] = {bold = true}, + [7] = {underline = true, bold = true, foreground = Screen.colors.SlateBlue}, + [8] = {foreground = Screen.colors.Slateblue, underline = true}, + [9] = {background = Screen.colors.Yellow}, + [10] = {reverse = true}, + [11] = {reverse = true, bold=true}, + [12] = {foreground = Screen.colors.Red, background = Screen.colors.Yellow}, + [13] = {bold = true, foreground = Screen.colors.SeaGreen}, + [14] = {foreground = Screen.colors.White, background = Screen.colors.Red}, + [15] = {bold=true, foreground=Screen.colors.Blue}, + [16] = {background=Screen.colors.Grey90}, -- cursorline + }) + end + + execute("set inccommand=" .. (inccommand and inccommand or "")) + + if text then + insert(text) + end +end + +describe(":substitute, 'inccommand' preserves", function() + if helpers.pending_win32(pending) then return end + + before_each(clear) + + it('listed buffers (:ls)', function() + local screen = Screen.new(30,10) + common_setup(screen, "split", "ABC") + + execute("%s/AB/BA/") + execute("ls") + + screen:expect([[ + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :ls | + 1 %a + "[No Name]" | + line 1 | + {13:Press ENTER or type command to}| + {13: continue}^ | + ]]) + end) + + it(':substitute with various delimiters', function() + for _, case in pairs{"", "split", "nosplit"} do + clear() + insert(default_text) + execute("set inccommand=" .. case) + + local delims = { '/', '#', ';', '%', ',', '@', '!', ''} + for _,delim in pairs(delims) do + execute("%s"..delim.."lines"..delim.."LINES"..delim.."g") + expect([[ + Inc substitution on + two LINES + ]]) + execute("undo") + end + end + end) + + it("'undolevels'", function() + for _, case in pairs{"", "split", "nosplit"} do + clear() + execute("set undolevels=139") + execute("setlocal undolevels=34") + execute("set inccommand=" .. case) + insert("as") + feed(":%s/as/glork/") + eq(meths.get_option('undolevels'), 139) + eq(curbufmeths.get_option('undolevels'), 34) + end + end) + + it("b:changedtick", function() + for _, case in pairs{"", "split", "nosplit"} do + clear() + execute("set inccommand=" .. case) + feed([[isome text 1]]) + feed([[osome text 2]]) + local expected_tick = eval("b:changedtick") + ok(expected_tick > 0) + + expect([[ + some text 1 + some text 2]]) + feed(":%s/e/XXX/") + wait() + + eq(expected_tick, eval("b:changedtick")) + end + end) + +end) + +describe(":substitute, 'inccommand' preserves undo", function() + if helpers.pending_win32(pending) then return end + + local cases = { "", "split", "nosplit" } + + local substrings = { + ":%s/1", + ":%s/1/", + ":%s/1/", + ":%s/1/a", + ":%s/1/a", + ":%s/1/ax", + ":%s/1/ax", + ":%s/1/ax", + ":%s/1/ax", + ":%s/1/ax/", + ":%s/1/ax/", + ":%s/1/ax//", + ":%s/1/ax/g", + ":%s/1/ax/g", + ":%s/1/ax/g" + } + + local function test_sub(substring, split, redoable) + clear() + execute("set inccommand=" .. split) + + insert("1") + feed("o2") + execute("undo") + feed("o3") + if redoable then + feed("o4") + execute("undo") + end + feed(substring.. "") + execute("undo") + + feed("g-") + expect([[ + 1 + 2]]) + + feed("g+") + expect([[ + 1 + 3]]) + end + + local function test_notsub(substring, split, redoable) + clear() + execute("set inccommand=" .. split) + + insert("1") + feed("o2") + execute("undo") + feed("o3") + if redoable then + feed("o4") + execute("undo") + end + feed(substring .. "") + + feed("g-") + expect([[ + 1 + 2]]) + + feed("g+") + expect([[ + 1 + 3]]) + + if redoable then + feed("") + expect([[ + 1 + 3 + 4]]) + end + end + + + local function test_threetree(substring, split) + clear() + execute("set inccommand=" .. split) + + insert("1") + feed("o2") + feed("o3") + feed("uu") + feed("oa") + feed("ob") + feed("uu") + feed("oA") + feed("oB") + + -- This is the undo tree (x-Axis is timeline), we're at B now + -- ----------------A - B + -- / + -- | --------a - b + -- |/ + -- 1 - 2 - 3 + + feed("2u") + feed(substring .. "") + feed("") + expect([[ + 1 + A]]) + + feed("g-") -- go to b + feed("2u") + feed(substring .. "") + feed("") + expect([[ + 1 + a]]) + + feed("g-") -- go to 3 + feed("2u") + feed(substring .. "") + feed("") + expect([[ + 1 + 2]]) + end + + -- TODO(vim): This does not work, even in Vim. + -- Waiting for fix (perhaps from upstream). + pending("at a non-leaf of the undo tree", function() + for _, case in pairs(cases) do + for _, str in pairs(substrings) do + for _, redoable in pairs({true}) do + test_sub(str, case, redoable) + end + end + end + end) + + it("at a leaf of the undo tree", function() + for _, case in pairs(cases) do + for _, str in pairs(substrings) do + for _, redoable in pairs({false}) do + test_sub(str, case, redoable) + end + end + end + end) + + it("when interrupting substitution", function() + for _, case in pairs(cases) do + for _, str in pairs(substrings) do + for _, redoable in pairs({true,false}) do + test_notsub(str, case, redoable) + end + end + end + end) + + it("in a complex undo scenario", function() + for _, case in pairs(cases) do + for _, str in pairs(substrings) do + test_threetree(str, case) + end + end + end) + + it('with undolevels=0', function() + for _, case in pairs(cases) do + clear() + common_setup(nil, case, default_text) + execute("set undolevels=0") + + feed("1G0") + insert("X") + feed(":%s/tw/MO/") + execute("undo") + expect(default_text) + execute("undo") + expect(default_text:gsub("Inc", "XInc")) + execute("undo") + + execute("%s/tw/MO/g") + expect(default_text:gsub("tw", "MO")) + execute("undo") + expect(default_text) + execute("undo") + expect(default_text:gsub("tw", "MO")) + end + end) + + it('with undolevels=1', function() + local screen = Screen.new(20,10) + + for _, case in pairs(cases) do + clear() + common_setup(screen, case, default_text) + execute("set undolevels=1") + + feed("1G0") + insert("X") + feed("IY") + feed(":%s/tw/MO/") + -- using execute("undo") here will result in a "Press ENTER" prompt + feed("u") + expect(default_text:gsub("Inc", "XInc")) + feed("u") + expect(default_text) + + feed(":%s/tw/MO/g") + feed(":%s/MO/GO/g") + feed(":%s/GO/NO/g") + feed("u") + expect(default_text:gsub("tw", "GO")) + feed("u") + expect(default_text:gsub("tw", "MO")) + feed("u") + + if case == "split" then + screen:expect([[ + ^MOo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + else + screen:expect([[ + Inc substitution on | + ^MOo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + end + end + screen:detach() + end) + + it('with undolevels=2', function() + local screen = Screen.new(20,10) + + for _, case in pairs(cases) do + clear() + common_setup(screen, case, default_text) + execute("set undolevels=2") + + feed("2GAx") + feed("Ay") + feed("Az") + feed(":%s/tw/AR") + -- using execute("undo") here will result in a "Press ENTER" prompt + feed("u") + expect(default_text:gsub("lines", "linesxy")) + feed("u") + expect(default_text:gsub("lines", "linesx")) + feed("u") + expect(default_text) + feed("u") + + if case == "split" then + screen:expect([[ + two line^s | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + else + screen:expect([[ + Inc substitution on | + two line^s | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + end + + feed(":%s/tw/MO/g") + feed(":%s/MO/GO/g") + feed(":%s/GO/NO/g") + feed(":%s/NO/LO/g") + feed("u") + expect(default_text:gsub("tw", "NO")) + feed("u") + expect(default_text:gsub("tw", "GO")) + feed("u") + expect(default_text:gsub("tw", "MO")) + feed("u") + + if case == "split" then + screen:expect([[ + ^MOo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + else + screen:expect([[ + Inc substitution on | + ^MOo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + end + screen:detach() + end + end) + + it('with undolevels=-1', function() + local screen = Screen.new(20,10) + + for _, case in pairs(cases) do + clear() + common_setup(screen, case, default_text) + + execute("set undolevels=-1") + feed(":%s/tw/MO/g") + -- using execute("undo") here will result in a "Press ENTER" prompt + feed("u") + if case == "split" then + screen:expect([[ + ^MOo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + else + screen:expect([[ + Inc substitution on | + ^MOo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + end + + -- repeat with an interrupted substitution + clear() + common_setup(screen, case, default_text) + + execute("set undolevels=-1") + feed("1G") + feed("IL") + feed(":%s/tw/MO/g") + feed("u") + + if case == "split" then + screen:expect([[ + ^two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + elseif case == "" then + screen:expect([[ + ^LInc substitution on| + two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + else + screen:expect([[ + LInc substitution on| + ^two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + Already...st change | + ]]) + end + end + screen:detach() + end) + +end) + +describe(":substitute, inccommand=split", function() + if helpers.pending_win32(pending) then return end + + local screen = Screen.new(30,15) + + before_each(function() + clear() + common_setup(screen, "split", default_text .. default_text) + end) + + after_each(function() + screen:detach() + end) + + it('shows split window when typing the pattern', function() + feed(":%s/tw") + screen:expect([[ + Inc substitution on | + two lines | + | + {15:~ }| + {15:~ }| + {11:[No Name] [+] }| + |2| two lines | + |4| two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw^ | + ]]) + end) + + it('shows split window with empty replacement', function() + feed(":%s/tw/") + screen:expect([[ + Inc substitution on | + o lines | + | + {15:~ }| + {15:~ }| + {11:[No Name] [+] }| + |2| o lines | + |4| o lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw/^ | + ]]) + + feed("x") + screen:expect([[ + xo lines | + Inc substitution on | + xo lines | + | + {15:~ }| + {11:[No Name] [+] }| + |2| {12:x}o lines | + |4| {12:x}o lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw/x^ | + ]]) + + feed("") + screen:expect([[ + o lines | + Inc substitution on | + o lines | + | + {15:~ }| + {11:[No Name] [+] }| + |2| o lines | + |4| o lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw/^ | + ]]) + + end) + + it('shows split window when typing replacement', function() + feed(":%s/tw/XX") + screen:expect([[ + XXo lines | + Inc substitution on | + XXo lines | + | + {15:~ }| + {11:[No Name] [+] }| + |2| {12:XX}o lines | + |4| {12:XX}o lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw/XX^ | + ]]) + end) + + it('does not show split window for :s/', function() + feed("2gg") + feed(":s/tw") + screen:expect([[ + Inc substitution on | + two lines | + Inc substitution on | + two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :s/tw^ | + ]]) + end) + + it("'hlsearch' highlights the substitution, 'cursorline' does not", function() + execute("set hlsearch") + execute("set cursorline") -- Should NOT appear in the preview window. + feed(":%s/tw") + screen:expect([[ + Inc substitution on | + {9:tw}{16:o lines }| + | + {15:~ }| + {15:~ }| + {11:[No Name] [+] }| + |2| {9:tw}o lines | + |4| {9:tw}o lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw^ | + ]]) + end) + + it('highlights the replacement text correctly', function() + feed('ggO') + feed('M M M') + feed(':%s/M/123/g') + screen:expect([[ + 123 123 123 | + Inc substitution on | + two lines | + Inc substitution on | + two lines | + {11:[No Name] [+] }| + |1| {12:123} {12:123} {12:123} | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/M/123/g^ | + ]]) + end) + + it('actually replaces text', function() + feed(":%s/tw/XX/g") + + screen:expect([[ + XXo lines | + Inc substitution on | + ^XXo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :%s/tw/XX/g | + ]]) + end) + + it('shows correct line numbers with many lines', function() + feed("gg") + feed("2yy") + feed("2000p") + execute("1,1000s/tw/BB/g") + + feed(":%s/tw/X") + screen:expect([[ + BBo lines | + Inc substitution on | + Xo lines | + Inc substitution on | + Xo lines | + {11:[No Name] [+] }| + |1001| {12:X}o lines | + |1003| {12:X}o lines | + |1005| {12:X}o lines | + |1007| {12:X}o lines | + |1009| {12:X}o lines | + |1011| {12:X}o lines | + |1013| {12:X}o lines | + {10:[Preview] }| + :%s/tw/X^ | + ]]) + end) + + it('does not spam the buffer numbers', function() + -- The preview buffer is re-used (unless user deleted it), so buffer numbers + -- will not increase on each keystroke. + feed(":%s/tw/Xo/g") + -- Delete and re-type the g a few times. + feed("") + wait() + feed("g") + wait() + feed("") + wait() + feed("g") + wait() + feed("") + wait() + feed(":vs tmp") + eq(3, helpers.call('bufnr', '$')) + end) + + it('works with the n flag', function() + feed(":%s/tw/Mix/n") + screen:expect([[ + ^two lines | + Inc substitution on | + two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + 2 matches on 2 lines | + ]]) + end) + +end) + +describe(":substitute, inccommand=nosplit", function() + if helpers.pending_win32(pending) then return end + + local screen = Screen.new(20,10) + + before_each(function() + clear() + common_setup(screen, "nosplit", default_text .. default_text) + end) + + after_each(function() + if screen then screen:detach() end + end) + + it('does not show a split window anytime', function() + execute("set hlsearch") + + feed(":%s/tw") + screen:expect([[ + Inc substitution on | + {9:tw}o lines | + Inc substitution on | + {9:tw}o lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :%s/tw^ | + ]]) + + feed("/BM") + screen:expect([[ + Inc substitution on | + BMo lines | + Inc substitution on | + BMo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :%s/tw/BM^ | + ]]) + + feed("/") + screen:expect([[ + Inc substitution on | + BMo lines | + Inc substitution on | + BMo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :%s/tw/BM/^ | + ]]) + + feed("") + screen:expect([[ + Inc substitution on | + BMo lines | + Inc substitution on | + ^BMo lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :%s/tw/BM/ | + ]]) + end) + +end) + +describe(":substitute, 'inccommand' with a failing expression", function() + if helpers.pending_win32(pending) then return end + + local screen = Screen.new(20,10) + local cases = { "", "split", "nosplit" } + + local function refresh(case) + clear() + common_setup(screen, case, default_text) + end + + it('in the pattern does nothing for', function() + for _, case in pairs(cases) do + refresh(case) + execute("set inccommand=" .. case) + feed(":silent! %s/tw\\(/LARD/") + expect(default_text) + end + end) + + it('in the replacement deletes the matches', function() + for _, case in pairs(cases) do + refresh(case) + local replacements = { "\\='LARD", "\\=xx_novar__xx" } + + for _, repl in pairs(replacements) do + execute("set inccommand=" .. case) + feed(":silent! %s/tw/" .. repl .. "/") + expect(default_text:gsub("tw", "")) + execute("undo") + end + end + end) + +end) + +describe("'inccommand' and :cnoremap", function() + local cases = { "", "split", "nosplit" } + + local function refresh(case) + clear() + common_setup(nil, case, default_text) + end + + it('work with remapped characters', function() + for _, case in pairs(cases) do + refresh(case) + local command = "%s/lines/LINES/g" + + for i = 1, string.len(command) do + local c = string.sub(command, i, i) + execute("cnoremap ".. c .. " " .. c) + end + + execute(command) + expect([[ + Inc substitution on + two LINES + ]]) + end + end) + + it('work when mappings move the cursor', function() + for _, case in pairs(cases) do + refresh(case) + execute("cnoremap ,S LINES/") + + feed(":%s/lines/,Sor three ") + expect([[ + Inc substitution on + two or three LINES + ]]) + + execute("cnoremap ;S /X/") + feed(":%s/;SI") + expect([[ + Xnc substitution on + two or three LXNES + ]]) + + execute("cnoremap ,T //Y/") + feed(":%s,TX") + expect([[ + Ync substitution on + two or three LYNES + ]]) + + execute("cnoremap ;T s//Z/") + feed(":%;TY") + expect([[ + Znc substitution on + two or three LZNES + ]]) + end + end) + + it('work with a failing mapping', function() + for _, case in pairs(cases) do + refresh(case) + execute("cnoremap x execute('bwipeout!')[-1].'x'") + + feed(":%s/tw/tox") + + -- error thrown b/c of the mapping + neq(nil, eval('v:errmsg'):find('^E523:')) + -- the substitution after the error only works for ics=split/nosplit + -- which seems like the right thing to do in all cases, but we probably + -- don't want to change the default, so all in all this seems alright + if case == '' then + expect(default_text) + else + expect(default_text:gsub("tw", "tox")) + end + end + end) + + it('work when temporarily moving the cursor', function() + for _, case in pairs(cases) do + refresh(case) + execute("cnoremap x cursor(1, 1)[-1].'x'") + + feed(":%s/tw/tox/g") + expect(default_text:gsub("tw", "tox")) + end + end) + + it("work when a mapping disables 'inccommand'", function() + for _, case in pairs(cases) do + refresh(case) + execute("cnoremap x execute('set inccommand=')[-1]") + + feed(":%s/tw/toxa/g") + expect(default_text:gsub("tw", "toa")) + end + end) + + it('work with a complex mapping', function() + for _, case in pairs(cases) do + refresh(case) + source([[cnoremap x eextend(g:, {'fo': getcmdline()}) + \.fo:new:bw!:=remove(g:, 'fo')x]]) + + feed(":%s/tw/tox") + feed("/") + expect(default_text:gsub("tw", "tox")) + end + end) + +end) + +describe("'inccommand': autocommands", function() + before_each(clear) + + -- keys are events to be tested + -- values are arrays like + -- { open = { 1 }, close = { 2, 3} } + -- which would mean that during the test below the event fires for + -- buffer 1 when opening the preview window, and for buffers 2 and 3 + -- when closing the preview window + local eventsExpected = { + BufAdd = {}, + BufDelete = {}, + BufEnter = {}, + BufFilePost = {}, + BufFilePre = {}, + BufHidden = {}, + BufLeave = {}, + BufNew = {}, + BufNewFile = {}, + BufRead = {}, + BufReadCmd = {}, + BufReadPre = {}, + BufUnload = {}, + BufWinEnter = {}, + BufWinLeave = {}, + BufWipeout = {}, + BufWrite = {}, + BufWriteCmd = {}, + BufWritePost = {}, + Syntax = {}, + FileType = {}, + WinEnter = {}, + WinLeave = {}, + CmdwinEnter = {}, + CmdwinLeave = {}, + } + + local function bufferlist(t) + local s = "" + for _, buffer in pairs(t) do + s = s .. ", " .. tostring(buffer) + end + return s + end + + -- fill the table with default values + for event, _ in pairs(eventsExpected) do + eventsExpected[event].open = eventsExpected[event].open or {} + eventsExpected[event].close = eventsExpected[event].close or {} + end + + local function register_autocmd(event) + meths.set_var(event .. "_fired", {}) + execute("autocmd " .. event .. " * call add(g:" .. event .. "_fired, expand(''))") + end + + it('are not fired when splitting', function() + common_setup(nil, "split", default_text) + + local eventsObserved = {} + for event, _ in pairs(eventsExpected) do + eventsObserved[event] = {} + register_autocmd(event) + end + + feed(":%s/tw") + + for event, _ in pairs(eventsExpected) do + eventsObserved[event].open = meths.get_var(event .. "_fired") + meths.set_var(event .. "_fired", {}) + end + + feed("/") + + for event, _ in pairs(eventsExpected) do + eventsObserved[event].close = meths.get_var(event .. "_fired") + end + + for event, _ in pairs(eventsExpected) do + eq(event .. bufferlist(eventsExpected[event].open), + event .. bufferlist(eventsObserved[event].open)) + eq(event .. bufferlist(eventsExpected[event].close), + event .. bufferlist(eventsObserved[event].close)) + end + end) + +end) + +describe("'inccommand': split windows", function() + if helpers.pending_win32(pending) then return end + + local screen + local function refresh() + clear() + screen = Screen.new(40,30) + common_setup(screen, "split", default_text) + end + + after_each(function() + screen:detach() + end) + + it('work after more splits', function() + refresh() + + execute("vsplit") + execute("split") + feed(":%s/tw") + screen:expect([[ + two lines {10:|}two lines | + {10:|} | + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {11:[No Name] [+] }{10:|}{15:~ }| + two lines {10:|}{15:~ }| + {10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {10:[No Name] [+] [No Name] [+] }| + |2| two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw^ | + ]]) + + feed("") + execute("only") + execute("split") + execute("vsplit") + + feed(":%s/tw") + screen:expect([[ + Inc substitution on {10:|}Inc substitution on| + two lines {10:|}two lines | + {10:|} | + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {15:~ }{10:|}{15:~ }| + {11:[No Name] [+] }{10:[No Name] [+] }| + Inc substitution on | + two lines | + | + {15:~ }| + {15:~ }| + {10:[No Name] [+] }| + |2| two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw^ | + ]]) + end) + + local settings = { + "splitbelow", + "splitright", + "noequalalways", + "equalalways eadirection=ver", + "equalalways eadirection=hor", + "equalalways eadirection=both", + } + + it("are not affected by various settings", function() + for _, setting in pairs(settings) do + refresh() + execute("set " .. setting) + + feed(":%s/tw") + + screen:expect([[ + Inc substitution on | + two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {11:[No Name] [+] }| + |2| two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {10:[Preview] }| + :%s/tw^ | + ]]) + end + end) + +end) diff --git a/test/functional/ui/incsubstitute_spec.lua b/test/functional/ui/incsubstitute_spec.lua deleted file mode 100644 index 4213a2ea93..0000000000 --- a/test/functional/ui/incsubstitute_spec.lua +++ /dev/null @@ -1,1302 +0,0 @@ -local helpers = require('test.functional.helpers')(after_each) -local Screen = require('test.functional.ui.screen') -local clear = helpers.clear -local curbufmeths = helpers.curbufmeths -local eq = helpers.eq -local eval = helpers.eval -local execute = helpers.execute -local expect = helpers.expect -local feed = helpers.feed -local insert = helpers.insert -local meths = helpers.meths -local neq = helpers.neq -local ok = helpers.ok -local source = helpers.source -local wait = helpers.wait - -local default_text = [[ - Inc substitution on - two lines -]] - -local function common_setup(screen, incsub, text) - if screen then - execute("syntax on") - execute("set nohlsearch") - execute("hi IncSubstitute guifg=red guibg=yellow") - screen:attach() - screen:set_default_attr_ids({ - [1] = {foreground = Screen.colors.Fuchsia}, - [2] = {foreground = Screen.colors.Brown, bold = true}, - [3] = {foreground = Screen.colors.SlateBlue}, - [4] = {bold = true, foreground = Screen.colors.SlateBlue}, - [5] = {foreground = Screen.colors.DarkCyan}, - [6] = {bold = true}, - [7] = {underline = true, bold = true, foreground = Screen.colors.SlateBlue}, - [8] = {foreground = Screen.colors.Slateblue, underline = true}, - [9] = {background = Screen.colors.Yellow}, - [10] = {reverse = true}, - [11] = {reverse = true, bold=true}, - [12] = {foreground = Screen.colors.Red, background = Screen.colors.Yellow}, - [13] = {bold = true, foreground = Screen.colors.SeaGreen}, - [14] = {foreground = Screen.colors.White, background = Screen.colors.Red}, - [15] = {bold=true, foreground=Screen.colors.Blue}, - [16] = {background=Screen.colors.Grey90}, -- cursorline - }) - end - - execute("set incsubstitute=" .. (incsub and incsub or "")) - - if text then - insert(text) - end -end - -describe("'incsubstitute' preserves", function() - if helpers.pending_win32(pending) then return end - - before_each(clear) - - it('listed buffers (:ls)', function() - local screen = Screen.new(30,10) - common_setup(screen, "split", "ABC") - - execute("%s/AB/BA/") - execute("ls") - - screen:expect([[ - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :ls | - 1 %a + "[No Name]" | - line 1 | - {13:Press ENTER or type command to}| - {13: continue}^ | - ]]) - end) - - it(':substitute with various delimiters', function() - for _, case in pairs{"", "split", "nosplit"} do - clear() - insert(default_text) - execute("set incsubstitute=" .. case) - - local delims = { '/', '#', ';', '%', ',', '@', '!', ''} - for _,delim in pairs(delims) do - execute("%s"..delim.."lines"..delim.."LINES"..delim.."g") - expect([[ - Inc substitution on - two LINES - ]]) - execute("undo") - end - end - end) - - it("'undolevels'", function() - for _, case in pairs{"", "split", "nosplit"} do - clear() - execute("set undolevels=139") - execute("setlocal undolevels=34") - execute("set incsubstitute=" .. case) - insert("as") - feed(":%s/as/glork/") - eq(meths.get_option('undolevels'), 139) - eq(curbufmeths.get_option('undolevels'), 34) - end - end) - - it("b:changedtick", function() - for _, case in pairs{"", "split", "nosplit"} do - clear() - execute("set incsubstitute=" .. case) - feed([[isome text 1]]) - feed([[osome text 2]]) - local expected_tick = eval("b:changedtick") - ok(expected_tick > 0) - - expect([[ - some text 1 - some text 2]]) - feed(":%s/e/XXX/") - wait() - - eq(expected_tick, eval("b:changedtick")) - end - end) - -end) - -describe("'incsubstitute' preserves undo", function() - if helpers.pending_win32(pending) then return end - - local cases = { "", "split", "nosplit" } - - local substrings = { - ":%s/1", - ":%s/1/", - ":%s/1/", - ":%s/1/a", - ":%s/1/a", - ":%s/1/ax", - ":%s/1/ax", - ":%s/1/ax", - ":%s/1/ax", - ":%s/1/ax/", - ":%s/1/ax/", - ":%s/1/ax//", - ":%s/1/ax/g", - ":%s/1/ax/g", - ":%s/1/ax/g" - } - - local function test_sub(substring, split, redoable) - clear() - execute("set incsubstitute=" .. split) - - insert("1") - feed("o2") - execute("undo") - feed("o3") - if redoable then - feed("o4") - execute("undo") - end - feed(substring.. "") - execute("undo") - - feed("g-") - expect([[ - 1 - 2]]) - - feed("g+") - expect([[ - 1 - 3]]) - end - - local function test_notsub(substring, split, redoable) - clear() - execute("set incsubstitute=" .. split) - - insert("1") - feed("o2") - execute("undo") - feed("o3") - if redoable then - feed("o4") - execute("undo") - end - feed(substring .. "") - - feed("g-") - expect([[ - 1 - 2]]) - - feed("g+") - expect([[ - 1 - 3]]) - - if redoable then - feed("") - expect([[ - 1 - 3 - 4]]) - end - end - - - local function test_threetree(substring, split) - clear() - execute("set incsubstitute=" .. split) - - insert("1") - feed("o2") - feed("o3") - feed("uu") - feed("oa") - feed("ob") - feed("uu") - feed("oA") - feed("oB") - - -- This is the undo tree (x-Axis is timeline), we're at B now - -- ----------------A - B - -- / - -- | --------a - b - -- |/ - -- 1 - 2 - 3 - - feed("2u") - feed(substring .. "") - feed("") - expect([[ - 1 - A]]) - - feed("g-") -- go to b - feed("2u") - feed(substring .. "") - feed("") - expect([[ - 1 - a]]) - - feed("g-") -- go to 3 - feed("2u") - feed(substring .. "") - feed("") - expect([[ - 1 - 2]]) - end - - -- TODO(vim): This does not work, even in Vim. - -- Waiting for fix (perhaps from upstream). - pending("at a non-leaf of the undo tree", function() - for _, case in pairs(cases) do - for _, str in pairs(substrings) do - for _, redoable in pairs({true}) do - test_sub(str, case, redoable) - end - end - end - end) - - it("at a leaf of the undo tree", function() - for _, case in pairs(cases) do - for _, str in pairs(substrings) do - for _, redoable in pairs({false}) do - test_sub(str, case, redoable) - end - end - end - end) - - it("when interrupting substitution", function() - for _, case in pairs(cases) do - for _, str in pairs(substrings) do - for _, redoable in pairs({true,false}) do - test_notsub(str, case, redoable) - end - end - end - end) - - it("in a complex undo scenario", function() - for _, case in pairs(cases) do - for _, str in pairs(substrings) do - test_threetree(str, case) - end - end - end) - - it('with undolevels=0', function() - for _, case in pairs(cases) do - clear() - common_setup(nil, case, default_text) - execute("set undolevels=0") - - feed("1G0") - insert("X") - feed(":%s/tw/MO/") - execute("undo") - expect(default_text) - execute("undo") - expect(default_text:gsub("Inc", "XInc")) - execute("undo") - - execute("%s/tw/MO/g") - expect(default_text:gsub("tw", "MO")) - execute("undo") - expect(default_text) - execute("undo") - expect(default_text:gsub("tw", "MO")) - end - end) - - it('with undolevels=1', function() - local screen = Screen.new(20,10) - - for _, case in pairs(cases) do - clear() - common_setup(screen, case, default_text) - execute("set undolevels=1") - - feed("1G0") - insert("X") - feed("IY") - feed(":%s/tw/MO/") - -- using execute("undo") here will result in a "Press ENTER" prompt - feed("u") - expect(default_text:gsub("Inc", "XInc")) - feed("u") - expect(default_text) - - feed(":%s/tw/MO/g") - feed(":%s/MO/GO/g") - feed(":%s/GO/NO/g") - feed("u") - expect(default_text:gsub("tw", "GO")) - feed("u") - expect(default_text:gsub("tw", "MO")) - feed("u") - - if case == "split" then - screen:expect([[ - ^MOo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - else - screen:expect([[ - Inc substitution on | - ^MOo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - end - end - screen:detach() - end) - - it('with undolevels=2', function() - local screen = Screen.new(20,10) - - for _, case in pairs(cases) do - clear() - common_setup(screen, case, default_text) - execute("set undolevels=2") - - feed("2GAx") - feed("Ay") - feed("Az") - feed(":%s/tw/AR") - -- using execute("undo") here will result in a "Press ENTER" prompt - feed("u") - expect(default_text:gsub("lines", "linesxy")) - feed("u") - expect(default_text:gsub("lines", "linesx")) - feed("u") - expect(default_text) - feed("u") - - if case == "split" then - screen:expect([[ - two line^s | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - else - screen:expect([[ - Inc substitution on | - two line^s | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - end - - feed(":%s/tw/MO/g") - feed(":%s/MO/GO/g") - feed(":%s/GO/NO/g") - feed(":%s/NO/LO/g") - feed("u") - expect(default_text:gsub("tw", "NO")) - feed("u") - expect(default_text:gsub("tw", "GO")) - feed("u") - expect(default_text:gsub("tw", "MO")) - feed("u") - - if case == "split" then - screen:expect([[ - ^MOo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - else - screen:expect([[ - Inc substitution on | - ^MOo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - end - screen:detach() - end - end) - - it('with undolevels=-1', function() - local screen = Screen.new(20,10) - - for _, case in pairs(cases) do - clear() - common_setup(screen, case, default_text) - - execute("set undolevels=-1") - feed(":%s/tw/MO/g") - -- using execute("undo") here will result in a "Press ENTER" prompt - feed("u") - if case == "split" then - screen:expect([[ - ^MOo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - else - screen:expect([[ - Inc substitution on | - ^MOo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - end - - -- repeat with an interrupted substitution - clear() - common_setup(screen, case, default_text) - - execute("set undolevels=-1") - feed("1G") - feed("IL") - feed(":%s/tw/MO/g") - feed("u") - - if case == "split" then - screen:expect([[ - ^two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - elseif case == "" then - screen:expect([[ - ^LInc substitution on| - two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - else - screen:expect([[ - LInc substitution on| - ^two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - Already...st change | - ]]) - end - end - screen:detach() - end) - -end) - -describe("incsubstitute=split", function() - if helpers.pending_win32(pending) then return end - - local screen = Screen.new(30,15) - - before_each(function() - clear() - common_setup(screen, "split", default_text .. default_text) - end) - - after_each(function() - screen:detach() - end) - - it('shows split window when typing the pattern', function() - feed(":%s/tw") - screen:expect([[ - Inc substitution on | - two lines | - | - {15:~ }| - {15:~ }| - {11:[No Name] [+] }| - |2| two lines | - |4| two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw^ | - ]]) - end) - - it('shows split window with empty replacement', function() - feed(":%s/tw/") - screen:expect([[ - Inc substitution on | - o lines | - | - {15:~ }| - {15:~ }| - {11:[No Name] [+] }| - |2| o lines | - |4| o lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw/^ | - ]]) - - feed("x") - screen:expect([[ - xo lines | - Inc substitution on | - xo lines | - | - {15:~ }| - {11:[No Name] [+] }| - |2| {12:x}o lines | - |4| {12:x}o lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw/x^ | - ]]) - - feed("") - screen:expect([[ - o lines | - Inc substitution on | - o lines | - | - {15:~ }| - {11:[No Name] [+] }| - |2| o lines | - |4| o lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw/^ | - ]]) - - end) - - it('shows split window when typing replacement', function() - feed(":%s/tw/XX") - screen:expect([[ - XXo lines | - Inc substitution on | - XXo lines | - | - {15:~ }| - {11:[No Name] [+] }| - |2| {12:XX}o lines | - |4| {12:XX}o lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw/XX^ | - ]]) - end) - - it('does not show split window for :s/', function() - feed("2gg") - feed(":s/tw") - screen:expect([[ - Inc substitution on | - two lines | - Inc substitution on | - two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :s/tw^ | - ]]) - end) - - it("'hlsearch' highlights the substitution, 'cursorline' does not", function() - execute("set hlsearch") - execute("set cursorline") -- Should NOT appear in the preview window. - feed(":%s/tw") - screen:expect([[ - Inc substitution on | - {9:tw}{16:o lines }| - | - {15:~ }| - {15:~ }| - {11:[No Name] [+] }| - |2| {9:tw}o lines | - |4| {9:tw}o lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw^ | - ]]) - end) - - it('highlights the replacement text correctly', function() - feed('ggO') - feed('M M M') - feed(':%s/M/123/g') - screen:expect([[ - 123 123 123 | - Inc substitution on | - two lines | - Inc substitution on | - two lines | - {11:[No Name] [+] }| - |1| {12:123} {12:123} {12:123} | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/M/123/g^ | - ]]) - end) - - it('actually replaces text', function() - feed(":%s/tw/XX/g") - - screen:expect([[ - XXo lines | - Inc substitution on | - ^XXo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :%s/tw/XX/g | - ]]) - end) - - it('shows correct line numbers with many lines', function() - feed("gg") - feed("2yy") - feed("2000p") - execute("1,1000s/tw/BB/g") - - feed(":%s/tw/X") - screen:expect([[ - BBo lines | - Inc substitution on | - Xo lines | - Inc substitution on | - Xo lines | - {11:[No Name] [+] }| - |1001| {12:X}o lines | - |1003| {12:X}o lines | - |1005| {12:X}o lines | - |1007| {12:X}o lines | - |1009| {12:X}o lines | - |1011| {12:X}o lines | - |1013| {12:X}o lines | - {10:[Preview] }| - :%s/tw/X^ | - ]]) - end) - - it('does not spam the buffer numbers', function() - -- The preview buffer is re-used (unless user deleted it), so buffer numbers - -- will not increase on each keystroke. - feed(":%s/tw/Xo/g") - -- Delete and re-type the g a few times. - feed("") - wait() - feed("g") - wait() - feed("") - wait() - feed("g") - wait() - feed("") - wait() - feed(":vs tmp") - eq(3, helpers.call('bufnr', '$')) - end) - - it('works with the n flag', function() - feed(":%s/tw/Mix/n") - screen:expect([[ - ^two lines | - Inc substitution on | - two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - 2 matches on 2 lines | - ]]) - end) - -end) - -describe("incsubstitute=nosplit", function() - if helpers.pending_win32(pending) then return end - - local screen = Screen.new(20,10) - - before_each(function() - clear() - common_setup(screen, "nosplit", default_text .. default_text) - end) - - after_each(function() - if screen then screen:detach() end - end) - - it('does not show a split window anytime', function() - execute("set hlsearch") - - feed(":%s/tw") - screen:expect([[ - Inc substitution on | - {9:tw}o lines | - Inc substitution on | - {9:tw}o lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :%s/tw^ | - ]]) - - feed("/BM") - screen:expect([[ - Inc substitution on | - BMo lines | - Inc substitution on | - BMo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :%s/tw/BM^ | - ]]) - - feed("/") - screen:expect([[ - Inc substitution on | - BMo lines | - Inc substitution on | - BMo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :%s/tw/BM/^ | - ]]) - - feed("") - screen:expect([[ - Inc substitution on | - BMo lines | - Inc substitution on | - ^BMo lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :%s/tw/BM/ | - ]]) - end) - -end) - -describe("'incsubstitute' with a failing expression", function() - if helpers.pending_win32(pending) then return end - - local screen = Screen.new(20,10) - local cases = { "", "split", "nosplit" } - - local function refresh(case) - clear() - common_setup(screen, case, default_text) - end - - it('in the pattern does nothing for', function() - for _, case in pairs(cases) do - refresh(case) - execute("set incsubstitute=" .. case) - feed(":silent! %s/tw\\(/LARD/") - expect(default_text) - end - end) - - it('in the replacement deletes the matches', function() - for _, case in pairs(cases) do - refresh(case) - local replacements = { "\\='LARD", "\\=xx_novar__xx" } - - for _, repl in pairs(replacements) do - execute("set incsubstitute=" .. case) - feed(":silent! %s/tw/" .. repl .. "/") - expect(default_text:gsub("tw", "")) - execute("undo") - end - end - end) - -end) - -describe("'incsubstitute' and :cnoremap", function() - local cases = { "", "split", "nosplit" } - - local function refresh(case) - clear() - common_setup(nil, case, default_text) - end - - it('work with remapped characters', function() - for _, case in pairs(cases) do - refresh(case) - local command = "%s/lines/LINES/g" - - for i = 1, string.len(command) do - local c = string.sub(command, i, i) - execute("cnoremap ".. c .. " " .. c) - end - - execute(command) - expect([[ - Inc substitution on - two LINES - ]]) - end - end) - - it('work when mappings move the cursor', function() - for _, case in pairs(cases) do - refresh(case) - execute("cnoremap ,S LINES/") - - feed(":%s/lines/,Sor three ") - expect([[ - Inc substitution on - two or three LINES - ]]) - - execute("cnoremap ;S /X/") - feed(":%s/;SI") - expect([[ - Xnc substitution on - two or three LXNES - ]]) - - execute("cnoremap ,T //Y/") - feed(":%s,TX") - expect([[ - Ync substitution on - two or three LYNES - ]]) - - execute("cnoremap ;T s//Z/") - feed(":%;TY") - expect([[ - Znc substitution on - two or three LZNES - ]]) - end - end) - - it('work with a failing mapping', function() - for _, case in pairs(cases) do - refresh(case) - execute("cnoremap x execute('bwipeout!')[-1].'x'") - - feed(":%s/tw/tox") - - -- error thrown b/c of the mapping - neq(nil, eval('v:errmsg'):find('^E523:')) - -- the substitution after the error only works for ics=split/nosplit - -- which seems like the right thing to do in all cases, but we probably - -- don't want to change the default, so all in all this seems alright - if case == '' then - expect(default_text) - else - expect(default_text:gsub("tw", "tox")) - end - end - end) - - it('work when temporarily moving the cursor', function() - for _, case in pairs(cases) do - refresh(case) - execute("cnoremap x cursor(1, 1)[-1].'x'") - - feed(":%s/tw/tox/g") - expect(default_text:gsub("tw", "tox")) - end - end) - - it('work when a mapping disables incsub', function() - for _, case in pairs(cases) do - refresh(case) - execute("cnoremap x execute('set incsubstitute=')[-1]") - - feed(":%s/tw/toxa/g") - expect(default_text:gsub("tw", "toa")) - end - end) - - it('work with a complex mapping', function() - for _, case in pairs(cases) do - refresh(case) - source([[cnoremap x eextend(g:, {'fo': getcmdline()}) - \.fo:new:bw!:=remove(g:, 'fo')x]]) - - feed(":%s/tw/tox") - feed("/") - expect(default_text:gsub("tw", "tox")) - end - end) - -end) - -describe("'incsubstitute': autocommands", function() - before_each(clear) - - -- keys are events to be tested - -- values are arrays like - -- { open = { 1 }, close = { 2, 3} } - -- which would mean that during the test below the event fires for - -- buffer 1 when opening an incsub window, and for buffers 2 and 3 - -- when closing an incsub window - local eventsExpected = { - BufAdd = {}, - BufDelete = {}, - BufEnter = {}, - BufFilePost = {}, - BufFilePre = {}, - BufHidden = {}, - BufLeave = {}, - BufNew = {}, - BufNewFile = {}, - BufRead = {}, - BufReadCmd = {}, - BufReadPre = {}, - BufUnload = {}, - BufWinEnter = {}, - BufWinLeave = {}, - BufWipeout = {}, - BufWrite = {}, - BufWriteCmd = {}, - BufWritePost = {}, - Syntax = {}, - FileType = {}, - WinEnter = {}, - WinLeave = {}, - CmdwinEnter = {}, - CmdwinLeave = {}, - } - - local function bufferlist(t) - local s = "" - for _, buffer in pairs(t) do - s = s .. ", " .. tostring(buffer) - end - return s - end - - -- fill the table with default values - for event, _ in pairs(eventsExpected) do - eventsExpected[event].open = eventsExpected[event].open or {} - eventsExpected[event].close = eventsExpected[event].close or {} - end - - local function register_autocmd(event) - meths.set_var(event .. "_fired", {}) - execute("autocmd " .. event .. " * call add(g:" .. event .. "_fired, expand(''))") - end - - it('are not fired when splitting', function() - common_setup(nil, "split", default_text) - - local eventsObserved = {} - for event, _ in pairs(eventsExpected) do - eventsObserved[event] = {} - register_autocmd(event) - end - - feed(":%s/tw") - - for event, _ in pairs(eventsExpected) do - eventsObserved[event].open = meths.get_var(event .. "_fired") - meths.set_var(event .. "_fired", {}) - end - - feed("/") - - for event, _ in pairs(eventsExpected) do - eventsObserved[event].close = meths.get_var(event .. "_fired") - end - - for event, _ in pairs(eventsExpected) do - eq(event .. bufferlist(eventsExpected[event].open), - event .. bufferlist(eventsObserved[event].open)) - eq(event .. bufferlist(eventsExpected[event].close), - event .. bufferlist(eventsObserved[event].close)) - end - end) - -end) - -describe("'incsubstitute': split windows", function() - if helpers.pending_win32(pending) then return end - - local screen - local function refresh() - clear() - screen = Screen.new(40,30) - common_setup(screen, "split", default_text) - end - - after_each(function() - screen:detach() - end) - - it('work after more splits', function() - refresh() - - execute("vsplit") - execute("split") - feed(":%s/tw") - screen:expect([[ - two lines {10:|}two lines | - {10:|} | - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {11:[No Name] [+] }{10:|}{15:~ }| - two lines {10:|}{15:~ }| - {10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {10:[No Name] [+] [No Name] [+] }| - |2| two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw^ | - ]]) - - feed("") - execute("only") - execute("split") - execute("vsplit") - - feed(":%s/tw") - screen:expect([[ - Inc substitution on {10:|}Inc substitution on| - two lines {10:|}two lines | - {10:|} | - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {15:~ }{10:|}{15:~ }| - {11:[No Name] [+] }{10:[No Name] [+] }| - Inc substitution on | - two lines | - | - {15:~ }| - {15:~ }| - {10:[No Name] [+] }| - |2| two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw^ | - ]]) - end) - - local settings = { - "splitbelow", - "splitright", - "noequalalways", - "equalalways eadirection=ver", - "equalalways eadirection=hor", - "equalalways eadirection=both", - } - - it("are not affected by various settings", function() - for _, setting in pairs(settings) do - refresh() - execute("set " .. setting) - - feed(":%s/tw") - - screen:expect([[ - Inc substitution on | - two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {11:[No Name] [+] }| - |2| two lines | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {10:[Preview] }| - :%s/tw^ | - ]]) - end - end) - -end) -- cgit