Skip to content

Add 'inccommand' option for live :substitute preview#19772

Open
jparise wants to merge 3 commits intovim:masterfrom
jparise:inccommand
Open

Add 'inccommand' option for live :substitute preview#19772
jparise wants to merge 3 commits intovim:masterfrom
jparise:inccommand

Conversation

@jparise
Copy link
Contributor

@jparise jparise commented Mar 20, 2026

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
See also: https://groups.google.com/g/vim_dev/c/FiUC3_mSv_o


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.

@jparise jparise force-pushed the inccommand branch 2 times, most recently from 0a64448 to 4537bef Compare March 20, 2026 15:03
Comment on lines +206 to +213
// 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

return FALSE;
}

first_line = search_first_line == 0 ? 1 : search_first_line;
Copy link
Contributor

@64-bitman 64-bitman Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 substitute preview

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if (state->match != NULL)
{
state->match->mit_hlg_id =
syn_namen2id((char_u *)"Substitute",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

continue;

// Skip multi-line matches.
if (regmatch.endpos[0].lnum > 0)
Copy link
Contributor

@64-bitman 64-bitman Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can get the expression replacement side effects, but what side effects would multi-lime matching have? Neovim supports it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chrisbra
Copy link
Member

anybody, please provide feedback

@dkearns
Copy link
Contributor

dkearns commented Mar 21, 2026

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.

@h-east
Copy link
Member

h-east commented Mar 21, 2026

By the way, Neovim seems to have forgotten the 'inccommand' for :uniq.

(Added:)
Ah, Vim also seems to have forgotten that 'incsearch' supports :uniq.

@jparise
Copy link
Contributor Author

jparise commented Mar 21, 2026

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.

Perhaps something like neovim's :command-preview?

jparise added 3 commits March 21, 2026 08:51
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 (vim#14868, vim#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.

Signed-off-by: Jon Parise <jon@indelible.org>
@h-east
Copy link
Member

h-east commented Mar 22, 2026

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.

@Konfekt
Copy link
Contributor

Konfekt commented Mar 22, 2026

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

@h-east
Copy link
Member

h-east commented Mar 22, 2026

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.

@chrisbra
Copy link
Member

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.

@habamax
Copy link
Contributor

habamax commented Mar 22, 2026

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/.../...

@dkearns
Copy link
Contributor

dkearns commented Mar 23, 2026

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.

@dkearns
Copy link
Contributor

dkearns commented Mar 23, 2026

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 plugins 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.

@chrisbra
Copy link
Member

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.

@jparise
Copy link
Contributor Author

jparise commented Mar 23, 2026

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 like traces.vim already 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: a separate preview buffer, something like neovim's split preview view, or some new ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in Substitution Live Preview Option or Plugin

7 participants