Running a search and replace across a massive codebase usually comes down to an uncomfortable gamble: either you blindly trust a one-liner script and hope nothing blows up, or you spend the next hour hand-checking fifty different tabs to keep from breaking dependencies. It turns out Vim already has an insanely sharp, surgical way to handle this once you step past basic single-buffer tweaks and tap into the quickfix list. Here is how to wire ripgrep right into your editor so you can refactor across an entire repo quickly without breaking a sweat.

Core Commands at a Glance

Task Command
Single-file, all matches with confirmation :%s/old/new/gc
Restrict to visual selection :'<,'>s/\%Vold/new/g
Search project with ripgrep :grep "pattern"
Execute replace across search results :cfdo %s/old/new/gce | update

The Core: Substitute Command and Essential Flags

At its heart, Vim’s substitution revolves around the classic :s command, using the familiar :[range]s/search/replace/[flags] pattern. If you don't feed it an explicit range, Vim simply looks at whatever line your cursor happens to be resting on.

Slapping a % right up front tells Vim you want to target the entire buffer from top to bottom.

Vim Script

code1 line
:%s/legacy_func/modern_func/g

The flags at the tail end control the behavior. You’ve probably seen g (global, catching every occurrence on a line instead of just the first) and c (confirm, prompting with y/n/a/q at every hit). You can also toss in i to ignore case, or a capital I if you need to be strictly case-sensitive.

Whenever you're doing real refactoring work, make it a habit to keep that c flag on. That quick visual confirmation is the cheapest insurance policy you'll ever get against unintended logic edits.

Precision Targeting: Ranges and Visual Selections

Most of the time, you don't actually want to wipe out every single matching string across a file. Vim makes it pretty painless to isolate your substitution to a specific neighborhood of code.

If you know the line numbers, you can just run something like :10,20s/foo/bar/g.

Marks work just as neatly here. Drop mark a at the top and mark b at the bottom, and you can simply fire off :'a,'bs/foo/bar/g.

Where people constantly get burned, though, is with visual blocks. If you highlight a visual block using Ctrl-v and hit :, Vim defaults to pre-filling :'<,'>. The catch is that Vim interprets those marks as the entire line boundaries of the selection, meaning it will happily replace matches outside your highlighted column box.

To tell Vim to only touch what's strictly inside the highlighted boundary, you have to tuck the \%V atom straight into your search pattern.

Vim Script

code1 line
:'<,'>s/\%Vfoo/bar/g

Mastering Vim Regex (The Magic Ladder)

Vim’s out-of-the-box regex engine feels pretty dated because it expects you to escape almost everything. Things you take for granted in modern engines - like parens, curly braces, and plus signs - demand backslashes just to act like operators, turning even moderate expressions into unreadable line noise.

The easiest way around this headache is leading your pattern with \v (very magic). It tells Vim to treat every non-alphanumeric character as a regular expression symbol right away, making it feel much closer to what you'd write in Python or Perl.

Vim Script

code5 lines
" Default (Magic) - Heavy escaping
:%s/\([a-z]\+\)_\([a-z]\+\)/\1-\2/g

" Very Magic - Clean syntax
:%s/\v([a-z]+)_([a-z]+)/\1-\2/g

Match Boundaries: \zs and \ze

Whenever you want to locate a wider pattern but only swap out a tiny slice in the middle, setting up lookaheads or lookbehinds can get messy fast. Vim offers a much cleaner trick with \zs (set start of match) and \ze (set end of match).

Say you want to turn config into setup, but specifically when it's sitting inside load_config_file():

Vim Script

code1 line
:%s/\vload_\zsconfig\ze_file()/setup/g

Vim uses the full context to match the line, but it only applies the replacement text between the \zs and \ze markers.

Capture Groups and Expressions (=)

One quick muscle-memory trap for anyone switching over from VS Code, bash, or sed is capture numbering. Vim relies on \1, \2 rather than the typical $1, $2 syntax.

You can also take things a step further by passing \= in the replacement slot. This instructs Vim to evaluate whatever comes next as a live Vimscript expression. You can grab the matched text using submatch(0) and tweak it with standard code on the fly.

Vim Script

code2 lines
" Multiply all numbers by 10
:%s/\v\d+/\=submatch(0) * 10/g

Project-Wide Search: Integrating Ripgrep

Vim does have an internal :vimgrep tool, but running it against a sizable project will bring things to a crawl because it silently reads every single candidate file into memory. If you want results to land immediately, redirecting Vim’s built-in :grep engine to use rg (ripgrep) is non-negotiable.

Drop these lines into your init.lua or .vimrc:

Vim Script

code4 lines
if executable('rg')
  set grepprg=rg\ --vimgrep\ --smart-case\ --hidden
  set grepformat=%f:%l:%c:%m
endif

From that point on, typing :grep "AuthService" uses ripgrep in the background and populates your quickfix list right away. If you ever need to find files by their names rather than what's inside them, Linux's find command is still the trusty fallback tool when your ripgrep setups aren't handy.

The Quickfix List: Your Refactoring Staging Area

Think of the quickfix list as a scratchpad buffer that stores file paths, line positions, and matched text. Once your :grep finishes running, pop it open with :copen to scan through everything, and navigate back and forth using :cnext and :cprev.

This window becomes your working staging area. It shows you the exact spread of what you're about to change before you actually pull the trigger.

Narrowing Results with the cfilter Plugin

In reality, your search results will almost always grab things you didn't want - like build output, fixture mocks, or test files. Vim 8.2 and newer actually comes with an awesome built-in plugin called cfilter that handles this, but it’s turned off out of the box.

To turn it on, just add this to your startup config:

Vim Script

code1 line
packadd cfilter

With your quickfix window visible, filtering items out takes seconds. If you want to purge all your test files from the list:

Vim Script

code1 line
:Cfilter! /_test\.go/

And if you want to trim the list down so it only contains files under your src directory:

Vim Script

code1 line
:Cfilter /src\//

Executing the Replace: cfdo vs cdo vs argdo vs bufdo

Once your quickfix list is trimmed down to the exact files you care about, it's time to run the replacement across them. Picking the wrong looping command here can cause some pretty nasty side effects, so it helps to know how they differ:

  • bufdo: Runs your command across every single open buffer in your editor session. Do not use this for project refactoring. If you happen to have a random config file sitting in a background buffer, bufdo will quietly alter it.

  • argdo: Runs the command across files explicitly queued up in your argument list (:args src/*.js). It’s reliable, but requires you to build that file list by hand first.

  • cdo: Runs your command against every single matching entry in your quickfix list. If a file contains ten occurrences, cdo will open that file and execute your command ten separate times.

  • cfdo: Runs your command once per unique file found in the quickfix list. It loads the file, does the work once across all its lines, and moves on to the next file.

For project-wide changes, cfdo is almost always the right tool for the job.

Vim Script

code1 line
:cfdo %s/foo/bar/gce | update

Pay attention to that little e flag near the end: it tells Vim to ignore errors if a pattern isn't found. If you used cfilter earlier or tweaked a file in another window, a missing pattern would normally choke and abort the entire loop. The e flag keeps things running smoothly through the whole list, while | update writes changes to disk only if the buffer actually changed.

Modern Neovim Workflows

If your daily driver is Neovim, you can plug modern ecosystem tools straight into this exact same workflow without changing your habits.

Telescope handles fuzzy finding smoothly. You can scan for text with :Telescope live_grep, select the hits you care about, and press <C-q> to dump everything cleanly into the quickfix list for standard batch processing.

If you prefer having a dedicated visual layout while keeping things snappy, grug-far.nvim is pretty much the go-to choice these days. It opens up a split buffer where you can fill in your ripgrep pattern alongside the replacement text, giving you a live interactive preview of the diff across your codebase before committing anything.

If you're still wrapping your head around Vim's modes, saving and exiting Vim walks through the essential :wq, :x, and :q! basics you'll lean on once the updates finish. And because multi-file refactoring can touch dozens of files at a clip, keeping Git's revert and reset commands fresh in your mind gives you a dependable safety net if a replacement ever catches you off guard.