Search and replace in Vim

Published on 28 March 2015.

24 September 2017: Fixed broken link to “Vim: Find and replace text across files.”

In this article I explain how I do search and replace in Vim.

As a programmer, I constantly rename variables, functions, and classes. The easier it is to rename something, the more likely I am to do it. I think renaming is probably one of the most important refactorings.

The advantage of being able to do it with Vim is that it is language independent. If I don’t have an IDE or a refactoring tool to help me, I can always use Vim.

At the heart of my workflow is the substitute command. I’ll show you how I use it to do renames in a single file and then show you how I have extended it to work across multiple files.

In a single file

When I want to rename something in a single file, I do the following:

Now I will explain how I have built this workflow. Let me start with the mapping:

map ,rw :call SubstituteInFile(expand("<cword>"))<CR>

The first thing that happens when I hit ,rw is that expand("<cword>") is evaluated. It returns the word that is under the cursor. That word gets passed to SubstituteInFile:

function! SubstituteInFile(text)
    execute GetSubstituteCommand("%", a:text)
endfunction

It calls GetSubstituteCommand with the % range and the passed in text. The search command that is returned gets executed. Here is GetSubstituteCommand:

function! GetSubstituteCommand(range, term)
  return a:range . "s" . input(":s", "/\\<" . a:term . "\\>/" . a:term . "/gc\<C-f>F/F/l")
endfunction

The call to input makes it possible for me to edit the substitute command before I proceed. The last bit in the input \<C-f>F/F/l drops me into the command-line window and moves the cursor to the first character of the replacement. That way I can quickly edit it. Say I make the following call: GetSubstituteCommand("%", "getCategory"). The command-line window shows the following with the cursor placed over g in the replacement:

/\<getCategory\>/getCategory/gc

Say I type cwfetchCategory<CR> (change word, type fetchCategory, hit enter). The return value is the following:

%s/\<getCategory\>/fetchCategory/gc

Running execute on that command is the equivalent of typing the following:

:%s/\<getCategory\>/fetchCategory/gc

Let me explain how this substitute command works:

So the Vim script I have written for renames in a single file is basically just to help me type the substitute command I use most often faster.

Across multiple files

The problem with renames is that they are not always local to a file. If I rename a function I also need to use the new name in all files where that function is called. My workflow involves both the grep command and the substitute command. It looks like this:

Now I will explain how I have built this workflow. Let me start with the mapping:

map ,mrw :call SubstituteInCodebase(expand("<cword>"))<CR>

When I hit ,mrw, the word under the cursor is passed to SubstituteInCodebase:

function! SubstituteInCodebase(text)
    let grepCommand = GetGrepCommand(a:text)
    let substituteCommand = GetSubstituteCommand("", a:text)
    execute grepCommand
    call QuickfixDo(substituteCommand . " | update")
endfunction

The first thing that happens here is that I build the grep command. It looks like this:

function! GetGrepCommand(term)
  return "grep " . input(":grep ", "-w '" . a:term . "'\<C-f>F'F'l")
endfunction

Similar to GetSubstituteCommand it drops me into command-line window so that I can customize the grep command. Say I make the following call: GetGrepCommand("getCategory"). The command-line window shows the following with the cursor placed over g:

-w 'getCategory'

Say I just hit enter here. The return value is then the following:

grep -w 'getCategory'

Running execute on that command is the equivalent of typing the following:

:grep -w 'getCategory'

I use ack as my grep program. In my .vimrc I have this:

set grepprg=ack

The -w flag is the equivalent of Vim’s \< and \>.

Typical customizations that I do:

After the grep command is created, the search command is created in the same way as before. But notice the lack of the % range. That is because now I only want the substitute command to operate on a single line, and not the whole file. After both commands have been created, the grep command is executed.

Now the quickfix list is populated with the search results and I can step through it and do the substitution on each matched line. That is what QuickfixDo is for: It will run an arbitrary command on each line in the quickfix list. In this case I pass the substitute command (without the % range) plus the update command. That ensures that I save the file if the substitution did any changes before I move on to the next match. QuickfixDo looks like this:

function! QuickfixDo(command)
    let itemCount = len(getqflist())
    let itemNr = 1
    while itemNr <= itemCount
        exe "cc " . itemNr
        exe a:command
        let itemNr = itemNr + 1
    endwhile
endfunction

The workflow for renames across multiple files contains only an extra grep step compared to the single file workflow. The defaults ensure that the matches found by the grep command are also found by the substitute command.

Further reading

The latest version of my search and replace configuration can be found at vimrc_search_replace.vim.

Other articles on the subject of search and replace in Vim that I found interesting:


Site proudly generated by Hakyll.