I find search and replace to be a remarkably interesting topic, because it touches on so many Vim concepts.

When I started using Vim, one of the first things I wanted to do was search for some text in my project and replace it with something else.

Unfortunately for me, it was not as simple as I had initially thought :)

In this post I will show how several different ways to search and replace text in Vim. From the obvious, beginner-friendly way, to the more advanced, vimmish way.

Before we begin: Reading key sequences

I will use Vim’s format to describe sequences of keys. That means <C-r> means press the CONTROL key, and while holding it, press the r key. <CR> means ENTER key, <Esc> means ESCAPE, <Space> means SPACEBAR, etc.

Searching in a single file

Let’s start with our most simple use-case: Searching for some text in a single file.

This is quite a common need. In my regular Vimming, I think one of my most used (and favorite) keys is *. In NORMAL mode, simply press * on top of a word, and Vim will highlight all the instances of that word in your current buffer. If you want to go to the next instance, press n, if you want to go back, press N.

I use that a lot for typos, if I want to make sure I didn’t make a typo on some word, and I know that word is already defined somewhere in the same file, I quickly press * to see if other words highlight.

It’s also useful to quickly navigate to all instances of that word, for example, navigate through all the usages of some variable in your current file.

Section Help

  • :help 03.8
  • :help *
  • :help n

Replacing in a single file: One by one

Another quite common need is to rename an identifier. This can be tricky when the variable exists in multiple files, but for local variables, it’s amazingly easy.

You can use the . operator to repeat the last thing I did in INSERT mode, together with * and n you can easily do fine-grained replacements.

With this approach you can skip some matches, so it’s great when you don’t want to just replace all matches, and instead want to choose which ones you want to operate on.

Section Help

  • :help 04.3

Replacing in a single file: Bulk

Alternatively, you can replace all matches in the file in one go, with the :substitute command, or just :s, as it’s normally used.

Without flags, :substitute will only replace one match per line. Most of the time, the g flag is passed, to substitute all matches. You can also pass the c flag so Vim asks for confirmation before doing each replacement.

:%s/this/that/gc

If you already were searching for something using / or ?, then Vim populates the search register for you. When using :s, and the search string is empty, Vim will use the search register to perform the replacement.

For example, you were searching for all instances of the word this by typing /this. You then notice you want to change them.

You can now simply type :%s//that/ to run the replacement. Vim “remembers” our last search.

You can also manually use the register by typing <C-r> followed by the register you want to use, such as ":

:%s/<C-r>"/replacement/g

Section help

  • :help registers
  • :help 10.2
  • :help i_CTRL-R

Searching in multiple files

To search across multiple files, Vim provides the :vimgrep command.

It works out of the box. For example, you can run the following command to search for all instances of SomeModelClass inside the app/models directory:

:vimgrep SomeModelClass app/models

Nice! By default, Vim will populate the quickfix list with the search results, but it won’t display it. You can open the quickfix window with the :copen command.

While that works, it’s not the best. You see :vimgrep uses Vim’s internal implementation of grep, which is totally compatible but terribly slow. Not really what you want to use for most modern software projects (ahem node_modules ahem).

Experienced Vim users will instead use something like ripgrep or ag for searching across multiple files.

Being the good first-class UNIX citizen that Vim is, it integrates nicely with external programs, so we can easily set this up:

set grepprg=rg\ --vimgrep\ --smart-case

We can now use the :grep command to search in our files, using ripgrep!

:grep myvariable app/models/

Section help

  • :help :vimgrep
  • :help :grep
  • :help grepprg
  • :help quickfix

Replacing in multiple files

Once you have performed your search, and have the quickfix list populated with all your matches, you can then run a replacement with:

:cfdo %s/pattern/replacement/g

The :cfdo command will take each file in your quickfix list and apply a command to it. We use the :substitute command (a.k.a :s) to do the actual replacement.

That’s it! This approach might seem complicated to non-Vim users, but it is made of smaller pieces, composed together to create one big action.

Being composable, I can replace commands here and there. For example, instead of replacing, I could actually delete all lines containing the match, with :global:

:cfdo %g/<my-grep-pattern>/d

Section help

  • :help :cfdo
  • :help :cdo
  • :help :global

Filtering results

What if you want to make a replacement, but only to some of the files in your quickfix list? Do you run a second, more specific search?

No need! Vim provides a plugin named cfilter, which can helps us in this case. You can use it by adding this to your .vimrc:

packadd! cfilter

Check it out with :help cfilter. It gives you a nifty little command to filter the results of the quickfix list: :Cfilter:

:Cfilter app/models  # display only entries matching `app/models`
:Cfilter! .swp       # remove entries matching `.swp`

That’s great, and most of the time, it’s just enough. But sometimes, it might make sense to cherry-pick the entries you want to keep, by going one by one over them. For that, I have mapped x to remove the entry under the cursor:

function! s:QfRemoveAtCursor() abort
  let currline = line('.')
  let items = getqflist()->filter({ index -> (index + 1) != currline })
  call setqflist(items, 'r')
  execute 'normal ' . currline . 'G'
endfunction

augroup quickfix
  autocmd!
  autocmd FileType qf nnoremap <buffer><silent> x :call <SID>QfRemoveAtCursor()<CR>
augroup END

Now whenever I press x on top of an entry in the quickfix list window, it gets deleted.

Trimming the quickfix list like this is useful, not only to find what you are looking for, but to perform batch operations on all matches with :cdo and :cfdo!

Section help

  • :help usr_40.txt
  • :help usr_41.txt

:Grep

Remember we had to manually run :copen to see the quickfix list after every :grep? Let’s make that automatic by creating a custom :Grep command to open the quickfix list for us:

function! s:Grep(...) abort
  let pattern = get(a:, 1, '')
  if pattern == '' | return | endif

  let path = get(a:, 2, '.')
  execute 'silent! grep! "' . escape(pattern, '"-') . '" ' . path . ' | redraw! | copen'
endfunction

command! -nargs=+ -complete=file Grep silent! call s:Grep(<f-args>)

Neat! Now all we have to do is use :Grep instead of :grep! We even get file autocompletion for grep’s optional second parameter.

Section help

  • :help usr_40.txt
  • :help key-mapping
  • :help mapleader
  • :help i_CTRL-R

:Replace

Following on :Grep, let’s implement a :Replace command, so our whole search and replace quest can be reduced to just running two easy to remember commands.

if !exists('s:latest_greps')
  let s:latest_greps = {}
endif

function! s:Grep(...) abort
  let pattern = get(a:, 1, '')
  if pattern == '' | return | endif

  let s:latest_greps[pattern] = 1
  let path = get(a:, 2, '.')
  execute 'silent! grep! "' . escape(pattern, '"-') . '" ' . path . ' | redraw! | copen'
endfunction

function! s:Replace(original, replacement) abort
  if a:original == '' || a:replacement == '' | return | endif

  execute 'cfdo %s/' . escape(a:original, '/') . '/' . a:replacement . '/ge'
endfunction

function! LatestGreps(ArgLead, CmdLine, CursorPos)
  return keys(s:latest_greps)
endfunction

command! -nargs=+ -complete=file Grep silent! call s:Grep(<f-args>)
command! -nargs=+ -complete=customlist,LatestGreps Replace silent! call s:Replace(<f-args>)

nnoremap <Leader>g :Grep<Space>
nnoremap <silent> <Leader>r :call feedkeys(':Replace<Space><Tab>', 't')<CR>

Our :Grep now stores a list of previous searches, so it can then be used as autocompletion for :Replace.

:Replace itself simply runs :cfdo with a :substitute command. It takes two arguments: a search string, and a replacement string. The search string is autocompleted, so we don’t have to worry about it.

Finally we map <Leader>g to the :Grep command, and <Leader>r to the :Replace command.

When searching, all we need to do now is press <Leader>g, and type what we want. For replacing, we simply press <Leader>r and type our replacement.

Now we’re talking! We can even filter the quickfix list in any way we want before doing our replacement. Or we could make the replacement confirm on each match, if we wanted to, using the c flag of the :substitute command.

You can also run all the search and replace machinery in a single file if you want, too. Simply do :Grep my-pattern % to search in the current buffer.

Section help

  • :help usr_40.txt
  • :help usr_41.txt

Conclusion

In this post I showed several ways to search and replace text in Vim.

It might be more complex than in regular editors, but that complexity has several advantages. Composability being one of the big ones.

Composability is the way of Vim. It allows you to compose several small atoms together, through a “Vim language”, into something bigger and more complex. It gives Vim users a lot of power, and it’s incredibly fun to learn :)

Every new thing you learn in Vim adds a lot to your final user experience, because you can compose it with all the other tools you know.

Let me know of any feedback you might have - I’m still learning Vim :) - and I hope you find this useful!