The new 'inccommand' boolean option shows a live preview of :substitute replacements as you type on the command line. When enabled alongside 'incsearch', typing :%s/foo/bar/ will temporarily modify the buffer to show "bar" in place of "foo", updating on each keystroke. Pressing Escape restores the original buffer; pressing Enter executes the substitution normally.
This is a frequently requested feature (#14868, #10205). Neovim has had 'inccommand' since 2017, and the traces.vim plugin provides similar functionality. This implementation brings the core functionality natively to Vim, using a boolean option rather than Neovim's "nosplit"/"split" approach. The "split" preview window could be added later.
The replacement text is highlighted using the new Substitute highlight group, which links to IncSearch by default. This makes it easy to distinguish what changed at a glance, and colorschemes can customize it independently.
For non-substitute commands like :global, :vglobal, and :sort, the option enhances the existing incsearch behavior by highlighting all pattern matches in the range with IncSearch, not just the first one.
This feature works by saving original lines, applying the substitutions, redrawing, and then restoring. We share some logic with ex_substitute(), but there's a bit of code duplication that could be revisited.
Expression replacements (=) and multi-line matches are skipped during preview since they have side effects or require more complex handling. The command still executes normally when Enter is pressed.
Resolves: #14868
See also: #10205
I used a good amount of AI assistance (Claude Code) for the initial research and in building the core functions because I hadn't used the buffer internals before. It also helped generate the test cases.
https://github.com/vim/vim/pull/19772
(23 files)
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@jparise pushed 1 commit.
—
View it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@jparise pushed 1 commit.
—
View it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@jparise commented on this pull request.
In src/ex_getln.c:
> +// Command type set by parse_pattern_and_range().
+typedef enum {
+ PPR_UNKNOWN = 0,
+ PPR_SUBSTITUTE, // :substitute, :smagic, :snomagic
+ PPR_GLOBAL, // :global, :vglobal
+ PPR_SORT, // :sort, :uniq
+ PPR_VIMGREP // :vimgrep, :vimgrepadd, :lvimgrep, etc.
+} ppr_cmd_T;
I thought this would be a better abstraction that a single is_sub output flag, but it's also a bit speculative given that we only have a real use case for PPR_SUBSTITUTE.
(This is all to avoid re-parsing the command in the inccommand path to determine if we're dealing with a substitution.)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@64-bitman commented on this pull request.
> @@ -5139,6 +5139,30 @@ A jump table for the options with a short description can be found at |Q_op|.
It is not allowed to change text or jump to another window while
evaluating 'includeexpr' |textlock|.
+ *'inccommand'* *'icm'* *'noinccommand'* *'noicm'*
+'inccommand' 'icm' boolean (default off)
+ global
+ {not available when compiled without the
+ |+extra_search| feature}
+ When this option is set together with 'incsearch', the
I think we should rename it to incsub, inccommand sounds misleading since this is only for the substitute command
In src/ex_getln.c:
> + // Compile the search pattern. Temporarily NUL-terminate the pattern.
+ save_char = *pat_end;
+ *pat_end = NUL;
+ ++emsg_off;
+ i = search_regcomp(ccline.cmdbuff + skiplen, (size_t)patlen,
+ NULL, RE_SUBST, RE_SUBST,
+ SEARCH_KEEP, ®match);
+ --emsg_off;
+ *pat_end = save_char;
+ if (i == FAIL)
+ {
+ vim_free(sub);
+ return FALSE;
+ }
+
+ first_line = search_first_line == 0 ? 1 : search_first_line;
Is it possible to instead only do substution preview on buffer lines that are visible?
E.g. Loop through every window that is showing this buffer, find topline and botline, and find the min and max range that we must substitue
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@64-bitman commented on this pull request.
In src/ex_getln.c:
> + if (lnum <= last_line)
+ {
+ inccommand_substitute_preview_cleanup(state);
+ return FALSE;
+ }
+
+ // Add temporary match highlight for replacement positions using the
+ // Substitute highlight group. Directly construct a matchitem_T with
+ // position matches rather than a pattern.
+ if (state->sub_positions.ga_len > 0)
+ {
+ state->match = ALLOC_CLEAR_ONE(matchitem_T);
+ if (state->match != NULL)
+ {
+ state->match->mit_hlg_id =
+ syn_namen2id((char_u *)"Substitute",
I think we should make the highlighting for substitution preview use the 'highlight' option, see the hlf_T enum. Then we can make that occasion use the "Substitute" group by default (see HIGHLIGHT_INIT in optiondefs.h)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + + // Find next match. + nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, + matchcol, NULL); + if (nmatch <= 0 || regmatch.endpos[0].lnum > 0) + break; + } + + // Copy text after the last match. + if (matchcol < (colnr_T)STRLEN(line)) + ga_concat_len(&new_ga, line + matchcol, + STRLEN(line) - (size_t)matchcol); + + ga_append(&new_ga, NUL); + + ml_replace(lnum, new_ga.ga_data, FALSE);
Note that ml_replace can fail, if it does, then new_ga is leaked
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + (int)(last_line - first_line + 1));
+
+ for (lnum = first_line; lnum <= last_line; ++lnum)
+ {
+ long nmatch;
+ colnr_T matchcol = 0;
+ char_u *line;
+ garray_T new_ga;
+
+ nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum,
+ matchcol, NULL);
+ if (nmatch <= 0)
+ continue;
+
+ // Skip multi-line matches.
+ if (regmatch.endpos[0].lnum > 0)
Any reason for skipping multi line matches? Neovim supports it
Example:
:%s/\n//
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@jparise pushed 1 commit.
—
View it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@jparise commented on this pull request.
In src/ex_getln.c:
> + + // Find next match. + nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, + matchcol, NULL); + if (nmatch <= 0 || regmatch.endpos[0].lnum > 0) + break; + } + + // Copy text after the last match. + if (matchcol < (colnr_T)STRLEN(line)) + ga_concat_len(&new_ga, line + matchcol, + STRLEN(line) - (size_t)matchcol); + + ga_append(&new_ga, NUL); + + ml_replace(lnum, new_ga.ga_data, FALSE);
Right! And ga_append() can also fail. I pushed a change addresses both failure paths.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
anybody, please provide feedback
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I think that this feature, as it stands, is overly specific and needs to be generalised if it's going to be included. If interactive edit preview is going to be provided then it should be available for every command. I don't think there's anything special about :substitute.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
By the way, Neovim seems to have forgotten the inccommand for :uniq.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@jparise pushed 1 commit.
—
View it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
> + // Compile the search pattern. Temporarily NUL-terminate the pattern.
+ save_char = *pat_end;
+ *pat_end = NUL;
+ ++emsg_off;
+ i = search_regcomp(ccline.cmdbuff + skiplen, (size_t)patlen,
+ NULL, RE_SUBST, RE_SUBST,
+ SEARCH_KEEP, ®match);
+ --emsg_off;
+ *pat_end = save_char;
+ if (i == FAIL)
+ {
+ vim_free(sub);
+ return FALSE;
+ }
+
+ first_line = search_first_line == 0 ? 1 : search_first_line;
Good idea! That's a nice optimization for large buffers. I implemented this across all windows in the current tab that are displaying the current buffer.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
> + (int)(last_line - first_line + 1));
+
+ for (lnum = first_line; lnum <= last_line; ++lnum)
+ {
+ long nmatch;
+ colnr_T matchcol = 0;
+ char_u *line;
+ garray_T new_ga;
+
+ nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum,
+ matchcol, NULL);
+ if (nmatch <= 0)
+ continue;
+
+ // Skip multi-line matches.
+ if (regmatch.endpos[0].lnum > 0)
I restricted this in the first version to simplify things. I should have called that out explicitly.
Multi-line matches complicate the preview implementation/restore because they can add or remove lines, but there's otherwise no reason not to support them. I'll do that if it looks like this feature is heading in the right direction.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
@jparise pushed 3 commits.
—
View it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Based on the "Principle of Least Astonishment," I am opposed to adding this option. Even with highlighting, it would temporarily overwrite the normal buffer display.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
If there was any principle that only buffer content characters may be displayed, it has been broken since the introduction of :help conceal more than 15 years ago
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I don't use flashy "conceal" features in my source code, and I don't think any such runtimes are bundled. Even if they exist, they shouldn't be enabled by default.
Are you going to bring up :h textprop next? (bitter smile)
Sure, you can do anything with map, abbrev, or autocmd if you really want to. But we don't include such "weird" settings by default, do we?
Are you trying to say, "Don't worry, 'inccommand' is off by default"?
Hmm... I feel this doesn't align with (what I consider) the Vim philosophy. The fact that it hasn't been ported to Vim until now seems to speak for itself.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I am not against this, when this is off per default In fact, I think this improves usability a bit, because one can see what effect the :s command can have, so I can see definitely merit of such an enhancement.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I think that this feature, as it stands, is overly specific. If interactive edit preview is going to be provided then it should be available for every command. I don't think there's anything special about
:substitute.
I believe this is quite a useful improvement and I agree it would be good to have for other related commands, e.g. :g/.../s/.../...
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Perhaps something like neovim's
:command-preview?
That would be nice, but I think implementing it only for native commands would be fine, at least for a first pass.
My main point was that this is currently an ad-hoc feature available for a single command that is really introducing a new feature class, being interactive edit preview. It makes no sense to me as a user for this to work for :substitute but not for :move, or :normal, or any text modifying command. The very idea is codified in the option name, it's not incsubstitute.
Unless it's implemented as a full edit preview feature it just feels like the inclusion of "plugin of the month" to me, something that has usually been carefully avoided.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I am not against this, when this is off per default In fact, I think this improves usability a bit, because one can see what effect the :s command can have, so I can see definitely merit of such an enhancement.
Isn't this exactly what plugins are for? We already have traces.vim which looks like it's more functional (assuming it works as advertised) than this PR.
I think "usability improvement" is such a low bar that there's probably many ahead in the queue. If the primary obstacle to having these included in core is a working PR, and we're willing to accept LLM generated PRs, we're on a very slippery slope.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
That is a fair point. We have now 2 maintainers voting against inclusion as of now. let's give it a bit more discussion time.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I worked on this for a few reasons: it was previously discussed with (nice-to-have) support on vim-dev; it's been natively available in neovim for a number of years; and :substitute previews are the only feature of traces.vim that I personally use. It also felt like a good, focused way to start, rather than a generalized preview system, although I see the benefits of the latter.
That being said, I also understand the arguments against including this directly in vim, especially when plugins already traces.vim exist.
There does appear to be some positive sentiment that an optional (and generalized) preview mode could be useful, so maybe that's a place to steer this discussion: maybe a separate window, something like neovim's split preview view, or some new ideas.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()