Multiple Cursors in 500 bytes of Vimscript!
Table of contents
In this post we’ll introduce a handful of keybindings that implement (and surpass!) the multiple cursors (MC) functionality in Sublime Text (ST). While the built-in commands :s
and :g
cover far more ground, MC have the advantage of immediate visual feedback throughout the editing operation—this is the same reason why one may prefer the longer sequence Vjy
to copy two lines instead of the faster yj
.
The full source code is found in the conclusion near the bottom of the post.
Changing a word
Mappings used:
nnoremap cn *``cgn
The simplest and most common case of MC deals with changing a word to another word in several locations.
We would like to change poisson
to exponential
and int
to double
in the snippet above. With the Vimscript mapping above , we can accomplish the same in Vim by
- Position the cursor anywhere in the word
poisson
; - hit
cn
, typeexponential
, then go back to Normal mode; - hit
.
n-1 times, where n is the number of replacements. - Do the same in changing
int
todouble
.
To do this in Sublime Text, we would
- Position the cursor on
poisson
; - hit
Ctrl-D
n times, where n is the number of replacements; - type
exponential
. - Do the same in changing
int
todouble
.
The process is comparable: both takes approximately n keystrokes for n replacements.
Changing a selection
Mappings used:
let g:mc = "y/\\V\<C-r>=escape(@\", '/')\<CR>\<CR>"
vnoremap <expr> cn g:mc . "``cgn"
Sometimes, the target to change is not a whole word. In these cases, we would first manually select the word (using appropriate motions and text objects) and then issue cn
.
In the screencast below, we want to change occurrences of pet
into cat
, except for the generic pet on the third line. To do this, we
- Select
pet
in visual mode. - Hit
cn
to start the replacement process. - Enter in
cat
, then hit.
an appropriate number of times. - Use
n
to skip over the matches in the third line.
Note that step #3 is impossible to do in Sublime Text! Thus, this is one place where the multiple cursors in vim surpasses its Subliem Text counterpart.
Playing a macro on searches
Mappings used:
let g:mc = "y/\\V\<C-r>=escape(@\", '/')\<CR>\<CR>"
function! SetupCR()
nnoremap <Enter> :nnoremap <lt>Enter> n@z<CR>q:<C-u>let @z=strpart(@z,0,strlen(@z)-1)<CR>n@z
endfunction
nnoremap cq :call SetupCR()<CR>*``qz
vnoremap <expr> cq ":\<C-u>call SetupCR()\<CR>" . "gv" . g:mc . "``qz"
In the most advanced usage, we want to execute a macro over a search match, instead of a usual replacement. To this end, the above code introduces the keybindings cq
; it works on the current word in normal mode and works on the selection in visual mode.
Since macros can’t be replayed with .
, we create a <Enter>
mapping instead; we could have used @@
but using <Enter>
saves one keystroke, which matters if we are replaying the macro over a large number of lines. Here are the steps:
- Position the cursor over a word; alternatively, make a selection.
- Hit
cq
to start recording the macro. - Once you are done with the macro, go back to normal mode.
- Hit
Enter
to repeat the macro over search matches.
Macros allow the full editing power of vim commands to be used with multiple cursors. Note that we used the command ytS
in the screencast to copy the country name in CountryStock
, which is not easily done in Sublime Text.
Full Source Code for Copy and Paste
The full source code is
let g:mc = "y/\\V\<C-r>=escape(@\", '/')\<CR>\<CR>"
nnoremap cn *``cgn
nnoremap cN *``cgN
vnoremap <expr> cn g:mc . "``cgn"
vnoremap <expr> cN g:mc . "``cgN"
function! SetupCR()
nnoremap <Enter> :nnoremap <lt>Enter> n@z<CR>q:<C-u>let @z=strpart(@z,0,strlen(@z)-1)<CR>n@z
endfunction
nnoremap cq :call SetupCR()<CR>*``qz
nnoremap cQ :call SetupCR()<CR>#``qz
vnoremap <expr> cq ":\<C-u>call SetupCR()\<CR>" . "gv" . g:mc . "``qz"
vnoremap <expr> cQ ":\<C-u>call SetupCR()\<CR>" . "gv" . substitute(g:mc, '/', '?', 'g') . "``qz"
Here I also added a capital letter of the command which iterates through matches in reverse.
If you found this post helpful, please consider making a donation; I am a PhD student in need of funding :-). Thank you for your support!!
Final Thoughts
These commands implement and surpass (in my opinion) the multiple cursor feature in Sublime Text, and weights just a bit over 500 bytes. It is not perfect, but good enough for everyday use and in some cases surpass the functionality in ST.
Note I did not include a feature which executes a macro or replacement on consecutive lines—this corresponds to Sublime Text’s “add cursor below” feature. But this can be more easily accomplished just by using a macro: 0qq <sequence of commands here> <position cursor in the next line>q
then replaying the macro q
.
Finally, here are some caveats to keep in mind:
cn
has a built-in meaning to change up to the next search (combination of operationc
and the motionn
). However, I have never found a use forcn
since I’m never sure, without checking, where the next search is. If you do usecn
I suggest the alternativevnc
.The
<Enter>
mappings above replace the register@z
. If you regularly use this register (I don’t), change the code above to adapt to your needs.
Appendix — How It Works
Changing a word
The mapping nnoremap cn *``cgn
maps cn
to execute the keystrokes *``cgn
. It does the following:
- Search for the word under the cursor (
*
) and go the next match. - Go back to the previous match using
``
(see:help ``
). - Change the search matches using
c
together withgn
. When an operator is pendinggn
is equivalent to (roughly)the [current or next] match
depending on whether the cursor is located on a match (see:help gn
). - Since
cgn
is repeatable with.
, this is good enough.
Changing a visual selection
The mapping
let g:mc = "y/\\V\<C-r>=escape(@\", '/')\<CR>\<CR>"
vnoremap <expr> cn g:mc . "``cgn"
maps cn
in visual mode to first search for the selection, then doing *``cgn
. The latter half of the command is explained above, so we only need to look at the key sequence in g:mc
. It performs the following (I removed the extraneous backslash escape characters for clarity):
y
: yank the selection. It is placed in the@"
register./\V<C-r>=escape(@",'/')<CR>
starts a literal (“very nomagic”) search with the selection as the search. Here, theescape()
function is called to escape occurrences of/
in the selection with\/
.- The final
<CR>
starts the search.
Playing a macro
The commands
function! SetupCR()
nnoremap <Enter> :nnoremap <lt>Enter> n@z<CR>q:<C-u>let @z=strpart(@z,0,strlen(@z)-1)<CR>n@z
endfunction
nnoremap cq :call SetupCR()<CR>*``qz
maps cq
to do the following:
- Calls
SetupCR()
which maps<Enter>
(equivalent to<CR>
, but I’m using<Enter>
for clarity) to do the following:- on the first invocation of
<Enter>
, stop the recording, go the next match, and execute register@z
. - on subsequent invocations of
<Enter>
, go to the next match and execute register@z
- on the first invocation of
- Searches for the word under the cursor and jump back, i.e.,
*``
. - Starts recording the macro
@z
.
If you found this post useful, please consider making a donation; I am a PhD student in need of funding :-). Thank you for your support!!