[vim/vim] Add 'inccommand' option for live :substitute preview (PR #19772)

52 views
Skip to first unread message

Jon Parise

unread,
Mar 20, 2026, 10:22:38 AM (7 days ago) Mar 20
to vim/vim, Subscribed

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.


You can view, comment on, or merge this pull request online at:

  https://github.com/vim/vim/pull/19772

Commit Summary

  • f3d0059 Add 'inccommand' option for live :substitute preview

File Changes

(23 files)

Patch Links:


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772@github.com>

Jon Parise

unread,
Mar 20, 2026, 10:35:33 AM (7 days ago) Mar 20
to vim/vim, Push

@jparise pushed 1 commit.

  • 0a64448 Add 'inccommand' option for live :substitute preview


View it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772/before/f3d0059665ccffe5d73485baa47a15e2d612addd/after/0a6444877fd183619dbf02bcc042d8ee280b0bf7@github.com>

Jon Parise

unread,
Mar 20, 2026, 11:04:07 AM (7 days ago) Mar 20
to vim/vim, Push

@jparise pushed 1 commit.

  • 4537bef Add 'inccommand' option for live :substitute preview


View it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772/before/0a6444877fd183619dbf02bcc042d8ee280b0bf7/after/4537befdded08ad67d4d9ddcdf3707847f630e91@github.com>

Jon Parise

unread,
Mar 20, 2026, 12:28:19 PM (7 days ago) Mar 20
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19772/review/3982629623@github.com>

Foxe Chen

unread,
Mar 20, 2026, 1:08:43 PM (7 days ago) Mar 20
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In runtime/doc/options.txt:

> @@ -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, &regmatch);
+    --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.Message ID: <vim/vim/pull/19772/review/3982808421@github.com>

Foxe Chen

unread,
Mar 20, 2026, 1:14:17 PM (7 days ago) Mar 20
to vim/vim, Subscribed

@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.Message ID: <vim/vim/pull/19772/review/3982898050@github.com>

Foxe Chen

unread,
Mar 20, 2026, 1:28:46 PM (7 days ago) Mar 20
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In src/ex_getln.c:

> +
+	    // Find next match.
+	    nmatch = vim_regexec_multi(&regmatch, 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.Message ID: <vim/vim/pull/19772/review/3982981020@github.com>

Foxe Chen

unread,
Mar 20, 2026, 1:34:46 PM (7 days ago) Mar 20
to vim/vim, Subscribed

@64-bitman commented on this pull request.


In src/ex_getln.c:

> +					    (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(&regmatch, 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.Message ID: <vim/vim/pull/19772/review/3983021604@github.com>

Jon Parise

unread,
Mar 20, 2026, 4:04:11 PM (6 days ago) Mar 20
to vim/vim, Push

@jparise pushed 1 commit.

  • 1ffa619 Handle ga_append() and ml_replace() failures


View it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772/before/4537befdded08ad67d4d9ddcdf3707847f630e91/after/1ffa619efa13f4a8982fa817b42ab7a46583f216@github.com>

Jon Parise

unread,
Mar 20, 2026, 4:04:59 PM (6 days ago) Mar 20
to vim/vim, Subscribed

@jparise commented on this pull request.


In src/ex_getln.c:

> +
+	    // Find next match.
+	    nmatch = vim_regexec_multi(&regmatch, 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.Message ID: <vim/vim/pull/19772/review/3983807702@github.com>

Christian Brabandt

unread,
Mar 20, 2026, 6:18:51 PM (6 days ago) Mar 20
to vim/vim, Subscribed
chrisbra left a comment (vim/vim#19772)

anybody, please provide feedback


Reply to this email directly, view it on GitHub.

You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772/c4101231214@github.com>

dkearns

unread,
Mar 20, 2026, 11:27:39 PM (6 days ago) Mar 20
to vim/vim, Subscribed
dkearns left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4102036805@github.com>

h_east

unread,
Mar 21, 2026, 4:32:53 AM (6 days ago) Mar 21
to vim/vim, Subscribed
h-east left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4102841503@github.com>

Jon Parise

unread,
Mar 21, 2026, 8:22:02 AM (6 days ago) Mar 21
to vim/vim, Push

@jparise pushed 1 commit.

  • 09ac9d6 Restrict preview to visible lines


View it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772/before/1ffa619efa13f4a8982fa817b42ab7a46583f216/after/09ac9d647e9b94474000593fff6369fbf482a0ec@github.com>

Jon Parise

unread,
Mar 21, 2026, 8:23:14 AM (6 days ago) Mar 21
to vim/vim, Subscribed

@jparise commented on this pull request.


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, &regmatch);
+    --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.Message ID: <vim/vim/pull/19772/review/3985971852@github.com>

Jon Parise

unread,
Mar 21, 2026, 8:41:13 AM (6 days ago) Mar 21
to vim/vim, Subscribed

@jparise commented on this pull request.


In src/ex_getln.c:

> +					    (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(&regmatch, 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.Message ID: <vim/vim/pull/19772/review/3985989402@github.com>

Jon Parise

unread,
Mar 21, 2026, 8:52:29 AM (6 days ago) Mar 21
to vim/vim, Push

@jparise pushed 3 commits.

  • 8a69779 Add 'inccommand' option for live :substitute preview
  • cec2c2b Handle ga_append() and ml_replace() failures
  • be92eec Restrict preview to visible lines


View it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/pull/19772/before/09ac9d647e9b94474000593fff6369fbf482a0ec/after/be92eecabc2d802c6f768552291a1952cbf3ec40@github.com>

h_east

unread,
Mar 22, 2026, 1:23:54 AM (5 days ago) Mar 22
to vim/vim, Subscribed
h-east left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4105562742@github.com>

Enno

unread,
Mar 22, 2026, 5:12:36 AM (5 days ago) Mar 22
to vim/vim, Subscribed
Konfekt left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4105860145@github.com>

h_east

unread,
Mar 22, 2026, 5:39:56 AM (5 days ago) Mar 22
to vim/vim, Subscribed
h-east left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4105898496@github.com>

Christian Brabandt

unread,
Mar 22, 2026, 1:17:00 PM (5 days ago) Mar 22
to vim/vim, Subscribed
chrisbra left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4106622299@github.com>

Maxim Kim

unread,
Mar 22, 2026, 6:41:53 PM (4 days ago) Mar 22
to vim/vim, Subscribed
habamax left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4107131162@github.com>

dkearns

unread,
Mar 22, 2026, 8:48:52 PM (4 days ago) Mar 22
to vim/vim, Subscribed
dkearns left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4107359821@github.com>

dkearns

unread,
Mar 22, 2026, 8:49:53 PM (4 days ago) Mar 22
to vim/vim, Subscribed
dkearns left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4107362247@github.com>

Christian Brabandt

unread,
Mar 23, 2026, 4:27:29 AM (4 days ago) Mar 23
to vim/vim, Subscribed
chrisbra left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4108817825@github.com>

Jon Parise

unread,
Mar 23, 2026, 7:27:49 PM (3 days ago) Mar 23
to vim/vim, Subscribed
jparise left a comment (vim/vim#19772)

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.Message ID: <vim/vim/pull/19772/c4114370489@github.com>

Jonathan Engel

unread,
Mar 23, 2026, 11:25:12 PM (3 days ago) Mar 23
to vim_dev
I'm just a user, not a developer, but traces.vim is really useful to me when constructing regexes for substitution because I can see in real time the mistakes that make them incorrect (because the highlighted matches disappear, or the substituted strings become incorrect).  This ability seems like a natural part of the editor itself, especially if it is off by default.
Reply all
Reply to author
Forward
0 new messages