Running a search and replace across a massive codebase usually means choosing between blindly trusting an external script or manually opening fifty files to avoid breaking dependencies. Vim offers a surgical alternative when you move beyond basic file-level substitutions and leverage the quickfix list. Here is how to wire ripgrep directly into your workflow for precise, safe project-wide refactoring.
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
The foundation of Vim substitution is the :s command. The basic syntax is :[range]s/search/replace/[flags]. Without a range, Vim only searches the current line.
Adding % as the range tells Vim to search the entire file.
:%s/legacy_func/modern_func/g
The flags dictate how the substitution behaves. You likely know g (global, replaces all occurrences on a line) and c (confirm, asks y/n/a/q for each match). The i flag ignores case, while I forces case sensitivity.
When refactoring, always use the c flag. It provides a visual sanity check before modifying your logic.
Precision Targeting: Ranges and Visual Selections
You rarely need to replace every single instance of a word in a file. Vim allows you to restrict substitutions to specific blocks of text.
Operating on a specific line range is straightforward: :10,20s/foo/bar/g.
You can also use marks. If you place mark a and mark b, the command becomes :'a,'bs/foo/bar/g.
The biggest trap developers fall into involves visual selections. If you highlight a visual block (Ctrl-v) and press :, Vim automatically inserts '<,'>. However, this range targets the entire lines spanned by your selection, not just the highlighted columns.
To restrict the replacement strictly to the visually selected text, you must inject the \%V atom into your search pattern.
:'<,'>s/\%Vfoo/bar/g
Mastering Vim Regex (The Magic Ladder)
Vim's default regular expression engine requires heavy escaping. Parentheses, braces, and the plus sign all need backslashes to act as regex operators. This makes complex patterns unreadable.
You fix this by starting your pattern with \v (very magic). It forces Vim to treat all non-alphanumeric characters as regex symbols, aligning it closer to standard Perl/Python regex.
" 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
When you need to find a pattern but only replace a small part of it, standard regex requires complex lookarounds. Vim uses \zs (start of match) and \ze (end of match) to simplify this.
If you want to change config to setup only when it appears inside load_config_file():
:%s/\vload_\zsobject\ze_file()/setup/g
Vim finds the entire string but only replaces the characters between \zs and \ze.
Capture Groups and Expressions (=)
A common gotcha for users coming from VS Code or sed is capture group notation. Vim uses \1, \2 instead of $1, $2.
You can take this further using the \= operator in the replacement field. This tells Vim to evaluate the replacement as a Vimscript expression. You can use submatch(0) to grab the entire match and mutate it dynamically.
" Multiply all numbers by 10
:%s/\v\d+/\=submatch(0) * 10/g
Project-Wide Search: Integrating Ripgrep
Vim's native :vimgrep is reliable but aggressively slow on large codebases because it loads every file into memory to search it. To search at the speed of thought, you must map Vim's :grep command to rg (ripgrep).
Add this to your init.lua or .vimrc:
if executable('rg')
set grepprg=rg\ --vimgrep\ --smart-case\ --hidden
set grepformat=%f:%l:%c:%m
endif
Now, typing :grep "AuthService" instantly populates Vim's quickfix list with the results. For searching by filename pattern rather than content, Linux's find command remains the reliable fallback when ripgrep isn't configured.
The Quickfix List: Your Refactoring Staging Area
The quickfix list is a dedicated buffer containing file locations and line numbers. After running :grep, type :copen to view the results. You can jump through them using :cnext and :cprev.
This list is your staging area. It holds exactly where your targeted changes need to happen.
Narrowing Results with the cfilter Plugin
Often, your ripgrep search returns noise, test files or compiled assets you don't want to modify. Vim 8.2+ ships with a built-in, but disabled, plugin called cfilter.
Enable it in your config:
packadd cfilter
With the quickfix window open, you can filter the list down. To remove all test files from the quickfix list:
:Cfilter! /_test\.go/
To keep only matches residing in the src directory:
:Cfilter /src\//
Executing the Replace: cfdo vs cdo vs argdo vs bufdo
Once your quickfix list contains the exact lines you want to change, you need to execute the substitute command across them. Choosing the wrong *do command here causes severe side effects.
bufdo: Executes a command across all open buffers. Never use this for search and replace. If you have an unrelated configuration file open in the background,bufdowill modify it silently.argdo: Executes a command across files explicitly loaded into the argument list (:args src/*.js). This is safe but requires manually specifying file paths.cdo: Executes a command on every valid line inside the quickfix list. If a file has 10 matches,cdoopens the file and runs the command 10 times.cfdo: Executes a command on every unique file present in the quickfix list. It opens the file once, runs the command, and moves to the next file.
For project-wide search and replace, always use cfdo.
:cfdo %s/foo/bar/gce | update
Notice the e flag appended to the command. It tells Vim to ignore "pattern not found" errors. If you use cfilter or modify the file externally before running cfdo, a missing match will halt the entire cfdo loop. The e flag ensures the loop completes successfully across all files. The | update portion automatically saves each file after the substitution.
Modern Neovim Workflows
If you use Neovim, you can bridge modern plugins directly into this core workflow.
Telescope is excellent for visual searching. You can search for a string using :Telescope live_grep, select the results you want, and press <C-q> to send them directly to the quickfix list. From there, you execute your standard :cfdo command.
For developers who want an IDE-like visual interface without sacrificing Neovim's speed, the grug-far.nvim plugin is the current standard. It provides a dedicated split window where you type your ripgrep search and your replacement string, rendering a live preview of the changes across the project before you commit them.
If you are new to Vim's modal interface, saving and exiting Vim covers the :wq, :x, and :q! commands you will need once the replace is done. And since project-wide refactoring touches many files at once, pairing cfdo with Git's revert and reset commands gives you a reliable safety net if the substitution produces unexpected results.




