# Multiple Cursors in 500 bytes of Vimscript!

Posted on January 19, 2017
Tags: programming, vim

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

1. Position the cursor anywhere in the word poisson;
2. hit cn, type exponential, then go back to Normal mode;
3. hit . n-1 times, where n is the number of replacements.
4. Do the same in changing int to double.

To do this in Sublime Text, we would

1. Position the cursor on poisson;
2. hit Ctrl-D n times, where n is the number of replacements;
3. type exponential.
4. Do the same in changing int to double.

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

1. Select pet in visual mode.
2. Hit cn to start the replacement process.
3. Enter in cat, then hit . an appropriate number of times.
4. 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> [email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */<CR>q:<C-u>let @z=strpart(@z,0,strlen(@z)-1)<CR>[email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */
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:

1. Position the cursor over a word; alternatively, make a selection.
2. Hit cq to start recording the macro.
3. Once you are done with the macro, go back to normal mode.
4. 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> [email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */<CR>q:<C-u>let @z=strpart(@z,0,strlen(@z)-1)<CR>[email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */
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:

1. cn has a built-in meaning to change up to the next search (combination of operation c and the motion n). However, I have never found a use for cn since I’m never sure, without checking, where the next search is. If you do use cn I suggest the alternative vnc.

2. 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:

1. Search for the word under the cursor (*) and go the next match.
2. Go back to the previous match using  (see :help ).
3. Change the search matches using c together with gn. When an operator is pending gn is equivalent to (roughly) the [current or next] match depending on whether the cursor is located on a match (see :help gn).
4. 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):

1. y: yank the selection. It is placed in the @" register.
2. /\V<C-r>=escape(@",'/')<CR> starts a literal (“very nomagic”) search with the selection as the search. Here, the escape() function is called to escape occurrences of / in the selection with \/.
3. The final <CR> starts the search.

## Playing a macro

The commands

function! SetupCR()
nnoremap <Enter> :nnoremap <lt>Enter> [email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */<CR>q:<C-u>let @z=strpart(@z,0,strlen(@z)-1)<CR>[email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */
endfunction

nnoremap cq :call SetupCR()<CR>*qz

maps cq to do the following:

1. 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
2. Searches for the word under the cursor and jump back, i.e., *.
3. 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!!