FOREWORDS
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.
: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.
By default, Vim is set to use grep
for :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 ag
or rg
over grep
can be a cheap and non-negligible upgrade to the built-in :grep
and :lgrep
.
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 :grep
, :help :cgetexpr
, that takes an "expression" as input. In concrete terms, :cgetexpr
takes the raw output of grep
or 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.
:cgetexpr <expression>
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:
:cgetexpr Grep('foo')
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:
Expression | Performance | Stylish |
---|---|---|
&grepprg . ' ' . join(a:000, ' ') |
0.000072s | No |
join(extend([&grepprg], a:000), ' ') |
0.000065s | Yes |
join([&grepprg] + a:000, ' ') |
0.000050s | Yes |
The third one it is, then:
function! Grep(...)
return system(join([&grepprg] + a:000, ' '))
endfunction
What this does is:
- concatenate a list with
&grepprg
as sole value with a list containing all the arguments passed to the function, - join the resulting list into a single string,
- pass it to
system()
.
Nice… Our little function works very well but calling it manually kind of defeats the point we are trying to make.
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:
:Grep foo
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 :help :lgrep
:
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>)
Our custom :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 %
.
Neat!
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 :help :cwindow
:
:Grep foo
:cwindow
or, shorter:
: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-:source
our vimrc
:
augroup quickfix
autocmd!
" our autocommands here
augroup END
Then, we add two autocommands:
- one to execute
:cwindow
whenever:cgetexpr
is executed, - another one to execute
:lwindow
whenever:lgetexpr
is executed.
: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 cgetexpr
and lgetexpr
to [^l]*
(to match commands that don't start with l
) and l*
(to match commands that start with l
), respectively.
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 vimscript 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.
I use this approach for quite some time now and am rather satisfied with it. If not just for the reason that I can avoid fuzzy finders that I do not particularly like anyway. I have one issue though. For some reason Vim can't seem to detect the filetype of the first item of the Quickfixlist, which is rather annoying. Do you have an idea what could be the reason for this rather strange behavior? I tested this in multiple directories so it seems to be independent off the specific filetype.