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?
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.
basteln
Updated on November 07, 2020Comments
-
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 over 12 yearsThanks, 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 over 12 yearsThanks, 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 over 12 years@basteln: there was typo in the post // did you copy and paste?
-
mb14 over 12 yearsSo 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 over 12 yearsyou can use n and & instead.
-
basteln over 12 yearshmm, 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. over 12 yearsThe 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 commandg&
. The command itself and comments to it misinform a reader. -
mb14 over 12 years@ib you are right, it doesn't work. Your solution is the shortest
-
Steve Vermeulen about 11 yearsThe only issue with this solution is that the options last (l) and quit (q) don't stop the second substitute command from running
-
q335r49 about 10 years@eventualEntropy See my solution below about prompting for another 'q' press.
-
ib. about 10 yearsIn 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 pressq
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 about 10 yearsDude... 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 about 10 yearsThe ideal method is, of course
yyyq
, continue editing, with no disorienting jumps. Butyyyqq
is almost as good, if you consider a double tap pretty much a single tap in terms of ease of use. -
ib. about 10 yearsNeither 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 firstq
was pressed). -
ib. about 10 yearsIn 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 about 10 yearsYes, 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 about 10 yearsAnd the Ctrl-O approach does not avoid the random jump to the beginning of the file.
-
here about 9 yearsFor "the next ten lines":
10:s/pattern/replace/gc
-
Tobias J almost 9 yearseven though it's not what the OP was looking for, this was very helpful to me, thanks!
-
Greg Hurrell about 8 yearsMy solution is very hacky, but should address all of the concerns, I think.
-
Greg Hurrell about 8 yearsI extracted this into a tiny plug-in and put it on GitHub here.
-
RastaJedi over 7 yearsDid you mean '
%
is a shortcut for1,$
(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 over 7 yearsDon't forget (like I did) to escape the pipe if you're putting this in a key mapping.
-
rocky raccoon almost 7 yearsOh my god, what do all those arbitrary characters even mean. How did you learn that?
-
basteln over 5 yearsThanks, 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 about 4 years@jazzabeanie How did you turn this into a keymapping? I am failing over and over
-
Tropilio about 4 yearsJust installed your plugin, it works like charm!! Thank you very much for making this!
-
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 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. 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.