DEV Community

Heiker
Heiker

Posted on • Edited on • Originally published at vonheikemen.github.io

How to survive without multiple cursors in vim

Pueden leer la versión en español aquí.

Yes, multiple cursors are magical. They are convenient, easy to use and every modern editor has them. Now vim on the other hand doesn't have this feature. It's fine. We can be happy without them. Well... I can and I'm going to tell you how.

We'll go throught a few scenarios where multiple cursors can be useful and I'll tell you what alternatives we have in vim.

Replace word under the cursor

In vim we begin this process by searching the word under the cursor, for this we press the * key. Then we press the sequence cgn to replace the next match. If we want to repeat this action we press the . key. If we want to ignore a match we move to the next with n.

We can make this process a lot more convenient by making a keybinding.

nnoremap <leader>j *``cgn
Enter fullscreen mode Exit fullscreen mode

With this we can use the leader key + j to replace the word under the cursor. We can navigate to other matches with n or N, then use the . key when we want to replace the text.

See in asciinema.

Rename a variable

Maybe the thing we want to change is a variable in our code, in this case we only need to change the valid references. Things get complicated here. Since vim isn't an IDE this kind of features are not available out of the box. But it doesn't mean is impossible, we can still do it, there are plugins that allow us to use LSP servers. It just so happens that rename variables is one the things an LSP server can do.

I use neovim btw, not vim. I just need something like this in my config.

lua require('lspconfig').ts_ls.setup({})

nnoremap <F2> <cmd>lua vim.lsp.buf.rename()<cr>
Enter fullscreen mode Exit fullscreen mode

Here I'm using nvim-lspconfig to configure tsserver. Then I create the keybinding <F2> to rename the variable under the cursor.

See in asciinema.

If you use vim you can try out one these plugins:

Replace a selection

So maybe the thing we want to change is not a word, maybe is a sentence or an html attribute. For this we don't have a built-in tool, we need to implement something ourselves. So let's do that.

First thing we should do is add the current selection to the "search register".

let @/=escape(@", '/')
Enter fullscreen mode Exit fullscreen mode

Here we take the text inside the " register, where our selection is stored, and put it in the / register.

The next step would be to delete the selection and enter insert mode. We use this sequence.

"_cgn
Enter fullscreen mode Exit fullscreen mode

With "_ we tell vim that our next operation should store text in the _ register. With cgn we replace the closest match to our search.

If we put the pieces together in a keybinding, we get this.

xnoremap <leader>j y<cmd>let @/=escape(@", '/')<cr>"_cgn
Enter fullscreen mode Exit fullscreen mode

But this command doesn't handle newlines. It won't work with selection with multiple lines. But we can handle that.

xnoremap <leader>j y<cmd>substitute(escape(@", '/'), '\n', '\\n', 'g')<cr>"_cgn
Enter fullscreen mode Exit fullscreen mode

Now here we use the substitute function to replace the newline character with \n, this way our search term will always be one line.

How do we use this? Same way we did with the previous keybinding <leader>j in the section "Replace word under the cursor". But here we must first enter visual mode and select something. Everything else works the same, if we want to replace we use the . key, then we move to the next match with n.

Add text to the beginning of a list

Let's say we have a list of words and we want to convert them to an ordered list in markdown.

Want to turn this.

volar
html
cssls
Enter fullscreen mode Exit fullscreen mode

Into this.

1. volar
1. html
1. cssls
Enter fullscreen mode Exit fullscreen mode

In vim we have a mode called Visual Block, when in this mode we can add text to each line selected if we go to insert mode using I or A. After you added the text and exit insert mode vim will repeat the action on every line.

Let's go step by step how to use this feature.

See in asciinema.

  1. We go to the first character in the line.
  2. Press Ctrl + v to enter visual block mode.
  3. Select the lines we want to change.
  4. Press I to place the cursor at the beginning of the selection.
  5. Add the text.
  6. Press Esc.

Append to the end of a list

We can do that too. The steps are almost identical to the previous section, the only difference is we need to extend the selection until the end of the line.

Let's add something to the previous example.

Okay, we have our ordered list but now we want to append (is supported) to the end of each item.

See in asciinema.

  1. We go to the first character in the line.
  2. Press Ctrl + v to enter visual block mode.
  3. Select the lines we want to change.
  4. Expand the selection to the end of the line using $.
  5. Press A to place the cursor at the end of the selection.
  6. Add the text.
  7. Press Esc.

Repeat movements

Visual block mode can be useful but is very limited. We can only add text in one place. What do we do in more complex scenarios? We use macros. A macro is a piece of text that describes a sequence of keypresses. We can "record" a macro and repeat the sequence as many times as we want.

How do we use macros? We need to pick a register so the first step is to press q followed by a letter. Then we go and do whatever actions we want. We stop recording the macro by pressing q again. To repeat these actions we press @ followed by the register we chose in the first step.

Example time.

We have this list.

volar
html
cssls
eslint
Enter fullscreen mode Exit fullscreen mode

And we want to turn it into an ordered list of links.

1. [volar](http://localhost/how-to-configure-volar-lsp)
1. [html](http://localhost/how-to-configure-html-lsp)
1. [cssls](http://localhost/how-to-configure-cssls-lsp)
1. [eslint](http://localhost/how-to-configure-eslint-lsp)
Enter fullscreen mode Exit fullscreen mode

Notice here we need to add text to the beginning and the end of the list. Additionally, we need to copy the item in the middle of the link.

What do we do? We record a macro, modify the first item then repeat the macro to convert the rest of the list. These are the steps.

  1. Record the macro in the register i. Press qi.
  2. Modify the first item.
  3. We stop recording the macro by pressing q again.
  4. We repeat the macro three times using 3@i.

See in asciinema.

When we apply a macro using a count we need to consider the position of the cursor. In this particular case I begin the macro by pressing 0, to make sure the cursor is at the beginning of the line. Then at the very end of the macro I press j, so the last movement can place the cursor in the next line.

Apply macro in specific lines

Another interesting way to apply a macro is by using the g command. With it we can begin a search and then execute a command in each line there is a match. In our case we want to apply a macro, we can do that with the command normal @i (where i can be any register).

Say we want to look for every line with the word vim then apply a macro. We do this.

:g/vim/normal @i
Enter fullscreen mode Exit fullscreen mode

Now, you might want to inspect the result of the search before doing anything you'll regret. If you omit the last section with the command then :g will just print the lines.

:g/vim/
Enter fullscreen mode Exit fullscreen mode

If everything looks okay then add the normal @i bit.

Apply a macro in a selection

We don't have to use the g command. The normal commands supports ranges, this means we can select any amount of lines then execute this.

'<,'>normal @i
Enter fullscreen mode Exit fullscreen mode

Note: Don't worry about writing '<,'>, vim will add that for you when you go from visual mode to command mode.

That command will execute the macro in each line of the selection. Keep in mind the cursor will be placed at the beginning of the line automatically.

Search selection and apply macro

Yet another alternative to the g command. Because maybe we don't want to make a regular expresion to search. Most of the time I just want to select something, search it, then apply a macro. We already know how to do all those things, let's just put the pieces together.

Remember this guy?

y<cmd>let @/=substitute(escape(@", '/'), '\n', '\\n', 'g')<cr>
Enter fullscreen mode Exit fullscreen mode

Is the thing we use to search the current selection. After this sequence we need to begin the macro. So we will add this.

gvqi
Enter fullscreen mode Exit fullscreen mode

Since we lose the selection when pressing y we need to reselect everything, so we use gv. Then qi just begins to record the macro in the register i.

Now everything together.

xnoremap <leader>i y<cmd>let @/=substitute(escape(@", '/'), '\n', '\\n', 'g')<cr>gvqi
Enter fullscreen mode Exit fullscreen mode

The story is not over yet. We need to apply the macro in each match. We will use gn to navigate to the match and select it. Once the cursor is in the match we apply the macro with @i. We are not doing that manually, no, we are going to create a keybinding.

nnoremap <F8> gn@i
Enter fullscreen mode Exit fullscreen mode

Story time.

A few months ago I was trying this plugin manager, packer.nvim. I had something like this in my configuration.

require('packer').startup(function(use)
  use({
    'nvim-lualine/lualine.nvim',
    config = function() require('plugins.lualine') end,
  })
  use({
    'akinsho/bufferline.nvim',
    config = function() require('plugins.bufferline') end,
  })
  use({
    'lukas-reineke/indent-blankline.nvim',
    config = function() require('plugins.indent-blankline') end,
  })
end)
Enter fullscreen mode Exit fullscreen mode

It bothered me that I had to repeat function() require... for each plugin. And yes, it's packer thing. They do weird stuff with functions. Anyway, I looked around in a few places and found a way to reduce the boilerplate. I wrote this function.

local function load(name)
  return string.format([[pcall(require, 'plugins.%s')]], name)
end
Enter fullscreen mode Exit fullscreen mode

And with it I could write the config option like this.

config = load('lualine')
Enter fullscreen mode Exit fullscreen mode

Now it's refactor time. I had to change each config option and this is how I did it.

See in asciinema.

  1. I select the pattern I want to search. Go to visual mode and select config =.
  2. I start recording the macro using <leader>i.
  3. I replace the old function with load.
  4. End the macro by pressing q.
  5. Press n to go to the next match.
  6. Press <F8> to apply the macro.

The Good Old Search and Replace

Sometimes a simple tool can do the job.

So vim has the substitute command. This is the syntax.

%s/<pattern>/<replacement>/g
Enter fullscreen mode Exit fullscreen mode

In here % is a range, it means the current buffer. Basically, search the entire buffer. The s is the actual command, 'cause we don't need to type substitute. <pattern> is the regular expression we want to search. <replacement> is the new text. And g is a flag, it tells vim to search the entire line. And notice that each item is separated by /, we could use other characters (like #) if we wanted to.

Say want to change the word config with setup. We just do this.

%s/config/setup/g
Enter fullscreen mode Exit fullscreen mode

It wasn't that difficult. We don't need to know regular expressions to use substitute.

Fighting Kirby

Okay. But there is something you should learn about regular expressions. Is just one simple trick, I swear.

What's this fighting Kirby deal? Is a way to remember this.

\(.*\)
Enter fullscreen mode Exit fullscreen mode

I learned this from ThePrimeagen.

With this pattern we can create a "group". Groups can capture the text in the search pattern, and we can reuse that text in the replacement pattern.

Consider this pattern.

%s/`\(.*\)`/[\1](#how-to-configure-\1-lsp)
Enter fullscreen mode Exit fullscreen mode

Here we capture the text that's surrounded by backticks. Then we reference that text using \1 and the replacement pattern. Here's a demo.

See in asciinema.

This demo shows how neovim makes a live preview, showing me the effects of the command in realtime. And yes, because of this feature I think search and replace is a decent alternative to multiple cursors.

If you liked the fighting Kirby consider making a keybinding for it.

cnoremap <F2> \(.*\)
Enter fullscreen mode Exit fullscreen mode

This way you can press <F2> in command mode and it'll type it for you.

Conclusion

You are ready. You can go out to the world and be productive in vim. I can't guarantee your happiness but you will survive. And its okay if you think multiple cursors are superior to all of this. Doesn't matter, now you can live without them when using vim.


Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.

buy me a coffee

Top comments (2)

Collapse
 
pbnj profile image
Peter Benjamin (they/them) • Edited

Great article with a lot of different tips.

Here is another tip: substitutions within visual selections

Imagine a file with content like:

1. 1.2.3.4 = 1.2.3.4
2. 2.3.4.5 = 2.3.4.5
3. 3.4.5.6 = 3.4.5.6
Enter fullscreen mode Exit fullscreen mode

And you want to change the dots . on the right-hand-side of = to another character, like _.

If you make a visual-block selection (i.e. ctrl+v) and then type :s/\./_/g, then all dots . on each line will be replaced with _. This is because ranges (i.e. '<,'>) operate on the entire line.

demo 1

But, vim offers \%V to match inside a visual selection. So, after making the visual-block selection, if you type :s/\%V\./_/g, then you will only change the dots . inside the visual selection to _.

demo 2

See :help \%V:

\%V Match inside the Visual area.  When Visual mode has already been
    stopped match in the area that |gv| would reselect.
    This is a |/zero-width| match.  To make sure the whole pattern is
    inside the Visual area put it at the start and just before the end of
    the pattern, e.g.: 
        /\%Vfoo.*ba\%Vr
    This also works if only "foo bar" was Visually selected. This:
        /\%Vfoo.*bar\%V
    would match "foo bar" if the Visual selection continues after the "r".
    Only works for the current buffer.
Enter fullscreen mode Exit fullscreen mode

Demo gifs recorded using ✨ vhs

Collapse
 
eckelon profile image
JA Samitier

this is GOLD, huge thanks!!!