141

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?

ib.
  • 27,830
  • 11
  • 80
  • 100
basteln
  • 2,483
  • 2
  • 19
  • 20
  • 2
    `\vpattern` - 'very magic' pattern: non-alphanumeric characters are interpreted as special regex symbols (no escaping needed) – Carlos Araya Sep 21 '16 at 11:53
  • what is the point to start from current position and wrap around the file instead of using just `%`? i see no reasonable use for that you're going through the whole file anyway. – tymik Feb 22 '18 at 15:07
  • @tymik The purpose was to start searching from the cursor, and go through each result step-by-step. But I haven't used Vim in years now. – basteln Feb 22 '18 at 15:22

6 Answers6

218

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
Peter Rincker
  • 43,539
  • 9
  • 74
  • 101
  • 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 Sep 29 '11 at 14:01
  • 12
    For "the next ten lines": `10:s/pattern/replace/gc` – here Feb 21 '15 at 00:38
  • 14
    even though it's not what the OP was looking for, this was very helpful to me, thanks! – Tobias J Jun 07 '15 at 16:20
61

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

ib.
  • 27,830
  • 11
  • 80
  • 100
  • 1
    The only issue with this solution is that the options last (l) and quit (q) don't stop the second substitute command from running – Steve Vermeulen Feb 08 '13 at 06:03
  • @eventualEntropy See my solution below about prompting for another 'q' press. – q335r49 Mar 18 '14 at 16:25
  • 2
    Don't forget (like I did) to escape the pipe if you're putting this in a key mapping. – jazzabeanie Sep 29 '16 at 23:16
  • 13
    Oh my god, what do all those arbitrary characters even mean. How did you learn that? – rocky raccoon Jun 16 '17 at 18:52
  • @jazzabeanie How did you turn this into a keymapping? I am failing over and over – Tropilio Apr 02 '20 at 16:21
  • @Tropilio: Like so: `:nnoremap R :,$s/BEFORE/AFTER/gc\|1,''-&&`. Make sure to also escape any special characters that might be present in the search pattern (`BEFORE`) and the replacement string (`AFTER`). – ib. Apr 04 '20 at 07:28
  • @ib.yes but, what about the BEFORE and AFTER? I want to be able to edit them every time I call the keymapping, obviously! – Tropilio Apr 05 '20 at 09:17
  • 2
    @Tropilio: Then you can try something like this: `:noremap R :,$s///gc\|1,''-&&`. 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*. – ib. Apr 06 '20 at 05:29
59

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

Rishi
  • 5,869
  • 7
  • 34
  • 45
mb14
  • 22,276
  • 7
  • 60
  • 102
  • 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 Sep 29 '11 at 14:01
  • @basteln: there was typo in the post // did you copy and paste? – sehe Sep 29 '11 at 14:09
  • 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 Sep 29 '11 at 14:10
  • you can use n and & instead. – mb14 Sep 29 '11 at 14:14
  • 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! :) – basteln Sep 29 '11 at 14:48
  • 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. – ib. Sep 30 '11 at 13:26
  • @ib you are right, it doesn't work. Your solution is the shortest – mb14 Sep 30 '11 at 21:22
  • 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. – RastaJedi Aug 11 '16 at 19:25
5

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)
q335r49
  • 608
  • 8
  • 10
  • In my opinion, this approach—inserting additional prompt in between of the two commands proposed in [my answer](http://stackoverflow.com/a/7608016/254635)—adds unnecessary complexity without improving usability. In [original version](http://stackoverflow.com/a/7608016/254635), 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. – ib. Mar 18 '14 at 20:21
  • 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 Mar 18 '14 at 21:53
  • 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. – q335r49 Mar 18 '14 at 22:05
  • 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. Mar 20 '14 at 21:40
  • 1
    In [the original version](http://stackoverflow.com/a/7608016/254635), 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!\`\``. – ib. Mar 20 '14 at 21:45
  • 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 Mar 21 '14 at 00:42
  • And the Ctrl-O approach does not avoid the random jump to the beginning of the file. – q335r49 Mar 21 '14 at 00:46
  • [My solution](http://stackoverflow.com/a/36907615/2103996) is very hacky, but should address all of the concerns, I think. – Greg Hurrell Apr 28 '16 at 07:23
3

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.
ib.
  • 27,830
  • 11
  • 80
  • 100
Greg Hurrell
  • 5,177
  • 23
  • 27
2

I am late to the party, but I relied too often on such late stackoverflow answerrs to not do it. I gathered hints on reddit and stackoverflow, and the best option is to use the \%>...c pattern in the search, which matches only after your cursor.

That said, it also messes up the pattern for the next replacement step, and is hard to type. To counter those effects, a custom function must filter the search pattern afterwards, thus resetting it. See below.

I have contended myself with a mapping that replaces the next occurence and jumps to the following after, and not more (was my goal anyway). I am sure, building on this, a global substitution can be worked out. Keep in mind when working on a solution aiming at something like :%s/.../.../g that the pattern below filters out matches in all lines left to the cursor position — but is cleaned up after the single substitution completes, so it loses that effect directly after, jumps to the next match and thus is able to run through all matches one by one.

fun! g:CleanColFromPattern(prevPattern)
    return substitute(a:prevPattern, '\V\^\\%>\[0-9]\+c', '', '')
endf
nmap <F3>n m`:s/\%><C-r>=col(".")-1<CR>c<C-.r>=g:CleanColFromPattern(getreg("/"))<CR>/~/&<CR>:call setreg("/", g:CleanColFromPattern(getreg("/")))<CR>``n
ib.
  • 27,830
  • 11
  • 80
  • 100
simlei
  • 155
  • 2
  • 12
  • 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. – basteln Nov 04 '18 at 17:13