Advanced Search and Replace with Vim

I find search and replace to be a remarkably interesting topic, because it touches on so many Vim concepts. As a text editor, one of Vim’s superpower is composability, which enables combining small commands to accomplish complex tasks with search and replace.

Intro: The Vimmish Way

While I was first starting to use Vim, one of the first things I wanted to do was search for some text in my project and replace it with something else. At first, I certainly felt that this was not as simple as I had initially thought it would be. After a little time and practice, I discovered that Vim was powerful, incredibly useful and even a little bit fun. 🙂

In this post I will share several different ways to search and replace text in Vim. I’ll cover both the obvious, beginner-friendly way, as well as the more advanced, vimmish way.

Before we begin: Reading key sequences

I will use Vim’s syntax 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 finding 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 can quickly press * to see if other words highlight.

Using * and n []

It’s also useful to quickly navigate to all instances of that word. For example, if you want to 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.

Replace with *, n and . []

With this approach you can skip some matches. So, it’s great when you don’t want to just replace all matches, but instead want to choose which one 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.


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, let’s say 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” your last search.

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


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 would 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. It’s 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 over them one by one. 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'

augroup quickfix
  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


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'

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


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 = {}

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'

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

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

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

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.

:Grep and :Replace []

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

Vim: The Power To Compose

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

This might be more complex than in regular editors, but that complexity has several advantages. Composability is one of the biggest advantages. And, composability is the way of Vim. This enables you to compose several small atoms together, through a “Vim language”, into something bigger and more complex. Then, 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.

All this gives Vim users a lot of power. Plus, it’s incredibly fun to learn and practice. 🙂

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