0

I'm a new Vim user, so please bear with me.

A while ago I wanted to be able to replace a pattern with user input, and I stumbled upon this question on StackOverflow, and its answer worked beautifully.

Here's what it suggested:

%s/value_one/\=input('replace <'.submatch(0).'> with: ')/g

So, if I have 3 occurrences of some pattern in a line, what I do is: I type what I want to replace it with, hit enter, and then press the up arrow key, then enter, so I don't have to type it again. I mapped it, so it's all faster.

So, as the title says, is there a way to apply that first user input to all the occurrences on the line? Meaning, is there a way to skip the "press the up arrow key" part of the process I described before?

I looked at :h :s= and :h input(). And I can't really find a way to do it. The command suggested works for me, but I can't really totally understand it.

Whoever replies, I beg you to explain the answer.

Thanks in advance.

Sz.

5 Answers 5

1

If you want to be asked only once for every line on how to replace all occurrences of value_one on this line, you can define a helper function which remembers the last line and only asks for new input if the last line and the current line differ. If the current match is still on the same line, the last input is re-used:

function! InputOnceForEveryLine(prompt)
    if line('.') == get(b:, 'last_matched_line', -1)
        return histget('input')
    endif

    let b:last_matched_line = line('.')
    return input(a:prompt)
endfunction

augroup InputOnce
    au!
    au CmdlineEnter : unlet! b:last_matched_line
augroup END

The augroup block makes sure that b:last_matched_line is invalidated before InputOnceForEveryLine() might be used again. Otherwise a :substitute that accidentally matches the first time in the same line where the previous :substitute matched its last time would not ask for a replacement text.

Then use this function instead of input():

:%s/value_one/\=InputOnceForEveryLine('replace <'.submatch(0).'> with: ')/g
7
  • Better invalidate b:last_matched_line before or after calling :s.
    – Friedrich
    May 16, 2023 at 14:24
  • @Friedrich Thanks for the hint. I have made an edit to the answer. May 16, 2023 at 14:43
  • It is the exact answer to the question. However, I think the OP wants to use plain old :s ... they just don't know it :)
    – Friedrich
    May 16, 2023 at 17:27
  • @Friedrich Yeah, maybe. :-) But with this solution the OP does not have to search for the next matching line and modify a :s/.../.../g from the command-line history, but with %s/.../.../g they are automatically sent to the next matching line and the replacement texts are stored in the (in this case) much cleaner input history. One disadvantage with my solution: During tests I was not able to completely update the display with the already done substitutions -- even with :redraw. :-( May 16, 2023 at 19:55
  • This is exactly it! Thanks @JürgenKrämer it is very much appreciated. Perfect.
    – Szpilman
    May 16, 2023 at 20:11
3

For a completely different approach, try /pattern (or * if it's a word), then cgn to make the first change (hit Escape when done) and . to repeat the change on the next occurrence of the pattern. You can also press n/N to navigate as normal, and use cgn again to make a different change (which also changes what . will do).

2
  • 2
    Upvoted. It's fast and flexible and works out of the box.
    – Friedrich
    May 17, 2023 at 21:25
  • 1
    I personally can't get along with this idiom because I don't like the way the . operates on the next match. But @romainl posted a lovely set of mappings on reddit that improves the gn workflow by using select mode. I copied them here, (under Using Select Mode).
    – Rich
    May 18, 2023 at 9:15
2

You asked for an explanation. Let's chop your command up and look at it bit by bit:

%s/value_one/\=input('replace <'.submatch(0).'> with: ')/g
% <- in the whole buffer
 s <- substitute (see :help :substitute)
  /value_one <- the search pattern "value one"
            /\= <- with the result of an expression (see :help :s\=)
               input <- prompt the user for input (see :help input())
                    ('replace <'.submatch(0).'> with: ') <- this is what the user sees.
  submatch(0) is whatever was matched ("value_one" in our case) and the dots concatenate the strings.
                                                        /g <- do this multiple times per line (see :h :s_g)

And it does exactly what one would expect: matches every "value_one" and prompts with what to replace. Every time.

It's not obvious why you would need this. If you simply want to "replace all", the plain :substitute command as mentioned in Aviik's answer is the way to go. It's actually easier to write :%s/value_one/new_value/g than to prompt for user input.

If you want to replace "value_one" with something several times on a line, the way to go is to do substitution on the current line only, i.e. simply by leaving out the % range:

:s/value_one/replacement_everywhere_on_this_line/g

The proper use of ranges is another key to mastering :substitute (and many other commands that use ranges). You can choose to operate on the whole buffer, the current line, a visual selection or arbitrary lines given as absolute or relative line numbers etc. See :help :range.

Coincidentally, the up arrow is working in the command line, so you can quickly access the last command and change it.

What's more, Vim will remember the last search pattern. To replace "value_one" with "foo" on one line, but with "bar" on another line, you can use the following pair of commands:

:s/value_one/foo/
:s//bar/

It is also possible to write a function that queries the user for a replacement string and then constructs a :substitute command from it. There's just no point in doing so. That would re-invent behavior already in the editor.

I suspect the question comes from a misunderstanding how replacing text is commonly done in Vim. Most editors have these popup windows to "search and replace (all)". Vim does not have this and you don't need it. In Vim, you specify a :substitute command that defines the replacement. It's precise and powerful - it does not have a pretty graphical user interface.

3
  • 1
    "It's not obvious why you would need this" <<< The question that the OP got the code from was about how to quickly replace all the matches of a pattern in a file with different replacements. As you point out, it doesn't make much sense to use this if you want to use the same replacement text every time.
    – Rich
    May 16, 2023 at 15:11
  • The input is different in every case, that's why I didn't use the "basic" :s. Thank you, though, for explaining what's inside \=input().
    – Szpilman
    May 16, 2023 at 20:13
  • @Szpilman Sorry! I re-read your question more carefully and get it now.
    – Rich
    May 16, 2023 at 21:28
1

Here's a solution that uses :global to request input once per matching line and then construct a :substitute command for that line:

:g/value_one/let r = input('replace ' .. @/ .. ' with: ') | exe 's//' .. r .. '/g'

This differs in behaviour from Jürgen Krämer's solution in a couple of ways, as he points out in the comments:

Firstly, note that :global doesn't offer something like submatch(), so you can't include the matched text in your input() prompt. Instead, we use the search pattern via the search register.

The disadvantage of this is that you never get to see the matched text that will be replaced. On the other hand the matched text may not be the same for all matches on the line: displaying only the first will sometimes be misleading.1

Secondly, note that this passes your input directly into the :substitute command, so you can use all the usual replacement features. This might be useful, but it's different to how your original command behaved: for something closer to that, you should change the the :execute command string to escape slashes/backslashes:

exe 's//' .. escape(r, '\/') .. '/g'

1: e.g. consider how it will behave with a search pattern of \d matching a line containing 1 2 3

2
  • 1
    This will always prompt with the search pattern, not the actually found text. And the replacement text should probably be escaped for the right-hand side of the constructed :s//.../ command (at least slashes and backslashes). May 17, 2023 at 5:39
  • @JürgenKrämer Good points. I'll update.
    – Rich
    May 17, 2023 at 8:59
0

:%s/old/new/ to replace all occurrences of old with new.

:%s/old/new/gc This will prompt you to confirm each replacement with a 'y' or 'n' response.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.