Nov 3, '18
One of the biggest productivity hurdles for me when switching to Vim was getting a good project-wide find-and-replace workflow down. Most editors have a simple
CMD-SHIFT-F to find in project. However, as with most things in Vim, it's not obvious where to start. This is the workflow I've come to love, and I hope maybe you can take something from it 🤙
Out-of-the-box, Vim provides us with the
:vimgrep commands. The former uses the external
grep CLI tool installed on your system, but allows you to search from within Vim, and populates linked results into the quickfix list for easier refactoring (more on this later). The latter uses Vim's internal pattern matching, which is more consistent with
/ searching, but can be quite slow. Aside from performance concerns, both tools lack some key features present in modern GUI-based editors, like the ability to respect your project's gitignore, smart-case searching, etc.
For reasons like these, Vim allows us to customize the external tool used by the
:grep command, and there are other search wrapper plugins that can do even more for your workflow. The first step though, is to choose a project searching tool that's going to be used under the hood. Ideally, we want something that is performant, easy to use, has powerful features for more advanced code searches, and sane defaults for the most basic searches (like respecting your project's
Two of the more popular tools these days are ag and rg. Both of these can be integrated into Vim quite easily, but first let's focus on how to search using one of these tools. Personally, I'm more familiar with
ag, so we'll be using it from here on out.
ag will respect your
.gitignore, and allow you to regex search anywhere recursively within your current working directory. Here's a few examples of how you would use
ag on the command line:
ag 'function store'
Will search for
function store anywhere within your current working directly.
ag 'func.*store' app/Http/Controllers
Will regex search within the
app/Http/Controllers path specifically.
ag -Q 'function store(Request $request)'
-Q option will search for a literal string, instead of pattern matching with regex.
ag -U 'extends Model'
-U option will search everywhere, despite what's in your
Feel free to explore other options via
ag --help; It's a very powerful tool, and can be just as useful on the command line as it is in Vim! That said, now we need to figure out how to integrate
ag into Vim.
As mentioned earlier, Vim provides us with
:grep which can actually be wired up to use
rg. However, I'm going to suggest a more intuitive solution using a plugin called fzf.vim. Not only is fzf.vim one of the best fuzzy finders available, but it also provides us with some really nice
:Rg wrapper commands that really improve the project searching experience in Vim. For example:
Is the same as running the following on the command line:
However, it will pipe results into fzf's extended search mode, which allows you to narrow down the results in real-time using an expressive syntax. For example, you could then type:
Which would narrow your results to those starting with
app in the path, ignoring results in the
This narrowing of results in real-time is what makes this workflow so intuitive. Most other search solutions require you to precisely think ahead to all the possibilities without any visual feedback or opportunity to refine the output. Did you forget about that
extensions folder? Traditionally, you would have to modify and re-run your original search command over again until you are happy with the results. However, the real-time nature of fzf's extended search mode allows you to visually see the narrowing of results as you type. Though
ag is powerful enough without fzf.vim, having the ability to narrow further after you're already looking at the results can be a huge time saver.
There are a few downsides to fzf.vim's
:Ag out-of-the-box though. By default, it only allows you to pass a search regex. If you want to pass raw command line options or a specific path through to
ag, you will need to hook into
fzf#vim#ag_raw(), which is included but not setup out-of-the-box.
:Ag escapes all quotes and treats them as part of your query, so it can be confusing when you wrap your query in quotes as you would on the command line:
:Ag 'function index'
You would expect the same results as on the command line, but nothing is returned.
To remedy the above, I've written a plugin called vim-agriculture 🚜 to allow the passing of raw command line options to your search tool via
:RgRaw. It even smart quotes your input, so all of the following will work as expected:
:AgRaw func.*index :AgRaw 'func.*index' :AgRaw -Q 'function index()' app/Http/Controllers
As soon as you pass any options or quotes, it gets out of your way and passes all of your input directly to your search tool, giving you access to everything you can do on the command line 👌
And as a bonus, it provides a few handy mappings which you can throw into your
nmap <Leader>/ <Plug>AgRawSearch vmap <Leader>/ <Plug>AgRawVisualSelection nmap <Leader>* <Plug>AgRawWordUnderCursor
Once you are looking at the results of your project search, you can open a result simply by hitting
Enter. This is great for one-off changes, but if you are doing a larger refactor across multiple files, the quickfix list can be a nice way to keep track of your refactor. The beauty of this quickfix list is that it is presented as a buffer in a separate window, so you can use it as you would a notepad or checklist as you refactor your files.
To populate the quickfix list with the results from your project search, you can mark individual results with the
Tab key, or mark all results with
ALT-A, then hit
Enter to populate the quickfix list. Once populated, you can again open a result by hitting
Enter. However, the real power of the quickfix list is that you have the ability to perform a batch command on everything listed in it.
:cdo will perform the above substitution on every valid entry in the quickfix list. Similarily,
:cfdo will perform the subtitution on each file in the quickfix list. You can also pipe
:update to ensure each file gets written:
:cfdo %s/chew/bacca/gc | update
And this isn't just for substitutions, but for any command you wish to run on your result set. For example, you could delete all the files in the quickfix list:
:cfdo silent !rm %