I don't mean the snippet at the bottom of this gist to be a generic plug-n-play solution to your search needs. It is very likely to not work for you or even break things, and it certainly is not as extensively tested and genericised as your regular third-party plugin.
My goal, here and in most of my posts, is to show how Vim's features can be leveraged to build your own high-level, low-maintenance, workflows without systematically jumping on the plugins bandwagon or twisting Vim's arm.
Instant grep + quickfix
:help :grep is a fantastic tool, especially when combined with the quickfix, but it is certainly a bit rough around the edges. Let's see what it takes to smooth it out.
Tell Vim what external command to use for grepping
By default, Vim is set to use
:grep, which seems logical given the name of the command and all. But it also lets us customize the actual command via
:help 'grepprg'. We can use it to add default parameters, for example, or to point Vim to a completely different program.
Over the years, people have been busy working on faster and/or more developer-friendly
grep alternatives. The first such effort that I remember of was ack, then ag changed the rules of the game and other players entered the field, the latest being rg. Depending on your use cases, switching
:grep to one of those alternatives may be a wise choice.
In the example below I use
ag but feel free to substitute it with whatever you want.
set grepprg=ag\ --vimgrep
Now, this change is not strictly necessary for what we are trying to do but, even if we stopped here, using
grep can be a cheap and non-negligible upgrade to the built-in
Perform the search in a sub-shell
One of those rough edges I alluded to earlier is the fact that Vim executes the command in Vim's parent shell. This has a few consequences that contribute to a less than ideal experience:
- Vim is suspended for how long it takes the command to finish.
- The results are printed in the shell and clog our terminal's scroll buffer.
- We must press the dreaded
<CR>to come back to Vim.
Enters a sister command of
:help :cgetexpr, that takes an "expression" as input. In concrete terms,
:cgetexpr takes the raw output of
ag or whatever, parses it according to
:help 'errorformat' and creates a quickfix list.
:cgetexpr operates at a lower level than
:grep and doesn't involve any I/O so it is simpler and faster.
Now, how do we get the output of
ag if we can't execute it in the shell? Via
:help system() of course:
:cgetexpr system('ag foo')
But that's a lot to type, right? Well, how about wrapping it in a custom function?
function! Grep(args) return system('ag ' . a:args) endfunction
Now we have the following, which seems to solve all of the issues outlined above:
But we already have
ag set as our
'grepprg' so we might as well use that, making our function future-proof:
function! Grep(args) return system(&grepprg . ' ' . a:args) endfunction
From there, we can modify the function to handle multiple arguments because
:grep 'foo bar' filename is a very reasonable thing to do:
function! Grep(...) return system(&grepprg . ' ' . join(a:000, ' ')) endfunction
There are many ways to write the expression above, and choosing one or the other is mostly a matter of personal preference… and performance:
The third one it is, then:
function! Grep(...) return system(join([&grepprg] + a:000), ' ')) endfunction
What this does is:
- concatenate a list with
&grepprgas sole value with a list containing all the arguments passed to the function,
- join the resulting list into a single string,
- pass it to
Nice… Our little function works very well but calling it manually kind of defeats the point we are trying to make.
Create a command to populate the quickfix list
We will start small and simple with a simple
:Grep command that takes one or more arguments:
command! -nargs=+ Grep cgetexpr grep(<f-args>)
which seems to do the job:
but we might want to restrict our search to a given directory or file so it might be a good idea to use command-line completion:
command! -nargs=+ -complete=file_in_path Grep cgetexpr Grep(<f-args>)
and we might want to execute another command after
:Grep foo, so we should add another flag:
command! -nargs=+ -complete=file_in_path -bar Grep cgetexpr Grep(<f-args>)
and let's not forget
command! -nargs=+ -complete=file_in_path -bar Grep cgetexpr Grep(<f-args>) command! -nargs=+ -complete=file_in_path -bar LGrep lgetexpr Grep(<f-args>)
Aaaaand we are done with the usability issues mentioned above. Here is our little snippet in all its glory:
function! Grep(...) return system(join([&grepprg] + a:000), ' ')) endfunction command! -nargs=+ -complete=file_in_path -bar Grep cgetexpr Grep(<f-args>) command! -nargs=+ -complete=file_in_path -bar LGrep lgetexpr Grep(<f-args>)
Dealing with not-so-edge case
:Grep command seems to do a good job:
:Grep foo *.js is faster than ever and we don't have to press
<Enter> anymore. This is all good but
:grep expands wildcards by default (think
:grep foo % or
:grep bar ##) and we don't want to lose that, so we will have to modify our expression slightly:
return system(join([&grepprg] + [expandcmd(join(a:000, ' '))], ' '))
Here we use
:help expandcmd() to expand any glob present in the "arguments" part of the command before it is executed, which makes
:Grep foo % work like
:grep foo %.
Open the location/quickfix window automatically if there are valid entries in the list
There is one usability issue that we haven't mentioned before: when the search is done, we get a little message about the first item in our search but that's all. True, we can use
:help :cn and friends to jump around quickfix entries but, if we want to see the results, we have to use
:Grep foo :cwindow
:Gr foo | cw
What if we could open the quickfix window automatically when the quickfix list changes?
Autocommands to the rescue! The first step is to define a self-clearing group so that our autocommands don't needlessly pile up if we re-
augroup quickfix autocmd! " our autocommands here augroup END
Then, we add two autocommands:
- one to execute
- another one to execute
:cwindow opens the quickfix window if there are valid entries in the quickfix list and
:lwindow does the same for the location list and the location window.
We need both if we don't want to lose the flexibility of the original commands we are trying to replace.
augroup quickfix autocmd! autocmd QuickFixCmdPost cgetexpr cwindow autocmd QuickFixCmdPost lgetexpr lwindow augroup END
Note that this snippet can be easily generalized to cover every quickfix command by changing the patterns
[^l]* (to match commands that don't start with
l* (to match commands that start with
So far, we have managed to:
- eliminate I/O-related usability issues,
- improve wildcard expansion,
- open the quickfix window automatically to show the results.
This leaves us us with a relatively straightforward snippet of vimcript and a much slicker experience:
function! Grep(...) return system(join([&grepprg] + [expandcmd(join(a:000, ' '))], ' ')) endfunction command! -nargs=+ -complete=file_in_path -bar Grep cgetexpr Grep(<f-args>) command! -nargs=+ -complete=file_in_path -bar LGrep lgetexpr Grep(<f-args>) augroup quickfix autocmd! autocmd QuickFixCmdPost cgetexpr cwindow autocmd QuickFixCmdPost lgetexpr lwindow augroup END
That was, in a nutshell, where I was when this article, that took a different approach to the exact same problems, was posted on Reddit. The author's approach involved an abbreviation that allowed the user to type
:grep as usual, and have it expanded to a more potent
silent grep in an effort to reduce I/O-related noise.
While I don't use abbreviations much myself, I really like this idea of modifying the underlying behavior of common commands to unlock their potential. This is a strategy I have explored in the past that allows you to get more from the standard commands without having to learn new commands. You just do
:foo bar, as usual, but you get a much more usable outcome.
It is that specific aspect that lacked in my lovely snippet: I had to use
:Grep instead of
:grep so it forced me to learn a new command (not a wildly different one, mind you, but still). Enlightenment came from Peter Rincker, who, in the spirit of the article, suggested the following missing piece:
cnoreabbrev <expr> grep (getcmdtype() ==# ':' && getcmdline() ==# 'grep') ? 'Grep' : 'grep'
which ties the whole thing up perfectly.
Now I can do the same
:grep foo that I learned 10 years ago, but with a much cleaner and less labor-intensive user experience, and with a few lines of minimal, readable, maintainable configuration.
NOTE: While I consider this short snippet to be extremely useful, I don't pretend it to be a plug and play solution to all your problems, real or imaginary. I would prefer this experiment to inspire you to experiment with your own workflow than to be yanked and put in your
vimrc. I can't stop you from doing that, of course, but oh well.