How to search and replace globally, starting from the cursor position and wrapping around the end of file, in a single command invocation in Vim?

68,022

Solution 1

1. It is not hard to achieve the behavior in question using a two-step substitution:

:,$s/BEFORE/AFTER/gc|1,''-&&

First, the substitution command is run for each line starting from the current one until the end of file:

,$s/BEFORE/AFTER/gc

Then, that :substitute command is repeated with the same search pattern, replacement string, and flags, using the :& command (see :help :&):

1,''-&&

The latter, however, performs the substitution on the range of lines from the first line of the file to the line where the previous context mark has been set, minus one. Since the first :substitute command stores the cursor position before starting actual replacements, the line addressed by '' is the line that was the current one before that substitution command was run. (The '' address refers to the ' pseudo-mark; see :help :range and :help '' for details.)

Note that the second command (after the | command separator—see :help :bar) does not require any change when the pattern or flags are altered in the first one.

2. To save some typing, in order to bring up the skeleton of the above substitution command in the command line, one can define a Normal-mode mapping, like so:

:noremap <leader>cs :,$s///gc\|1,''-&&<c-b><right><right><right><right>

The trailing <c-b><right><right><right><right> part is necessary to move the cursor to the beginning of the command line (<c-b>) and then four characters to the right (<right> × 4), thus putting it between the first two slash signs, ready for the user to start typing the search pattern. Once the desired pattern and the replacement are ready, the resulting command can be run by pressing Enter.

(One might consider having // instead of /// in the mapping above, if one prefers to type the pattern, then type the separating slash oneself, followed by the replacement string, instead of using the right arrow to move the cursor over an already present separating slash starting the replacement part.)

Solution 2

You are already using a range, %, which is short hand for 1,$ meaning the entire file. To go from the current line to the end you use .,$. The period means current line and $ means the last line. So the command would be:

:.,$s/\vBEFORE/AFTER/gc

But the . or current line can be assumed therefore can be removed:

:,$s/\vBEFORE/AFTER/gc

For more help see

:h range

Solution 3

% is a shortcut for 1,$
( Vim help => :help :% equal to 1,$ (the entire file).)

. is the cursor postion so you can do

:.,$s/\vBEFORE/AFTER/gc

To replace from the beginning of the document till the cursor

:1,.s/\vBEFORE/AFTER/gc

etc

I strongly suggest you read the manual about range :help range as pretty much all commands work with a range.

Solution 4

I've FINALLY come up with a solution to the fact that quitting the search wraps around to the beginning of the file without writing an enormous function...

You wouldn't believe how long it took me to come up with this. Simply add a prompt whether to wrap: if the user presses q again, don't wrap. So basically, quit search by tapping qq instead of q! (And if you do want to wrap, just type y.)

:,$s/BEFORE/AFTER/gce|echo 'Continue at beginning of file? (y/q)'|if getchar()!=113|1,''-&&|en

I actually have this mapped to a hotkey. So, for example, if you want to search and replace every word under the cursor, starting from the current position, with q*:

exe 'nno q* :,$s/\<<c-r>=expand("<cword>")<cr>\>//gce\|echo "Continue at beginning of file? (y/q)"\|if getchar()==121\|1,''''-&&\|en'.repeat('<left>',77)

Solution 5

Here’s something very rough that addresses the concerns about wrapping the search around with the two-step approach (:,$s/BEFORE/AFTER/gc|1,''-&&) or with an intermediate “Continue at beginning of file?”-prompt approach:

" Define a mapping that calls a command.
nnoremap <Leader>e :Substitute/\v<<C-R>=expand('<cword>')<CR>>//<Left>

" And that command calls a script-local function.
command! -nargs=1 Substitute call s:Substitute(<q-args>)

function! s:Substitute(patterns)
  if getregtype('s') != ''
    let l:register = getreg('s')
  endif
  normal! qs
  redir => l:replacements
  try
    execute ',$s' . a:patterns . 'gce#'
  catch /^Vim:Interrupt$/
    return
  finally
    normal! q
    let l:transcript = getreg('s')
    if exists('l:register')
      call setreg('s', l:register)
    endif
  endtry
  redir END

  if len(l:replacements) > 0
    " At least one instance of pattern was found.
    let l:last = strpart(l:transcript, len(l:transcript) - 1)
    " Note: type the literal <Esc> (^[) here with <C-v><Esc>:
    if l:last ==# 'l' || l:last ==# 'q' || l:last ==# '^['
      " User bailed.
      return
    endif
  endif

  " Loop around to top of file and continue.
  " Avoid unwanted "Backwards range given, OK to swap (y/n)?" messages.
  if line("''") > 1
    1,''-&&"
  endif
endfunction

This function uses a couple of hacks to check whether we should wrap around to the top:

  • No wrapping if user pressed L, Q, or Esc, any of which indicate a desire to abort.
  • Detect that final key press by recording a macro into the s register and inspecting last character of it.
  • Avoid overwriting an existing macro by saving/restoring the s register.
  • If you are already recording a macro when using the command, all bets are off.
  • Tries to do the right thing with interrupts.
  • Avoids “backwards range” warnings with an explicit guard.
Share:
68,022
basteln
Author by

basteln

Updated on November 07, 2020

Comments

  • basteln
    basteln over 3 years

    When I search with the / Normal-mode command:

    /\vSEARCHTERM
    

    Vim starts the search from the cursor position and continues downwards, wrapping around to the top. However, when I search and replace using the :substitute command:

    :%s/\vBEFORE/AFTER/gc
    

    Vim starts at the top of the file, instead.

    Is there a way to make Vim perform search and replace starting from the cursor position and wrapping around to the top once it reaches the end?

  • basteln
    basteln over 12 years
    Thanks, I already knew about that. I want the search & replace to wrap around once it reaches the end of the document. With :.,$s it doesn't do that, it just says "Pattern not found".
  • basteln
    basteln over 12 years
    Thanks, I already knew about that. I want the search & replace to wrap around once it reaches the end of the document. With :.,$s it doesn't do that, it just says "Pattern not found".
  • sehe
    sehe over 12 years
    @basteln: there was typo in the post // did you copy and paste?
  • mb14
    mb14 over 12 years
    So you want to replace EVERYTHING but as you want to ask confirmation you want to start from the current position. Just do it twice then.
  • mb14
    mb14 over 12 years
    you can use n and & instead.
  • basteln
    basteln over 12 years
    hmm, I guess n and & is the best solution, though it's not exactly how it should be (with the yes/no prompt on every match). But definitely good enough, thanks! :)
  • ib.
    ib. over 12 years
    The last command you propose does not work as you probably expect it to. The g& is interpreted as an Ex command here, hence it means the same as :g// which does not repeat the last substitution, it rather prints all the lines matching the last search pattern instead. Perhaps, here you meant a Normal mode command g&. The command itself and comments to it misinform a reader.
  • mb14
    mb14 over 12 years
    @ib you are right, it doesn't work. Your solution is the shortest
  • Steve Vermeulen
    Steve Vermeulen about 11 years
    The only issue with this solution is that the options last (l) and quit (q) don't stop the second substitute command from running
  • q335r49
    q335r49 about 10 years
    @eventualEntropy See my solution below about prompting for another 'q' press.
  • ib.
    ib. about 10 years
    In my opinion, this approach—inserting additional prompt in between of the two commands proposed in my answer—adds unnecessary complexity without improving usability. In original version, if you quit the first substitution command (with q, l, or Esc) and do not want to continue from the top, you already can press q or Esc again to quit the second command right away. That is, the proposed addition of the prompt seems to effectively duplicate the already present functionality.
  • q335r49
    q335r49 about 10 years
    Dude... have you TRIED your approach? Let's say I want to replace the next 3 occurrences of "let" with "unlet", and then -- this is the key-- continue editing the paragraph. Your approach "Press y,y,y, now press q. Now you start searching at the first line of the paragraph. Press q again to quit. Now go back to where you were before, to continue editing. Ok, g;. So, basically: yyyqq... become confused... g; (or u^R) My method: yyyqq
  • q335r49
    q335r49 about 10 years
    The ideal method is, of course yyyq, continue editing, with no disorienting jumps. But yyyqq is almost as good, if you consider a double tap pretty much a single tap in terms of ease of use.
  • ib.
    ib. about 10 years
    Neither the original command nor its version with a prompt returns the cursor to its prior position in that scenario. The former leaves the user at the first occurrence of the pattern from the top (where it was at the moment of pressing q the second time). The latter leaves the cursor at the last occurrence that was replaced (just before the first q was pressed).
  • ib.
    ib. about 10 years
    In the original version, if it is preferable to automatically return the cursor to its prior position (without the user typing Ctrl + O), one can just append the norm!`` command: :,$s/BEFORE/AFTER/gc|1,''-&&|norm!``.
  • q335r49
    q335r49 about 10 years
    Yes, that is precisely the intended behavior -- to leave it at the last replacement. Reason: this is the behavior of every other text editor out there, and not just the microsoft ones. Wordpad, notepad, notepad2, notepad++, TED, etc etc. I don't consider this conversation too productive, so I'll compromise: I aknowledge that there is more than one way to skin a cat, ie, addresses the original "annoyance", which has been bothering me for about a year.
  • q335r49
    q335r49 about 10 years
    And the Ctrl-O approach does not avoid the random jump to the beginning of the file.
  • here
    here about 9 years
    For "the next ten lines": 10:s/pattern/replace/gc
  • Tobias J
    Tobias J almost 9 years
    even though it's not what the OP was looking for, this was very helpful to me, thanks!
  • Greg Hurrell
    Greg Hurrell about 8 years
    My solution is very hacky, but should address all of the concerns, I think.
  • Greg Hurrell
    Greg Hurrell about 8 years
    I extracted this into a tiny plug-in and put it on GitHub here.
  • RastaJedi
    RastaJedi over 7 years
    Did you mean '% is a shortcut for 1,$ (beginning to end).' (i.e., s/start/end)? Also, you might want to add a colon at the end of your 'To replace from the beginning of the document till the cursor' line, because even though it begins with a capital letter, it still is kind of ambiguous because at first I thought it was referring to the line above it, so it might help to clarify that it's for the line below it a little bit better.
  • jazzabeanie
    jazzabeanie over 7 years
    Don't forget (like I did) to escape the pipe if you're putting this in a key mapping.
  • rocky raccoon
    rocky raccoon almost 7 years
    Oh my god, what do all those arbitrary characters even mean. How did you learn that?
  • basteln
    basteln over 5 years
    Thanks, I agree that a late answer is often helpful. I personally don't use Vim anymore since a long time (for reasons such as this), but I'm sure a lot of other people will find this helpful.
  • Tropilio
    Tropilio about 4 years
    @jazzabeanie How did you turn this into a keymapping? I am failing over and over
  • Tropilio
    Tropilio about 4 years
    Just installed your plugin, it works like charm!! Thank you very much for making this!
  • ib.
    ib. about 4 years
    @Tropilio: Like so: :nnoremap <leader>R :,$s/BEFORE/AFTER/gc\|1,''-&&<cr>. Make sure to also escape any special characters that might be present in the search pattern (BEFORE) and the replacement string (AFTER).
  • Tropilio
    Tropilio about 4 years
    @ib.yes but, what about the BEFORE and AFTER? I want to be able to edit them every time I call the keymapping, obviously!
  • ib.
    ib. about 4 years
    @Tropilio: Then you can try something like this: :noremap <leader>R :,$s///gc\|1,''-&&<c-b><right><right><right><right>. It puts :,$s///gc|1,''-&& into the command line with the cursor between the first two slash signs. Once you type the desired pattern and replacement, then you can run the resulting command by pressing Enter.