There are plenty of occasions when using an LSP to ‘rename’ a symbol isn’t possible. Perhaps you are amending JSON files, or some other basic data format like csv or txt. In those instances there will be times when you want to do a find and replace across multiple files. How can we do that in Neovim?

I’ll give you the short version of how I do it, and then I’ll explain what is going on in more detail…

In short

  1. Use telescope to find (grep) the things
  2. Send the things to the Quickfix list
  3. Use cfdo command to change the string to what you want and update all the files: cfdo %s/stringOne/stringTwo/g | update | bd

Simple enough when you are used to it but I found that workflow strange having come to Vim/Neovim from conventional text editors.

But assuming you want to know how we just did what we did…

The explanation

Let’s break down what we are doing here.

  1. Use Telescope to find the things you want. In Vim-land, ‘grep’ and ‘find’ are kind of interchangeable terms. Certainly for our purposes here, it’s the same thing. The term grep is just short-hand for the “global regular expression print”; you know, “find” 😉 It is also worth saying here you can use whatever finding tool you want, fzf, or just the grep command itself. As long as that tool gives you a list of things you can send to the Quickfix list.
  2. So, you have a list of all the instances of the thing you were looking for. Now what? Well, built into Neovim is the ‘Quickfix’ list and the ‘Location’ list. Quickfix can deal with locations across multiple files, Location list is for the current window. Either way, these are just lists of things you may want to do something with. They might be a list of errors, but in this case we are using it to send a list of the lines where we want to change the text we searched for.
  3. Telescope is able to send things directly to the Quickfix list, and I am using that with a mapping of CTRL+Q (which might be the default) to do that.
  4. So now, the list of things is in this Quickfix list, and you can see them like a small buffer in a tray at the bottom of your interface. You can go up and down like you would in any normal file and go back and forth through the locations with :cnext, and :cprev. But what we want to do is just cycle through them all replacing one string with another.
  5. To do string replacement across all the content of any other buffer, we would use %s/stringOne/stringTwo/g where g means global so it replaces every instance on a line (and append c if you wanted to confirm each one). However, we want to do this in one go across all our instances and files.

To do that we use cfdo which is short for “Change File Do”.

So we “change file do”, tell it the change we want with our %s/this/that/g command and then pipe that into the update command, to save the file (otherwise you would have a load of changed buffers you then need to manually write/save), and finally pipe into bd to delete the buffer and keep the resources low.

Summary

I think the way you do find and replace across a project in a modern editor like Sublime Text, Zed, or even VS Code, is much simpler to reason about, but this is an effective way to do the same thing in Vim. There are alternative approaches to this, even when you can’t use an LSP, like using fd and sed outside of Neovim but this is the way I do it.