diff options
author | Justin M. Keyes <justinkz@gmail.com> | 2016-11-09 03:19:22 +0100 |
---|---|---|
committer | Justin M. Keyes <justinkz@gmail.com> | 2016-11-09 03:19:22 +0100 |
commit | 0213e99aaf6eba303fd459183dd14a4a11cc5b07 (patch) | |
tree | 9cf382b3ac1922520f9934cf47887430d99bc023 | |
parent | bd33f1165b95e9f8f0d20ae3a941ee55906107ee (diff) | |
parent | aa0e09d251610f9b17f0bf96a4f3485032c36e0f (diff) | |
download | rneovim-0213e99aaf6eba303fd459183dd14a4a11cc5b07.tar.gz rneovim-0213e99aaf6eba303fd459183dd14a4a11cc5b07.tar.bz2 rneovim-0213e99aaf6eba303fd459183dd14a4a11cc5b07.zip |
Merge #5561 'inccommand'
Initial work by:
Robin Elrharbi-Fleury (Robinhola)
Audrey Rayé (Adrey06)
Philémon Hullot (DesbyP)
Aymeric Collange (aym7)
Clément Guyomard (Clement0)
Major revisions by:
KillTheMule
Björn Linse <bjorn.linse@gmail.com>
Justin M. Keyes <justinkz@gmail.com>
-rw-r--r-- | runtime/doc/intro.txt | 2 | ||||
-rw-r--r-- | runtime/doc/options.txt | 10 | ||||
-rw-r--r-- | runtime/doc/syntax.txt | 3 | ||||
-rw-r--r-- | runtime/doc/vim_diff.txt | 32 | ||||
-rw-r--r-- | runtime/syntax/vim.vim | 7 | ||||
-rw-r--r-- | src/nvim/buffer.c | 93 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 684 | ||||
-rw-r--r-- | src/nvim/ex_cmds.h | 17 | ||||
-rw-r--r-- | src/nvim/ex_cmds.lua | 6 | ||||
-rw-r--r-- | src/nvim/ex_cmds_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 46 | ||||
-rw-r--r-- | src/nvim/ex_docmd.h | 15 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 23 | ||||
-rw-r--r-- | src/nvim/fileio.c | 64 | ||||
-rw-r--r-- | src/nvim/globals.h | 6 | ||||
-rw-r--r-- | src/nvim/mark.c | 2 | ||||
-rw-r--r-- | src/nvim/memline.c | 15 | ||||
-rw-r--r-- | src/nvim/option.c | 11 | ||||
-rw-r--r-- | src/nvim/option_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/options.lua | 8 | ||||
-rw-r--r-- | src/nvim/quickfix.c | 5 | ||||
-rw-r--r-- | src/nvim/syntax.c | 14 | ||||
-rw-r--r-- | src/nvim/undo.c | 86 | ||||
-rw-r--r-- | src/nvim/window.c | 3 | ||||
-rw-r--r-- | test/functional/ui/inccommand_spec.lua | 1398 |
25 files changed, 2148 insertions, 404 deletions
diff --git a/runtime/doc/intro.txt b/runtime/doc/intro.txt index cbe017e051..786097dd74 100644 --- a/runtime/doc/intro.txt +++ b/runtime/doc/intro.txt @@ -450,7 +450,7 @@ notation meaning equivalent decimal value(s) ~ <k0> - <k9> keypad 0 to 9 *keypad-0* *keypad-9* <S-...> shift-key *shift* *<S-* <C-...> control-key *control* *ctrl* *<C-* -<M-...> alt-key or meta-key *meta* *alt* *<M-* +<M-...> alt-key or meta-key *META* *meta* *alt* *<M-* <A-...> same as <M-...> *<A-* <D-...> command-key or "super" key *<D-* <t_xx> key with "xx" entry in termcap diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index bcd9c08c26..94c5e2d11d 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -3416,6 +3416,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| @@ -3896,6 +3905,7 @@ A jump table for the options with a short description can be found at |Q_op|. global Strings to use in 'list' mode and for the |:list| command. It is a comma separated list of string settings. + *lcs-eol* eol:c Character to show at the end of each line. When omitted, there is no extra character at the end of the diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt index 308fa90ab3..a9762a7121 100644 --- a/runtime/doc/syntax.txt +++ b/runtime/doc/syntax.txt @@ -4852,6 +4852,9 @@ SignColumn column where |signs| are displayed *hl-IncSearch* IncSearch 'incsearch' highlighting; also used for the text replaced with ":s///c" + *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 590994b7aa..c4795bec57 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -63,7 +63,7 @@ these differences. 3. New Features *nvim-features* -MAJOR FEATURES ~ +MAJOR COMPONENTS ~ Embedded terminal emulator |terminal-emulator| RPC API |RPC| @@ -84,25 +84,29 @@ avoids features that cannot be provided on all platforms--instead that is delegated to external plugins/extensions. -OTHER FEATURES ~ +ARCHITECTURE ~ -|bracketed-paste-mode| is built-in and enabled by default. +External plugins run in separate processes. |remote-plugin| This improves +stability and allows those plugins to perform tasks without blocking the +editor. Even "legacy" Python and Ruby plugins which use the old Vim interfaces +(|if_py| and |if_ruby|) run out-of-process. + + +FEATURES ~ -Meta (alt) chords are recognized (even in the terminal). - <M-1>, <M-2>, ... - <M-BS>, <M-Del>, <M-Ins>, ... - <M-/>, <M-\>, ... - <M-Space>, <M-Enter>, <M-=>, <M-->, <M-?>, <M-$>, ... +|bracketed-paste-mode| is built-in and enabled by default. - Note: Meta chords are case-sensitive (<M-a> is distinguished from <M-A>). +|META| (ALT) chords are recognized, even in the terminal. Any |<M-| mapping +will work. Some examples: <M-1>, <M-2>, <M-BS>, <M-Del>, <M-Ins>, <M-/>, +<M-\>, <M-Space>, <M-Enter>, <M-=>, <M-->, <M-?>, <M-$>, ... +META chords are case-sensitive: <M-a> and <M-A> are two different keycodes. Some `CTRL-SHIFT-...` key chords are distinguished from `CTRL-...` variants (even in the terminal). Specifically, the following are known to work: - <C-Tab>, <C-S-Tab> - <C-BS>, <C-S-BS> - <C-Enter>, <C-S-Enter> + <C-Tab>, <C-S-Tab>, <C-BS>, <C-S-BS>, <C-Enter>, <C-S-Enter> Options: + 'inccommand' shows results while typing a |:substitute| command 'statusline' supports unlimited alignment sections 'tabline' %@Func@foo%X can call any function on mouse-click @@ -123,12 +127,13 @@ Functions: Events: |TabNewEntered| - |TermOpen| |TermClose| + |TermOpen| |TextYankPost| Highlight groups: |hl-QuickFixLine| + |hl-Substitute| |hl-TermCursor| |hl-TermCursorNC| @@ -295,6 +300,7 @@ Other commands: :mode (no longer accepts an argument) :open :shell + :smile :tearoff Other compile-time features: diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim index 32e871ea79..ae01c0f3ee 100644 --- a/runtime/syntax/vim.vim +++ b/runtime/syntax/vim.vim @@ -1,9 +1,4 @@ " Vim syntax file -" Language: Vim 7.4 script -" Maintainer: Charles E. Campbell <NdrOchipS@PcampbellAfamily.Mbiz> -" Last Change: March 29, 2016 -" Version: 7.4-45 -" Automatically generated keyword lists: {{{1 " ############################################################################# " ############################################################################# @@ -61,7 +56,7 @@ syn keyword vimGroup contained Comment Constant String Character Number Boolean syn keyword vimHLGroup contained ColorColumn Cursor CursorColumn CursorIM CursorLine CursorLineNr DiffAdd DiffChange DiffDelete DiffText Directory ErrorMsg FoldColumn Folded IncSearch LineNr MatchParen Menu ModeMsg MoreMsg NonText Normal Pmenu PmenuSbar PmenuSel PmenuThumb Question Scrollbar Search SignColumn SpecialKey SpellBad SpellCap SpellLocal SpellRare StatusLine StatusLineNC TabLine TabLineFill TabLineSel Title Tooltip VertSplit Visual WarningMsg WildMenu syn match vimHLGroup contained "Conceal" syn keyword vimOnlyHLGroup contained VisualNOS -syn keyword nvimHLGroup contained EndOfBuffer TermCursor TermCursorNC QuickFixLine +syn keyword nvimHLGroup contained EndOfBuffer IncSubstitute TermCursor TermCursorNC QuickFixLine "}}}2 syn case match " Special Vim Highlighting (not automatic) {{{1 diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index a66fdc1304..75caf2223b 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -268,6 +268,9 @@ open_buffer ( bool buf_valid(buf_T *buf) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { + if (buf == NULL) { + return false; + } FOR_ALL_BUFFERS(bp) { if (bp == buf) { return true; @@ -479,6 +482,18 @@ void buf_clear_file(buf_T *buf) buf->b_ml.ml_flags = ML_EMPTY; /* empty buffer */ } +/// Clears the current buffer contents. +void buf_clear(void) +{ + linenr_T line_count = curbuf->b_ml.ml_line_count; + while (!(curbuf->b_ml.ml_flags & ML_EMPTY)) { + ml_delete((linenr_T)1, false); + } + deleted_lines_mark(1, line_count); // prepare for display + ml_close(curbuf, true); // free memline_T + buf_clear_file(curbuf); +} + /// buf_freeall() - free all things allocated for a buffer that are related to /// the file. Careful: get here with "curwin" NULL when exiting. /// @@ -671,14 +686,15 @@ void handle_swap_exists(buf_T *old_curbuf) * aborting() returns FALSE when closing a buffer. */ enter_cleanup(&cs); - /* User selected Quit at ATTENTION prompt. Go back to previous - * buffer. If that buffer is gone or the same as the current one, - * open a new, empty buffer. */ - swap_exists_action = SEA_NONE; /* don't want it again */ - swap_exists_did_quit = TRUE; - close_buffer(curwin, curbuf, DOBUF_UNLOAD, FALSE); - if (!buf_valid(old_curbuf) || old_curbuf == curbuf) + // User selected Quit at ATTENTION prompt. Go back to previous + // buffer. If that buffer is gone or the same as the current one, + // open a new, empty buffer. + swap_exists_action = SEA_NONE; // don't want it again + swap_exists_did_quit = true; + close_buffer(curwin, curbuf, DOBUF_UNLOAD, false); + if (!buf_valid(old_curbuf) || old_curbuf == curbuf) { old_curbuf = buflist_new(NULL, NULL, 1L, BLN_CURBUF | BLN_LISTED); + } if (old_curbuf != NULL) { enter_buffer(old_curbuf); if (old_tw != curbuf->b_p_tw) @@ -1319,28 +1335,29 @@ void do_autochdir(void) } } -/* - * functions for dealing with the buffer list - */ +// +// functions for dealing with the buffer list +// -/* - * Add a file name to the buffer list. Return a pointer to the buffer. - * If the same file name already exists return a pointer to that buffer. - * If it does not exist, or if fname == NULL, a new entry is created. - * If (flags & BLN_CURBUF) is TRUE, may use current buffer. - * If (flags & BLN_LISTED) is TRUE, add new buffer to buffer list. - * If (flags & BLN_DUMMY) is TRUE, don't count it as a real buffer. - * This is the ONLY way to create a new buffer. - */ -static int top_file_num = 1; /* highest file number */ - -buf_T * -buflist_new ( - char_u *ffname, /* full path of fname or relative */ - char_u *sfname, /* short fname or NULL */ - linenr_T lnum, /* preferred cursor line */ - int flags /* BLN_ defines */ -) +static int top_file_num = 1; ///< highest file number + +/// Add a file name to the buffer list. +/// If the same file name already exists return a pointer to that buffer. +/// If it does not exist, or if fname == NULL, a new entry is created. +/// If (flags & BLN_CURBUF) is TRUE, may use current buffer. +/// If (flags & BLN_LISTED) is TRUE, add new buffer to buffer list. +/// If (flags & BLN_DUMMY) is TRUE, don't count it as a real buffer. +/// If (flags & BLN_NEW) is TRUE, don't use an existing buffer. +/// This is the ONLY way to create a new buffer. +/// +/// @param ffname full path of fname or relative +/// @param sfname short fname or NULL +/// @param lnum preferred cursor line +/// @param flags BLN_ defines +/// @param bufnr +/// +/// @return pointer to the buffer +buf_T * buflist_new(char_u *ffname, char_u *sfname, linenr_T lnum, int flags) { buf_T *buf; @@ -2374,10 +2391,11 @@ buf_T *setaltfname(char_u *ffname, char_u *sfname, linenr_T lnum) { buf_T *buf; - /* Create a buffer. 'buflisted' is not set if it's a new buffer */ + // Create a buffer. 'buflisted' is not set if it's a new buffer buf = buflist_new(ffname, sfname, lnum, 0); - if (buf != NULL && !cmdmod.keepalt) + if (buf != NULL && !cmdmod.keepalt) { curwin->w_alt_fnum = buf->b_fnum; + } return buf; } @@ -2412,8 +2430,9 @@ int buflist_add(char_u *fname, int flags) buf_T *buf; buf = buflist_new(fname, NULL, (linenr_T)0, flags); - if (buf != NULL) + if (buf != NULL) { return buf->b_fnum; + } return 0; } @@ -5253,3 +5272,15 @@ wipe_buffer ( unblock_autocmds(); } } + +/// Creates or switches to a special-purpose buffer. +/// +/// @param bufnr Buffer to switch to, or 0 to create a new buffer. +void buf_open_special(handle_T bufnr, char *bufname, char *buftype) +{ + (void)do_ecmd((int)bufnr, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, NULL); + (void)setfname(curbuf, (char_u *)bufname, NULL, true); + set_option_value((char_u *)"bt", 0L, (char_u *)buftype, OPT_LOCAL); + set_option_value((char_u *)"swf", 0L, NULL, OPT_LOCAL); + RESET_BINDING(curwin); +} diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 4674460a16..13a0282b76 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -8,7 +8,11 @@ #include <string.h> #include <stdlib.h> #include <inttypes.h> +#include <math.h> +#include "nvim/api/private/defs.h" +#include "nvim/api/buffer.h" +#include "nvim/log.h" #include "nvim/vim.h" #include "nvim/ascii.h" #include "nvim/ex_cmds.h" @@ -83,6 +87,15 @@ typedef struct { SubIgnoreType do_ic; ///< ignore case flag } subflags_T; +/// Lines matched during :substitute. +typedef struct { + linenr_T lnum; + long nmatch; + char_u *line; + kvec_t(colnr_T) cols; ///< columns of in-line matches +} MatchedLine; +typedef kvec_t(MatchedLine) MatchedLineVec; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ex_cmds.c.generated.h" #endif @@ -1524,8 +1537,9 @@ int rename_buffer(char_u *new_fname) curbuf->b_flags |= BF_NOTEDITED; if (xfname != NULL && *xfname != NUL) { buf = buflist_new(fname, xfname, curwin->w_cursor.lnum, 0); - if (buf != NULL && !cmdmod.keepalt) + if (buf != NULL && !cmdmod.keepalt) { curwin->w_alt_fnum = buf->b_fnum; + } } xfree(fname); xfree(sfname); @@ -2019,37 +2033,34 @@ theend: return retval; } -/* - * start editing a new file - * - * fnum: file number; if zero use ffname/sfname - * ffname: the file name - * - full path if sfname used, - * - any file name if sfname is NULL - * - empty string to re-edit with the same file name (but may be - * in a different directory) - * - NULL to start an empty buffer - * sfname: the short file name (or NULL) - * eap: contains the command to be executed after loading the file and - * forced 'ff' and 'fenc' - * newlnum: if > 0: put cursor on this line number (if possible) - * if ECMD_LASTL: use last position in loaded file - * if ECMD_LAST: use last position in all files - * if ECMD_ONE: use first line - * flags: - * ECMD_HIDE: if TRUE don't free the current buffer - * ECMD_SET_HELP: set b_help flag of (new) buffer before opening file - * ECMD_OLDBUF: use existing buffer if it exists - * ECMD_FORCEIT: ! used for Ex command - * ECMD_ADDBUF: don't edit, just add to buffer list - * oldwin: Should be "curwin" when editing a new buffer in the current - * window, NULL when splitting the window first. When not NULL info - * of the previous buffer for "oldwin" is stored. - * - * return FAIL for failure, OK otherwise - */ -int -do_ecmd ( +/// start editing a new file +/// +/// @param fnum file number; if zero use ffname/sfname +/// @param ffname the file name +/// - full path if sfname used, +/// - any file name if sfname is NULL +/// - empty string to re-edit with the same file name (but may +/// be in a different directory) +/// - NULL to start an empty buffer +/// @param sfname the short file name (or NULL) +/// @param eap contains the command to be executed after loading the file +/// and forced 'ff' and 'fenc' +/// @param newlnum if > 0: put cursor on this line number (if possible) +/// ECMD_LASTL: use last position in loaded file +/// ECMD_LAST: use last position in all files +/// ECMD_ONE: use first line +/// @param flags ECMD_HIDE: if TRUE don't free the current buffer +/// ECMD_SET_HELP: set b_help flag of (new) buffer before +/// opening file +/// ECMD_OLDBUF: use existing buffer if it exists +/// ECMD_FORCEIT: ! used for Ex command +/// ECMD_ADDBUF: don't edit, just add to buffer list +/// @param oldwin Should be "curwin" when editing a new buffer in the current +/// window, NULL when splitting the window first. When not NULL +/// info of the previous buffer for "oldwin" is stored. +/// +/// @return FAIL for failure, OK otherwise +int do_ecmd( int fnum, char_u *ffname, char_u *sfname, @@ -2174,9 +2185,10 @@ do_ecmd ( buflist_altfpos(oldwin); } - if (fnum) + if (fnum) { buf = buflist_findnr(fnum); - else { + } else { + ILOG("here"); if (flags & ECMD_ADDBUF) { linenr_T tlnum = 1L; @@ -2189,7 +2201,7 @@ do_ecmd ( goto theend; } buf = buflist_new(ffname, sfname, 0L, - BLN_CURBUF | ((flags & ECMD_SET_HELP) ? 0 : BLN_LISTED)); + BLN_CURBUF | (flags & ECMD_SET_HELP ? 0 : BLN_LISTED)); // Autocmds may change curwin and curbuf. if (oldwin != NULL) { oldwin = curwin; @@ -2978,10 +2990,12 @@ static bool sub_joining_lines(exarg_T *eap, char_u *pat, ex_may_print(eap); } - if (!cmdmod.keeppatterns) { - save_re_pat(RE_SUBST, pat, p_magic); + if (!eap->is_live) { + if (!cmdmod.keeppatterns) { + save_re_pat(RE_SUBST, pat, p_magic); + } + add_to_history(HIST_SEARCH, pat, true, NUL); } - add_to_history(HIST_SEARCH, pat, TRUE, NUL); return true; } @@ -3087,16 +3101,17 @@ static char_u *sub_parse_flags(char_u *cmd, subflags_T *subflags, return cmd; } -/* do_sub() - * - * Perform a substitution from line eap->line1 to line eap->line2 using the - * command pointed to by eap->arg which should be of the form: - * - * /pattern/substitution/{flags} - * - * The usual escapes are supported as described in the regexp docs. - */ -void do_sub(exarg_T *eap) +/// do_sub() +/// +/// Perform a substitution from line eap->line1 to line eap->line2 using the +/// command pointed to by eap->arg which should be of the form: +/// +/// /pattern/substitution/{flags} +/// +/// The usual escapes are supported as described in the regexp docs. +/// +/// @return buffer used for 'inccommand' preview +buf_T *do_sub(exarg_T *eap) { long i = 0; regmmatch_T regmatch; @@ -3112,20 +3127,22 @@ void do_sub(exarg_T *eap) }; char_u *pat = NULL, *sub = NULL; // init for GCC int delimiter; + bool has_second_delim = false; int sublen; int got_quit = false; int got_match = false; int which_pat; char_u *cmd = eap->arg; linenr_T first_line = 0; // first changed line - linenr_T last_line= 0; // below last changed line AFTER the - // change + linenr_T last_line= 0; // below last changed line AFTER the change linenr_T old_line_count = curbuf->b_ml.ml_line_count; - char_u *sub_firstline; // allocated copy of first sub line - bool endcolumn = false; // cursor in last column when done + char_u *sub_firstline; // allocated copy of first sub line + bool endcolumn = false; // cursor in last column when done + MatchedLineVec matched_lines = KV_INITIAL_VALUE; pos_T old_cursor = curwin->w_cursor; int start_nsubs; int save_ma = 0; + int save_b_changed = curbuf->b_changed; if (!global_busy) { sub_nsubs = 0; @@ -3144,7 +3161,7 @@ void do_sub(exarg_T *eap) /* don't accept alphanumeric for separator */ if (isalpha(*cmd)) { EMSG(_("E146: Regular expressions can't be delimited by letters")); - return; + return NULL; } /* * undocumented vi feature: @@ -3155,21 +3172,26 @@ void do_sub(exarg_T *eap) ++cmd; if (vim_strchr((char_u *)"/?&", *cmd) == NULL) { EMSG(_(e_backslash)); - return; + return NULL; } - if (*cmd != '&') - which_pat = RE_SEARCH; /* use last '/' pattern */ - pat = (char_u *)""; /* empty search pattern */ - delimiter = *cmd++; /* remember delimiter character */ - } else { /* find the end of the regexp */ - if (p_altkeymap && curwin->w_p_rl) + if (*cmd != '&') { + which_pat = RE_SEARCH; // use last '/' pattern + } + pat = (char_u *)""; // empty search pattern + delimiter = *cmd++; // remember delimiter character + has_second_delim = true; + } else { // find the end of the regexp + if (p_altkeymap && curwin->w_p_rl) { lrF_sub(cmd); - which_pat = RE_LAST; /* use last used regexp */ - delimiter = *cmd++; /* remember delimiter character */ - pat = cmd; /* remember start of search pat */ + } + which_pat = RE_LAST; // use last used regexp + delimiter = *cmd++; // remember delimiter character + pat = cmd; // remember start of search pat cmd = skip_regexp(cmd, delimiter, p_magic, &eap->arg); - if (cmd[0] == delimiter) /* end delimiter found */ - *cmd++ = NUL; /* replace it with a NUL */ + if (cmd[0] == delimiter) { // end delimiter found + *cmd++ = NUL; // replace it with a NUL + has_second_delim = true; + } } /* @@ -3188,7 +3210,7 @@ void do_sub(exarg_T *eap) mb_ptr_adv(cmd); } - if (!eap->skip) { + if (!eap->skip && !eap->is_live) { sub_set_replacement((SubReplacementString) { .sub = xstrdup((char *) sub), .timestamp = os_time(), @@ -3198,7 +3220,7 @@ void do_sub(exarg_T *eap) } else if (!eap->skip) { /* use previous pattern and substitution */ if (old_sub.sub == NULL) { /* there is no previous command */ EMSG(_(e_nopresub)); - return; + return NULL; } pat = NULL; /* search_regcomp() will use previous pattern */ sub = (char_u *) old_sub.sub; @@ -3209,7 +3231,7 @@ void do_sub(exarg_T *eap) } if (sub_joining_lines(eap, pat, sub, cmd)) { - return; + return NULL; } cmd = sub_parse_flags(cmd, &subflags, &which_pat); @@ -3223,7 +3245,7 @@ void do_sub(exarg_T *eap) i = getdigits_long(&cmd); if (i <= 0 && !eap->skip && subflags.do_error) { EMSG(_(e_zerocount)); - return; + return NULL; } eap->line1 = eap->line2; eap->line2 += i - 1; @@ -3239,24 +3261,27 @@ void do_sub(exarg_T *eap) eap->nextcmd = check_nextcmd(cmd); if (eap->nextcmd == NULL) { EMSG(_(e_trailing)); - return; + return NULL; } } - if (eap->skip) /* not executing commands, only parsing */ - return; + if (eap->skip) { // not executing commands, only parsing + return NULL; + } if (!subflags.do_count && !MODIFIABLE(curbuf)) { // Substitution is not allowed in non-'modifiable' buffer EMSG(_(e_modifiable)); - return; + return NULL; } - if (search_regcomp(pat, RE_SUBST, which_pat, SEARCH_HIS, ®match) == FAIL) { + int search_options = eap->is_live ? 0 : SEARCH_HIS; + if (search_regcomp(pat, RE_SUBST, which_pat, search_options, + ®match) == FAIL) { if (subflags.do_error) { EMSG(_(e_invcmd)); } - return; + return NULL; } // the 'i' or 'I' flag overrules 'ignorecase' and 'smartcase' @@ -3343,6 +3368,8 @@ void do_sub(exarg_T *eap) sub_firstlnum = lnum; copycol = 0; matchcol = 0; + // the current match + MatchedLine matched_line = { 0, 0, NULL, KV_INITIAL_VALUE }; /* At first match, remember current cursor position. */ if (!got_match) { @@ -3379,6 +3406,12 @@ void do_sub(exarg_T *eap) curwin->w_cursor.lnum = lnum; do_again = FALSE; + if (eap->is_live) { + // Increment the in-line match count and store the column. + matched_line.nmatch++; + kv_push(matched_line.cols, regmatch.startpos[0].col); + } + /* * 1. Match empty string does not count, except for first * match. This reproduces the strange vi behaviour. @@ -3426,7 +3459,7 @@ void do_sub(exarg_T *eap) goto skip; } - if (subflags.do_ask) { + if (subflags.do_ask && !eap->is_live) { int typed = 0; /* change State to CONFIRM, so that the mouse works @@ -3597,134 +3630,131 @@ void do_sub(exarg_T *eap) * use "\=col("."). */ curwin->w_cursor.col = regmatch.startpos[0].col; - /* - * 3. substitute the string. - */ - if (subflags.do_count) { - // prevent accidentally changing the buffer by a function - save_ma = curbuf->b_p_ma; - curbuf->b_p_ma = false; - sandbox++; - } - // Save flags for recursion. They can change for e.g. - // :s/^/\=execute("s#^##gn") - subflags_T subflags_save = subflags; - // get length of substitution part - sublen = vim_regsub_multi(®match, - sub_firstlnum - regmatch.startpos[0].lnum, - sub, sub_firstline, false, p_magic, true); - // Don't keep flags set by a recursive call - subflags = subflags_save; - if (subflags.do_count) { - curbuf->b_p_ma = save_ma; - if (sandbox > 0) { - sandbox--; + // 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 + save_ma = curbuf->b_p_ma; + curbuf->b_p_ma = false; + sandbox++; + } + // Save flags for recursion. They can change for e.g. + // :s/^/\=execute("s#^##gn") + subflags_T subflags_save = subflags; + // get length of substitution part + sublen = vim_regsub_multi(®match, + sub_firstlnum - regmatch.startpos[0].lnum, + sub, sub_firstline, false, p_magic, true); + // Don't keep flags set by a recursive call + subflags = subflags_save; + if (subflags.do_count) { + curbuf->b_p_ma = save_ma; + if (sandbox > 0) { + sandbox--; + } + goto skip; } - goto skip; - } - - /* When the match included the "$" of the last line it may - * go beyond the last line of the buffer. */ - if (nmatch > curbuf->b_ml.ml_line_count - sub_firstlnum + 1) { - nmatch = curbuf->b_ml.ml_line_count - sub_firstlnum + 1; - skip_match = TRUE; - } - /* Need room for: - * - result so far in new_start (not for first sub in line) - * - original text up to match - * - length of substituted part - * - original text after match - */ - if (nmatch == 1) - p1 = sub_firstline; - else { - p1 = ml_get(sub_firstlnum + nmatch - 1); - nmatch_tl += nmatch - 1; - } - size_t copy_len = regmatch.startpos[0].col - copycol; - new_end = sub_grow_buf(&new_start, - copy_len + (STRLEN(p1) - regmatch.endpos[0].col) - + sublen + 1); + // When the match included the "$" of the last line it may + // go beyond the last line of the buffer. + if (nmatch > curbuf->b_ml.ml_line_count - sub_firstlnum + 1) { + nmatch = curbuf->b_ml.ml_line_count - sub_firstlnum + 1; + skip_match = true; + } - /* - * copy the text up to the part that matched - */ - memmove(new_end, sub_firstline + copycol, (size_t)copy_len); - new_end += copy_len; - - (void)vim_regsub_multi(®match, - sub_firstlnum - regmatch.startpos[0].lnum, - sub, new_end, TRUE, p_magic, TRUE); - sub_nsubs++; - did_sub = TRUE; - - /* Move the cursor to the start of the line, to avoid that it - * is beyond the end of the line after the substitution. */ - curwin->w_cursor.col = 0; - - /* For a multi-line match, make a copy of the last matched - * line and continue in that one. */ - if (nmatch > 1) { - sub_firstlnum += nmatch - 1; - xfree(sub_firstline); - sub_firstline = vim_strsave(ml_get(sub_firstlnum)); - // When going beyond the last line, stop substituting. - if (sub_firstlnum <= line2) { - do_again = true; + // Need room for: + // - result so far in new_start (not for first sub in line) + // - original text up to match + // - length of substituted part + // - original text after match + if (nmatch == 1) { + p1 = sub_firstline; } else { - subflags.do_all = false; + p1 = ml_get(sub_firstlnum + nmatch - 1); + nmatch_tl += nmatch - 1; } - } + size_t copy_len = regmatch.startpos[0].col - copycol; + new_end = sub_grow_buf(&new_start, + (STRLEN(p1) - regmatch.endpos[0].col) + + copy_len + sublen + 1); + + // copy the text up to the part that matched + memmove(new_end, sub_firstline + copycol, (size_t)copy_len); + new_end += copy_len; + + (void)vim_regsub_multi(®match, + sub_firstlnum - regmatch.startpos[0].lnum, + sub, new_end, true, p_magic, true); + sub_nsubs++; + did_sub = true; - /* Remember next character to be copied. */ - copycol = regmatch.endpos[0].col; + // Move the cursor to the start of the line, to avoid that it + // is beyond the end of the line after the substitution. + curwin->w_cursor.col = 0; - if (skip_match) { - /* Already hit end of the buffer, sub_firstlnum is one - * less than what it ought to be. */ - xfree(sub_firstline); - sub_firstline = vim_strsave((char_u *)""); - copycol = 0; - } + // For a multi-line match, make a copy of the last matched + // line and continue in that one. + if (nmatch > 1) { + sub_firstlnum += nmatch - 1; + xfree(sub_firstline); + sub_firstline = vim_strsave(ml_get(sub_firstlnum)); + // When going beyond the last line, stop substituting. + if (sub_firstlnum <= line2) { + do_again = true; + } else { + subflags.do_all = false; + } + } - /* - * Now the trick is to replace CTRL-M chars with a real line - * break. This would make it impossible to insert a CTRL-M in - * the text. The line break can be avoided by preceding the - * CTRL-M with a backslash. To be able to insert a backslash, - * they must be doubled in the string and are halved here. - * That is Vi compatible. - */ - for (p1 = new_end; *p1; ++p1) { - if (p1[0] == '\\' && p1[1] != NUL) /* remove backslash */ - STRMOVE(p1, p1 + 1); - else if (*p1 == CAR) { - if (u_inssub(lnum) == OK) { /* prepare for undo */ - *p1 = NUL; /* truncate up to the CR */ - ml_append(lnum - 1, new_start, - (colnr_T)(p1 - new_start + 1), FALSE); - mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L); - if (subflags.do_ask) { - appended_lines(lnum - 1, 1L); - } else { - if (first_line == 0) { - first_line = lnum; + // Remember next character to be copied. + copycol = regmatch.endpos[0].col; + + if (skip_match) { + // Already hit end of the buffer, sub_firstlnum is one + // less than what it ought to be. + xfree(sub_firstline); + sub_firstline = vim_strsave((char_u *)""); + copycol = 0; + } + + // Now the trick is to replace CTRL-M chars with a real line + // break. This would make it impossible to insert a CTRL-M in + // the text. The line break can be avoided by preceding the + // CTRL-M with a backslash. To be able to insert a backslash, + // they must be doubled in the string and are halved here. + // That is Vi compatible. + for (p1 = new_end; *p1; p1++) { + if (p1[0] == '\\' && p1[1] != NUL) { // remove backslash + STRMOVE(p1, p1 + 1); + } else if (*p1 == CAR) { + if (u_inssub(lnum) == OK) { // prepare for undo + *p1 = NUL; // truncate up to the CR + ml_append(lnum - 1, new_start, + (colnr_T)(p1 - new_start + 1), false); + mark_adjust(lnum + 1, (linenr_T)MAXLNUM, 1L, 0L); + if (subflags.do_ask) { + appended_lines(lnum - 1, 1L); + } else { + if (first_line == 0) { + first_line = lnum; + } + last_line = lnum + 1; } - last_line = lnum + 1; + // All line numbers increase. + sub_firstlnum++; + lnum++; + line2++; + // move the cursor to the new line, like Vi + curwin->w_cursor.lnum++; + // copy the rest + STRMOVE(new_start, p1 + 1); + p1 = new_start - 1; } - /* All line numbers increase. */ - ++sub_firstlnum; - ++lnum; - ++line2; - /* move the cursor to the new line, like Vi */ - ++curwin->w_cursor.lnum; - /* copy the rest */ - STRMOVE(new_start, p1 + 1); - p1 = new_start - 1; + } else if (has_mbyte) { + p1 += (*mb_ptr2len)(p1) - 1; } - } else if (has_mbyte) - p1 += (*mb_ptr2len)(p1) - 1; + } } // 4. If subflags.do_all is set, find next match. @@ -3845,6 +3875,12 @@ skip: xfree(new_start); /* for when substitute was cancelled */ xfree(sub_firstline); /* free the copy of the original line */ sub_firstline = NULL; + + if (eap->is_live) { + matched_line.lnum = lnum; + matched_line.line = vim_strsave(ml_get(lnum)); + kv_push(matched_lines, matched_line); + } } line_breakcheck(); @@ -3880,7 +3916,7 @@ skip: beginline(BL_WHITE | BL_FIX); } } - if (!do_sub_msg(subflags.do_count) && subflags.do_ask) { + if (!eap->is_live && !do_sub_msg(subflags.do_count) && subflags.do_ask) { MSG(""); } } else { @@ -3912,6 +3948,26 @@ skip: // Restore the flag values, they can be used for ":&&". subflags.do_all = save_do_all; subflags.do_ask = save_do_ask; + + // 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) { + curbuf->b_changed = save_b_changed; // preserve 'modified' during preview + preview_buf = show_sub(old_cursor, pat, sub, eap->line1, eap->line2, + &matched_lines); + + } else if (*p_icm != NUL && eap->is_live) { + curwin->w_cursor = old_cursor; // don't move the cursor + } + + for (MatchedLine m; kv_size(matched_lines);) { + m = kv_pop(matched_lines); + xfree(m.line); + kv_destroy(m.cols); + } + kv_destroy(matched_lines); + + return preview_buf; } // NOLINT(readability/fn_size) /* @@ -5793,50 +5849,39 @@ static enum EXP_SIGN_NAMES /* expand with name of placed signs */ } expand_what; -/* - * Function given to ExpandGeneric() to obtain the sign command - * expansion. - */ +/// Function given to ExpandGeneric() to obtain the sign command +/// expansion. char_u * get_sign_name(expand_T *xp, int idx) { - sign_T *sp; - int current_idx; - - switch (expand_what) - { + switch (expand_what) + { case EXP_SUBCMD: - return (char_u *)cmds[idx]; - case EXP_DEFINE: - { - char *define_arg[] = - { - "icon=", "linehl=", "text=", "texthl=", NULL - }; - return (char_u *)define_arg[idx]; - } - case EXP_PLACE: - { - char *place_arg[] = - { - "line=", "name=", "file=", "buffer=", NULL - }; - return (char_u *)place_arg[idx]; - } - case EXP_UNPLACE: - { - char *unplace_arg[] = { "file=", "buffer=", NULL }; - return (char_u *)unplace_arg[idx]; - } - case EXP_SIGN_NAMES: - /* Complete with name of signs already defined */ - current_idx = 0; - for (sp = first_sign; sp != NULL; sp = sp->sn_next) - if (current_idx++ == idx) - return sp->sn_name; - return NULL; + return (char_u *)cmds[idx]; + case EXP_DEFINE: { + char *define_arg[] = { "icon=", "linehl=", "text=", "texthl=", NULL }; + return (char_u *)define_arg[idx]; + } + case EXP_PLACE: { + char *place_arg[] = { "line=", "name=", "file=", "buffer=", NULL }; + return (char_u *)place_arg[idx]; + } + case EXP_UNPLACE: { + char *unplace_arg[] = { "file=", "buffer=", NULL }; + return (char_u *)unplace_arg[idx]; + } + case EXP_SIGN_NAMES: { + // Complete with name of signs already defined + int current_idx = 0; + for (sign_T *sp = first_sign; sp != NULL; sp = sp->sn_next) { + if (current_idx++ == idx) { + return sp->sn_name; + } + } + } + return NULL; default: - return NULL; - } + return NULL; + } } /* @@ -5956,3 +6001,174 @@ void set_context_in_sign_cmd(expand_T *xp, char_u *arg) } } } + +/// Shows the effects of the :substitute command being typed ('inccommand'). +/// If inccommand=split, shows a preview window and later restores the layout. +static buf_T *show_sub(pos_T old_cusr, 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 + + garray_T save_winsizes; + win_T *save_curwin = curwin; + cmdmod_T save_cmdmod = cmdmod; + char_u *save_shm_p = vim_strsave(p_shm); + size_t sub_size = mb_string2cells(sub); + size_t pat_size = mb_string2cells(pat); + + // We keep a special-purpose buffer around, but don't assume it exists. + 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 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_icm != 'n') && (sub_size || pat_size); + if (preview_buf == curbuf) { // Preview buffer cannot preview itself! + split = false; + preview_buf = NULL; + } + + // 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; + MatchedLine curmatch; + for (size_t j = 0; j < matched_lines->size && cur_col == -1; j++) { + curmatch = matched_lines->items[j]; + if (curmatch.lnum == old_cusr.lnum) { + // On cursor line; iterate in-line matches to find one after cursor. + for (size_t i = 0; i < curmatch.cols.size; i++) { + if (curmatch.cols.items[i] >= old_cusr.col) { + cur_col = curmatch.cols.items[i]; + curwin->w_cursor.lnum = curmatch.lnum; + curwin->w_cursor.col = cur_col; + break; + } + } + } else if (curmatch.lnum > old_cusr.lnum) { + // After cursor; put cursor on first match there. + cur_col = curmatch.cols.items[0]; + curwin->w_cursor.lnum = curmatch.lnum; + curwin->w_cursor.col = cur_col; + } + } + + if (split && win_split((int)p_cwh, WSP_BOT) != FAIL) { + buf_open_special(preview_buf ? bufnr : 0, "[Preview]", "incsub"); + buf_clear(); + preview_buf = curbuf; + set_option_value((char_u *)"bufhidden", 0L, (char_u *)"hide", OPT_LOCAL); + bufnr = preview_buf->handle; + curbuf->b_p_bl = false; + curbuf->b_p_ma = true; + curbuf->b_p_ul = -1; + curbuf->b_p_tw = 0; // Reset 'textwidth' (was set by ftplugin) + curwin->w_p_cul = false; + curwin->w_p_spell = false; + curwin->w_p_fen = false; + + // Width of the "| lnum|..." column which displays the line numbers. + linenr_T highest_num_line = kv_last(*matched_lines).lnum; + int col_width = log10(highest_num_line) + 1 + 3; + + char *str = NULL; + size_t old_line_size = 0; + size_t line_size; + int src_id_highlight = 0; + int hl_id = syn_check_group((char_u *)"Substitute", 13); + + // 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; + + // Reallocate if str not long enough + if (line_size > old_line_size) { + str = xrealloc(str, line_size * sizeof(char)); + old_line_size = line_size; + } + + // 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); + + // highlight the replaced part + if (sub_size > 0) { + for (size_t i = 0; i < mat.cols.size; i++) { + colnr_T col_start = mat.cols.items[i] + col_width + + i * (sub_size - pat_size) + 1; + colnr_T col_end = col_start - 1 + sub_size; + src_id_highlight = bufhl_add_hl(curbuf, src_id_highlight, hl_id, + line + 1, col_start, col_end); + } + } + } + xfree(str); + } + + redraw_later(SOME_VALID); + + win_enter(save_curwin, false); // Return to original window + win_size_restore(&save_winsizes); + ga_clear(&save_winsizes); + + set_option_value((char_u *)"shm", 0L, save_shm_p, 0); + xfree(save_shm_p); + + // Update screen now. Must do this _before_ close_windows(). + int save_rd = RedrawingDisabled; + RedrawingDisabled = 0; + update_screen(NOT_VALID); + RedrawingDisabled = save_rd; + + cmdmod = save_cmdmod; + + return preview_buf; +} + +/// :substitute command +/// +/// 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_icm == NUL || !eap->is_live) { // 'inccommand' is disabled + (void)do_sub(eap); + return; + } + + char_u *save_eap = eap->arg; + save_search_patterns(); + int save_changedtick = curbuf->b_changedtick; + time_t save_b_u_time_cur = curbuf->b_u_time_cur; + u_header_T *save_b_u_newhead = curbuf->b_u_newhead; + 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 show_sub() opens window/buffer + emsg_off++; // No error messages for live commands + + buf_T *preview_buf = do_sub(eap); + + if (save_changedtick != curbuf->b_changedtick) { + if (!u_undo_and_forget(1)) { abort(); } + // Restore newhead. It is meaningless when curhead is valid, but we must + // restore it so that undotree() is identical before/after the preview. + curbuf->b_u_newhead = save_b_u_newhead; + curbuf->b_u_time_cur = save_b_u_time_cur; + curbuf->b_changedtick = save_changedtick; + } + if (buf_valid(preview_buf)) { + // XXX: Must do this *after* u_undo_and_forget(), why? + close_windows(preview_buf, false); + } + curbuf->b_p_ul = save_b_p_ul; + eap->arg = save_eap; + restore_search_patterns(); + emsg_off--; + unblock_autocmds(); +} diff --git a/src/nvim/ex_cmds.h b/src/nvim/ex_cmds.h index 721145efd8..ccb2202edb 100644 --- a/src/nvim/ex_cmds.h +++ b/src/nvim/ex_cmds.h @@ -5,14 +5,17 @@ #include "nvim/os/time.h" #include "nvim/eval_defs.h" +#include "nvim/pos.h" + +// flags for do_ecmd() +#define ECMD_HIDE 0x01 // don't free the current buffer +#define ECMD_SET_HELP 0x02 // set b_help flag of (new) buffer before + // opening file +#define ECMD_OLDBUF 0x04 // use existing buffer if it exists +#define ECMD_FORCEIT 0x08 // ! used in Ex command +#define ECMD_ADDBUF 0x10 // don't edit, just add to buffer list +#define ECMD_RESERVED_BUFNR 0x20 // bufnr argument is reserved bufnr -/* flags for do_ecmd() */ -#define ECMD_HIDE 0x01 /* don't free the current buffer */ -#define ECMD_SET_HELP 0x02 /* set b_help flag of (new) buffer before - opening file */ -#define ECMD_OLDBUF 0x04 /* use existing buffer if it exists */ -#define ECMD_FORCEIT 0x08 /* ! used in Ex command */ -#define ECMD_ADDBUF 0x10 /* don't edit, just add to buffer list */ /* for lnum argument in do_ecmd() */ #define ECMD_LASTL (linenr_T)0 /* use last position in loaded file */ diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index 3f5d9b3244..95ede4bdc5 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -2194,7 +2194,7 @@ return { command='substitute', flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN), addr_type=ADDR_LINES, - func='do_sub', + func='ex_substitute', }, { command='sNext', @@ -3181,7 +3181,7 @@ return { enum='CMD_and', flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN, MODIFY), addr_type=ADDR_LINES, - func='do_sub', + func='ex_substitute', }, { command='<', @@ -3222,6 +3222,6 @@ return { enum='CMD_tilde', flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN, MODIFY), addr_type=ADDR_LINES, - func='do_sub', + func='ex_substitute', }, } diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h index 8148eb5cee..8a2ebe2cd4 100644 --- a/src/nvim/ex_cmds_defs.h +++ b/src/nvim/ex_cmds_defs.h @@ -124,6 +124,7 @@ struct exarg { LineGetter getline; ///< Function used to get the next line void *cookie; ///< argument for getline() struct condstack *cstack; ///< condition stack for ":if" etc. + bool is_live; ///< 'inccommand' live preview }; #define FORCE_BIN 1 // ":edit ++bin file" diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 5e418bf099..30347cbe85 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -271,7 +271,7 @@ do_exmode ( int do_cmdline_cmd(char *cmd) { return do_cmdline((char_u *)cmd, NULL, NULL, - DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED); + DOCMD_NOWAIT|DOCMD_KEYTYPED); } /* @@ -597,11 +597,11 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline, * do_one_cmd() will return NULL if there is no trailing '|'. * "cmdline_copy" can change, e.g. for '%' and '#' expansion. */ - ++recursive; - next_cmdline = do_one_cmd(&cmdline_copy, flags & DOCMD_VERBOSE, - &cstack, - cmd_getline, cmd_cookie); - --recursive; + recursive++; + next_cmdline = do_one_cmd(&cmdline_copy, flags, + &cstack, + cmd_getline, cmd_cookie); + recursive--; if (cmd_cookie == (void *)&cmd_loop_cookie) /* Use "current_line" from "cmd_loop_cookie", it may have been @@ -1225,7 +1225,7 @@ static void get_wincmd_addr_type(char_u *arg, exarg_T *eap) * This function may be called recursively! */ static char_u * do_one_cmd(char_u **cmdlinep, - int sourcing, + int flags, struct condstack *cstack, LineGetter fgetline, void *cookie /* argument for fgetline() */ @@ -1248,7 +1248,8 @@ static char_u * do_one_cmd(char_u **cmdlinep, memset(&ea, 0, sizeof(ea)); ea.line1 = 1; ea.line2 = 1; - ++ex_nesting_level; + ea.is_live = flags & DOCMD_LIVE; + ex_nesting_level++; /* When the last file has not been edited :q has to be typed twice. */ if (quitmore @@ -1726,8 +1727,9 @@ static char_u * do_one_cmd(char_u **cmdlinep, if (ea.cmdidx == CMD_SIZE) { if (!ea.skip) { STRCPY(IObuff, _("E492: Not an editor command")); - if (!sourcing) + if (!(flags & DOCMD_VERBOSE)) { append_command(*cmdlinep); + } errormsg = IObuff; did_emsg_syntax = TRUE; } @@ -1809,7 +1811,7 @@ static char_u * do_one_cmd(char_u **cmdlinep, */ if (!global_busy && ea.line1 > ea.line2) { if (msg_silent == 0) { - if (sourcing || exmode_active) { + if ((flags & DOCMD_VERBOSE) || exmode_active) { errormsg = (char_u *)_("E493: Backwards range given"); goto doend; } @@ -2221,7 +2223,7 @@ doend: curwin->w_cursor.lnum = 1; if (errormsg != NULL && *errormsg != NUL && !did_emsg) { - if (sourcing) { + if (flags & DOCMD_VERBOSE) { if (errormsg != IObuff) { STRCPY(IObuff, errormsg); errormsg = IObuff; @@ -9647,3 +9649,25 @@ static void ex_terminal(exarg_T *eap) xfree(name); } } + +/// Check whether commandline starts with a live command +/// +/// @param[in] cmd Commandline to check. May start with a range. +/// +/// @return True if first command is a live command +bool cmd_is_live(char_u *cmd) +{ + if (cmd == NULL) { + return false; + } + + exarg_T ea; + // parse the command line + ea.cmd = skip_range(cmd, NULL); + if (*ea.cmd == '*') { + ea.cmd = skipwhite(ea.cmd + 1); + } + find_command(&ea, NULL); + + return (ea.cmdidx == CMD_substitute); +} diff --git a/src/nvim/ex_docmd.h b/src/nvim/ex_docmd.h index bafad20169..84f1a13a15 100644 --- a/src/nvim/ex_docmd.h +++ b/src/nvim/ex_docmd.h @@ -3,13 +3,14 @@ #include "nvim/ex_cmds_defs.h" -/* flags for do_cmdline() */ -#define DOCMD_VERBOSE 0x01 /* included command in error message */ -#define DOCMD_NOWAIT 0x02 /* don't call wait_return() and friends */ -#define DOCMD_REPEAT 0x04 /* repeat exec. until getline() returns NULL */ -#define DOCMD_KEYTYPED 0x08 /* don't reset KeyTyped */ -#define DOCMD_EXCRESET 0x10 /* reset exception environment (for debugging)*/ -#define DOCMD_KEEPLINE 0x20 /* keep typed line for repeating with "." */ +// flags for do_cmdline() +#define DOCMD_VERBOSE 0x01 // included command in error message +#define DOCMD_NOWAIT 0x02 // don't call wait_return() and friends +#define DOCMD_REPEAT 0x04 // repeat exec. until getline() returns NULL +#define DOCMD_KEYTYPED 0x08 // don't reset KeyTyped +#define DOCMD_EXCRESET 0x10 // reset exception environment (for debugging +#define DOCMD_KEEPLINE 0x20 // keep typed line for repeating with "." +#define DOCMD_LIVE 0x40 // show updates as-you-type ("live" command) /* defines for eval_vars() */ #define VALID_PATH 1 diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index e525c949bd..17693ecfc8 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1591,6 +1591,15 @@ static int command_line_changed(CommandLineState *s) msg_starthere(); redrawcmdline(); s->did_incsearch = true; + } else if (s->firstc == ':' + && KeyTyped // only if interactive + && *p_icm != NUL // 'inccommand' is set + && curbuf->b_p_ma // buffer is modifiable + && cmdline_star == 0 // not typing a password + && cmd_is_live(ccline.cmdbuff)) { + // process a "live" command ('inccommand') + do_cmdline(ccline.cmdbuff, NULL, NULL, DOCMD_KEEPLINE|DOCMD_LIVE); + redrawcmdline(); } if (cmdmsg_rl || (p_arshape && !p_tbidi && enc_utf8)) { @@ -5137,16 +5146,12 @@ static int ex_window(void) } cmdwin_type = get_cmdline_type(); - /* Create the command-line buffer empty. */ - (void)do_ecmd(0, NULL, NULL, NULL, ECMD_ONE, ECMD_HIDE, NULL); - (void)setfname(curbuf, (char_u *)"[Command Line]", NULL, TRUE); - set_option_value((char_u *)"bt", 0L, (char_u *)"nofile", OPT_LOCAL); - set_option_value((char_u *)"swf", 0L, NULL, OPT_LOCAL); - curbuf->b_p_ma = TRUE; - curwin->w_p_fen = FALSE; + // Create empty command-line buffer. + buf_open_special(0, "[Command Line]", "nofile"); curwin->w_p_rl = cmdmsg_rl; - cmdmsg_rl = FALSE; - RESET_BINDING(curwin); + cmdmsg_rl = false; + curbuf->b_p_ma = true; + curwin->w_p_fen = false; /* Do execute autocommands for setting the filetype (load syntax). */ unblock_autocmds(); diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 0eb475d425..934d81dcd0 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -5062,10 +5062,10 @@ void buf_reload(buf_T *buf, int orig_mode) * the old contents. Can't use memory only, the file might be * too big. Use a hidden buffer to move the buffer contents to. */ - if (bufempty() || saved == FAIL) + if (bufempty() || saved == FAIL) { savebuf = NULL; - else { - /* Allocate a buffer without putting it in the buffer list. */ + } else { + // Allocate a buffer without putting it in the buffer list. savebuf = buflist_new(NULL, NULL, (linenr_T)1, BLN_DUMMY); if (savebuf != NULL && buf == curbuf) { /* Open the memline. */ @@ -6342,27 +6342,24 @@ aucmd_prepbuf ( aco->new_curbuf = curbuf; } -/* - * Cleanup after executing autocommands for a (hidden) buffer. - * Restore the window as it was (if possible). - */ -void -aucmd_restbuf ( - aco_save_T *aco /* structure holding saved values */ -) +/// Cleanup after executing autocommands for a (hidden) buffer. +/// Restore the window as it was (if possible). +/// +/// @param aco structure holding saved values +void aucmd_restbuf(aco_save_T *aco) { int dummy; if (aco->use_aucmd_win) { - --curbuf->b_nwindows; - /* Find "aucmd_win", it can't be closed, but it may be in another tab - * page. Do not trigger autocommands here. */ + curbuf->b_nwindows--; + // Find "aucmd_win", it can't be closed, but it may be in another tab page. + // Do not trigger autocommands here. block_autocmds(); if (curwin != aucmd_win) { FOR_ALL_TAB_WINDOWS(tp, wp) { if (wp == aucmd_win) { if (tp != curtab) { - goto_tabpage_tp(tp, TRUE, TRUE); + goto_tabpage_tp(tp, true, true); } win_goto(aucmd_win); goto win_found; @@ -6371,49 +6368,50 @@ aucmd_restbuf ( } win_found: - /* Remove the window and frame from the tree of frames. */ + // Remove the window and frame from the tree of frames. (void)winframe_remove(curwin, &dummy, NULL); win_remove(curwin, NULL); - aucmd_win_used = FALSE; - last_status(FALSE); /* may need to remove last status line */ - restore_snapshot(SNAP_AUCMD_IDX, FALSE); - (void)win_comp_pos(); /* recompute window positions */ + aucmd_win_used = false; + last_status(false); // may need to remove last status line + restore_snapshot(SNAP_AUCMD_IDX, false); + (void)win_comp_pos(); // recompute window positions unblock_autocmds(); - if (win_valid(aco->save_curwin)) + if (win_valid(aco->save_curwin)) { curwin = aco->save_curwin; - else - /* Hmm, original window disappeared. Just use the first one. */ + } else { + // Hmm, original window disappeared. Just use the first one. curwin = firstwin; - vars_clear(&aucmd_win->w_vars->dv_hashtab); /* free all w: variables */ - hash_init(&aucmd_win->w_vars->dv_hashtab); /* re-use the hashtab */ + } + vars_clear(&aucmd_win->w_vars->dv_hashtab); // free all w: variables + hash_init(&aucmd_win->w_vars->dv_hashtab); // re-use the hashtab curbuf = curwin->w_buffer; xfree(globaldir); globaldir = aco->globaldir; - /* the buffer contents may have changed */ + // the buffer contents may have changed check_cursor(); if (curwin->w_topline > curbuf->b_ml.ml_line_count) { curwin->w_topline = curbuf->b_ml.ml_line_count; curwin->w_topfill = 0; } } else { - /* restore curwin */ + // restore curwin if (win_valid(aco->save_curwin)) { - /* Restore the buffer which was previously edited by curwin, if - * it was changed, we are still the same window and the buffer is - * valid. */ + // Restore the buffer which was previously edited by curwin, if it was + // changed, we are still the same window and the buffer is valid. if (curwin == aco->new_curwin && curbuf != aco->new_curbuf && buf_valid(aco->new_curbuf) && aco->new_curbuf->b_ml.ml_mfp != NULL) { - if (curwin->w_s == &curbuf->b_s) + if (curwin->w_s == &curbuf->b_s) { curwin->w_s = &aco->new_curbuf->b_s; - --curbuf->b_nwindows; + } + curbuf->b_nwindows--; curbuf = aco->new_curbuf; curwin->w_buffer = curbuf; - ++curbuf->b_nwindows; + curbuf->b_nwindows++; } curwin = aco->save_curwin; diff --git a/src/nvim/globals.h b/src/nvim/globals.h index e42382ad00..301a2c1663 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -600,9 +600,9 @@ EXTERN int redraw_tabline INIT(= FALSE); /* need to redraw tabline */ * All buffers are linked in a list. 'firstbuf' points to the first entry, * 'lastbuf' to the last entry and 'curbuf' to the currently active buffer. */ -EXTERN buf_T *firstbuf INIT(= NULL); /* first buffer */ -EXTERN buf_T *lastbuf INIT(= NULL); /* last buffer */ -EXTERN buf_T *curbuf INIT(= NULL); /* currently active buffer */ +EXTERN buf_T *firstbuf INIT(= NULL); // first buffer +EXTERN buf_T *lastbuf INIT(= NULL); // last buffer +EXTERN buf_T *curbuf INIT(= NULL); // currently active buffer // Iterates over all buffers in the buffer list. # define FOR_ALL_BUFFERS(buf) for (buf_T *buf = firstbuf; buf != NULL; buf = buf->b_next) diff --git a/src/nvim/mark.c b/src/nvim/mark.c index 2a65cf396b..6453c41415 100644 --- a/src/nvim/mark.c +++ b/src/nvim/mark.c @@ -473,7 +473,7 @@ static void fname2fnum(xfmark_T *fm) os_dirname(IObuff, IOSIZE); p = path_shorten_fname(NameBuff, IObuff); - /* buflist_new() will call fmarks_check_names() */ + // buflist_new() will call fmarks_check_names() (void)buflist_new(NameBuff, p, (linenr_T)1, 0); } } diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 5505335769..a9a53ebca7 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -2340,14 +2340,13 @@ int ml_replace(linenr_T lnum, char_u *line, int copy) return OK; } -/* - * Delete line 'lnum' in the current buffer. - * - * Check: The caller of this function should probably also call - * deleted_lines() after this. - * - * return FAIL for failure, OK otherwise - */ +/// Delete line `lnum` in the current buffer. +/// +/// @note The caller of this function should probably also call +/// deleted_lines() after this. +/// +/// @param message Show "--No lines in buffer--" message. +/// @return FAIL for failure, OK otherwise int ml_delete(linenr_T lnum, int message) { ml_flush_line(curbuf); diff --git a/src/nvim/option.c b/src/nvim/option.c index 311982982b..3bd1ce217e 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -288,6 +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_icm_values[]) = { "nosplit", "split", NULL }; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "option.c.generated.h" @@ -3100,9 +3101,13 @@ did_set_string_option ( else if (gvarp == &p_cino) { /* TODO: recognize errors */ parse_cino(curbuf); - } - /* Options that are a list of flags. */ - else { + // 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. + } else { p = NULL; if (varp == &p_ww) p = (char_u *)WW_ALL; diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 8af6f2194f..6e89a093c8 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -468,6 +468,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' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 583c63614a..14707aaa6c 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1188,6 +1188,14 @@ return { } }, { + 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'}, vi_def=true, diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index a7aff15121..f23037613b 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -3326,10 +3326,11 @@ load_dummy_buffer ( int failed = TRUE; aco_save_T aco; - /* Allocate a buffer without putting it in the buffer list. */ + // Allocate a buffer without putting it in the buffer list. newbuf = buflist_new(NULL, NULL, (linenr_T)1, BLN_DUMMY); - if (newbuf == NULL) + if (newbuf == NULL) { return NULL; + } /* Init the options. */ buf_copy_options(newbuf, BCO_ENTER | BCO_NOHELP); diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index b49ae9da21..e57965ac2c 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -5902,6 +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 Substitute Search", NULL }; @@ -7159,16 +7160,13 @@ int syn_namen2id(char_u *linep, int len) */ int syn_check_group(char_u *pp, int len) { - int id; - char_u *name; - - name = vim_strnsave(pp, len); - - id = syn_name2id(name); - if (id == 0) /* doesn't exist yet */ + char_u *name = vim_strnsave(pp, len); + int id = syn_name2id(name); + if (id == 0) { // doesn't exist yet id = syn_add_group(name); - else + } else { xfree(name); + } return id; } diff --git a/src/nvim/undo.c b/src/nvim/undo.c index d80aaf443a..2f4317980a 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -1669,7 +1669,7 @@ void u_undo(int count) undo_undoes = TRUE; else undo_undoes = !undo_undoes; - u_doit(count); + u_doit(count, false); } /* @@ -1678,15 +1678,56 @@ void u_undo(int count) */ void u_redo(int count) { - if (vim_strchr(p_cpo, CPO_UNDO) == NULL) - undo_undoes = FALSE; - u_doit(count); + if (vim_strchr(p_cpo, CPO_UNDO) == NULL) { + undo_undoes = false; + } + + u_doit(count, false); } -/* - * Undo or redo, depending on 'undo_undoes', 'count' times. - */ -static void u_doit(int startcount) +/// undo, and remove the undo branch from the undo tree. +bool u_undo_and_forget(int count) +{ + if (curbuf->b_u_synced == false) { + u_sync(true); + count = 1; + } + undo_undoes = true; + u_doit(count, true); + + if (curbuf->b_u_curhead == NULL) { + // nothing was undone. + return false; + } + + // Delete the current redo header + // set the redo header to the next alternative branch (if any) + // otherwise we will be in the leaf state + u_header_T *to_forget = curbuf->b_u_curhead; + curbuf->b_u_newhead = to_forget->uh_next.ptr; + curbuf->b_u_curhead = to_forget->uh_alt_next.ptr; + if (curbuf->b_u_curhead) { + to_forget->uh_alt_next.ptr = NULL; + curbuf->b_u_curhead->uh_alt_prev.ptr = to_forget->uh_alt_prev.ptr; + curbuf->b_u_seq_cur = curbuf->b_u_curhead->uh_seq-1; + } else if (curbuf->b_u_newhead) { + curbuf->b_u_seq_cur = curbuf->b_u_newhead->uh_seq; + } + if (to_forget->uh_alt_prev.ptr) { + to_forget->uh_alt_prev.ptr->uh_alt_next.ptr = curbuf->b_u_curhead; + } + if (curbuf->b_u_newhead) { + curbuf->b_u_newhead->uh_prev.ptr = curbuf->b_u_curhead; + } + if (curbuf->b_u_seq_last == to_forget->uh_seq) { + curbuf->b_u_seq_last--; + } + u_freebranch(curbuf, to_forget, NULL); + return true; +} + +/// Undo or redo, depending on `undo_undoes`, `count` times. +static void u_doit(int startcount, bool quiet) { int count = startcount; @@ -1722,7 +1763,7 @@ static void u_doit(int startcount) break; } - u_undoredo(TRUE); + u_undoredo(true); } else { if (curbuf->b_u_curhead == NULL || get_undolevel() <= 0) { beep_flush(); /* nothing to redo */ @@ -1742,7 +1783,7 @@ static void u_doit(int startcount) curbuf->b_u_curhead = curbuf->b_u_curhead->uh_prev.ptr; } } - u_undo_end(undo_undoes, FALSE); + u_undo_end(undo_undoes, false, quiet); } /* @@ -2055,7 +2096,7 @@ void undo_time(long step, int sec, int file, int absolute) } } } - u_undo_end(did_undo, absolute); + u_undo_end(did_undo, absolute, false); } /* @@ -2299,16 +2340,13 @@ static void u_undoredo(int undo) #endif } -/* - * If we deleted or added lines, report the number of less/more lines. - * Otherwise, report the number of changes (this may be incorrect - * in some cases, but it's better than nothing). - */ -static void -u_undo_end ( - int did_undo, /* just did an undo */ - int absolute /* used ":undo N" */ -) +/// If we deleted or added lines, report the number of less/more lines. +/// Otherwise, report the number of changes (this may be incorrect +/// in some cases, but it's better than nothing). +static void u_undo_end( + int did_undo, ///< just did an undo + int absolute, ///< used ":undo N" + bool quiet) { char *msgstr; u_header_T *uhp; @@ -2317,9 +2355,11 @@ u_undo_end ( if ((fdo_flags & FDO_UNDO) && KeyTyped) foldOpenCursor(); - if (global_busy /* no messages now, wait until global is finished */ - || !messaging()) /* 'lazyredraw' set, don't do messages now */ + if (quiet + || global_busy // no messages until global is finished + || !messaging()) { // 'lazyredraw' set, don't do messages now return; + } if (curbuf->b_ml.ml_flags & ML_EMPTY) --u_newcount; diff --git a/src/nvim/window.c b/src/nvim/window.c index 9c6a2e26a6..8512556c0a 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -2904,8 +2904,9 @@ static int win_alloc_firstwin(win_T *oldwin) /* Very first window, need to create an empty buffer for it and * initialize from scratch. */ curbuf = buflist_new(NULL, NULL, 1L, BLN_LISTED); - if (curbuf == NULL) + if (curbuf == NULL) { return FAIL; + } curwin->w_buffer = curbuf; curwin->w_s = &(curbuf->b_s); curbuf->b_nwindows = 1; /* there is one window */ diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua new file mode 100644 index 0000000000..f2282e3fb8 --- /dev/null +++ b/test/functional/ui/inccommand_spec.lua @@ -0,0 +1,1398 @@ +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=split does not trigger preview", function() + before_each(function() + clear() + common_setup(nil, "split", default_text) + end) + + it("if invoked by a script ", function() + source('%s/tw/MO/g') + wait() + eq(1, eval("bufnr('$')")) + + -- sanity check: assert the buffer state + expect(default_text:gsub("tw", "MO")) + end) + + it("if invoked by feedkeys()", function() + -- in a script... + source([[:call feedkeys(":%s/tw/MO/g\<CR>")]]) + wait() + -- or interactively... + feed([[:call feedkeys(":%s/tw/MO/g\<CR>")<CR>]]) + wait() + eq(1, eval("bufnr('$')")) + + -- sanity check: assert the buffer state + expect(default_text:gsub("tw", "MO")) + 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) + + for _, case in pairs{"", "split", "nosplit"} do + it("various delimiters (inccommand="..case..")", function() + 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 + + for _, case in pairs{"", "split", "nosplit"} do + it("'undolevels' (inccommand="..case..")", function() + execute("set undolevels=139") + execute("setlocal undolevels=34") + execute("set inccommand=" .. case) + insert("as") + feed(":%s/as/glork/<enter>") + eq(meths.get_option('undolevels'), 139) + eq(curbufmeths.get_option('undolevels'), 34) + end) + end + + for _, case in ipairs({"", "split", "nosplit"}) do + it("empty undotree() (inccommand="..case..")", function() + execute("set undolevels=1000") + execute("set inccommand=" .. case) + local expected_undotree = eval("undotree()") + + -- Start typing an incomplete :substitute command. + feed([[:%s/e/YYYY/g]]) + wait() + -- Cancel the :substitute. + feed([[<C-\><C-N>]]) + + -- The undo tree should be unchanged. + eq(expected_undotree, eval("undotree()")) + eq({}, eval("undotree()")["entries"]) + end) + end + + for _, case in ipairs({"", "split", "nosplit"}) do + it("undotree() with branches (inccommand="..case..")", function() + execute("set undolevels=1000") + execute("set inccommand=" .. case) + -- Make some changes. + feed([[isome text 1<C-\><C-N>]]) + feed([[osome text 2<C-\><C-N>]]) + -- Add an undo branch. + feed([[u]]) + -- More changes, more undo branches. + feed([[osome text 3<C-\><C-N>]]) + feed([[AX<C-\><C-N>]]) + feed([[...]]) + feed([[uu]]) + feed([[osome text 4<C-\><C-N>]]) + feed([[u<C-R>u]]) + feed([[osome text 5<C-\><C-N>]]) + expect([[ + some text 1 + some text 3XX + some text 5]]) + local expected_undotree = eval("undotree()") + eq(5, #expected_undotree["entries"]) -- sanity + + -- Start typing an incomplete :substitute command. + feed([[:%s/e/YYYY/g]]) + wait() + -- Cancel the :substitute. + feed([[<C-\><C-N>]]) + + -- The undo tree should be unchanged. + eq(expected_undotree, eval("undotree()")) + end) + end + + for _, case in pairs{"", "split", "nosplit"} do + it("b:changedtick (inccommand="..case..")", function() + execute("set inccommand=" .. case) + feed([[isome text 1<C-\><C-N>]]) + feed([[osome text 2<C-\><C-N>]]) + 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/<bs>", + ":%s/1/a", + ":%s/1/a<bs>", + ":%s/1/ax", + ":%s/1/ax<bs>", + ":%s/1/ax<bs><bs>", + ":%s/1/ax<bs><bs><bs>", + ":%s/1/ax/", + ":%s/1/ax/<bs>", + ":%s/1/ax/<bs>/", + ":%s/1/ax/g", + ":%s/1/ax/g<bs>", + ":%s/1/ax/g<bs><bs>" + } + + local function test_sub(substring, split, redoable) + clear() + execute("set inccommand=" .. split) + + insert("1") + feed("o2<esc>") + execute("undo") + feed("o3<esc>") + if redoable then + feed("o4<esc>") + execute("undo") + end + feed(substring.. "<enter>") + 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<esc>") + execute("undo") + feed("o3<esc>") + if redoable then + feed("o4<esc>") + execute("undo") + end + feed(substring .. "<esc>") + + feed("g-") + expect([[ + 1 + 2]]) + + feed("g+") + expect([[ + 1 + 3]]) + + if redoable then + feed("<c-r>") + expect([[ + 1 + 3 + 4]]) + end + end + + + local function test_threetree(substring, split) + clear() + execute("set inccommand=" .. split) + + insert("1") + feed("o2<esc>") + feed("o3<esc>") + feed("uu") + feed("oa<esc>") + feed("ob<esc>") + feed("uu") + feed("oA<esc>") + feed("oB<esc>") + + -- 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 .. "<esc>") + feed("<c-r>") + expect([[ + 1 + A]]) + + feed("g-") -- go to b + feed("2u") + feed(substring .. "<esc>") + feed("<c-r>") + expect([[ + 1 + a]]) + + feed("g-") -- go to 3 + feed("2u") + feed(substring .. "<esc>") + feed("<c-r>") + 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/<esc>") + 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<esc>") + feed(":%s/tw/MO/<esc>") + -- 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<enter>") + feed(":%s/MO/GO/g<enter>") + feed(":%s/GO/NO/g<enter>") + 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<esc>") + feed("Ay<esc>") + feed("Az<esc>") + feed(":%s/tw/AR<esc>") + -- 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<enter>") + feed(":%s/MO/GO/g<enter>") + feed(":%s/GO/NO/g<enter>") + feed(":%s/NO/LO/g<enter>") + 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<enter>") + -- 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<esc>") + feed(":%s/tw/MO/g<esc>") + 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("preserves 'modified' buffer flag", function() + execute("set nomodified") + 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^ | + ]]) + feed([[<C-\><C-N>]]) -- Cancel the :substitute command. + eq(0, eval("&modified")) + 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("<bs>") + 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<esc>') + 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<enter>") + + 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("<BS>") + wait() + feed("g") + wait() + feed("<BS>") + wait() + feed("g") + wait() + feed("<CR>") + wait() + feed(":vs tmp<enter>") + eq(3, helpers.call('bufnr', '$')) + end) + + it('works with the n flag', function() + feed(":%s/tw/Mix/n<enter>") + 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("<enter>") + 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', function() + for _, case in pairs(cases) do + refresh(case) + execute("set inccommand=" .. case) + feed(":silent! %s/tw\\(/LARD/<enter>") + 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 .. "/<enter>") + 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/<left><left><left><left><left><left>") + + feed(":%s/lines/,Sor three <enter>") + expect([[ + Inc substitution on + two or three LINES + ]]) + + execute("cnoremap ;S /X/<left><left><left>") + feed(":%s/;SI<enter>") + expect([[ + Xnc substitution on + two or three LXNES + ]]) + + execute("cnoremap ,T //Y/<left><left><left>") + feed(":%s,TX<enter>") + expect([[ + Ync substitution on + two or three LYNES + ]]) + + execute("cnoremap ;T s//Z/<left><left><left>") + feed(":%;TY<enter>") + expect([[ + Znc substitution on + two or three LZNES + ]]) + end + end) + + it('does not work with a failing mapping', function() + for _, case in pairs(cases) do + refresh(case) + execute("cnoremap <expr> x execute('bwipeout!')[-1].'x'") + + feed(":%s/tw/tox<enter>") + + -- error thrown b/c of the mapping + neq(nil, eval('v:errmsg'):find('^E523:')) + expect(default_text) + end + end) + + it('work when temporarily moving the cursor', function() + for _, case in pairs(cases) do + refresh(case) + execute("cnoremap <expr> x cursor(1, 1)[-1].'x'") + + feed(":%s/tw/tox/g<enter>") + 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 <expr> x execute('set inccommand=')[-1]") + + feed(":%s/tw/toxa/g<enter>") + 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 <C-\>eextend(g:, {'fo': getcmdline()}) + \.fo<CR><C-c>:new<CR>:bw!<CR>:<C-r>=remove(g:, 'fo')<CR>x]]) + + feed(":%s/tw/tox") + feed("/<enter>") + 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('<abuf>'))") + 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("/<enter>") + + 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("<esc>") + 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) |