28
Interactive Fuzzy Finding in Vim without Plugins
Recently, however, I have been experimenting with a plugin-free Vim setup and while :find
is sufficient for some use-cases, I found myself quitting vim and running fzf
to find deeply nested files in new or large projects.
There has to be a simple way to integrate a command-line program with a command-line editor, right?
This setup is simple and it leverages a feature in vim called quickfix.
The flow is:
- Call
fzf
- Format the output to make it compatible with
errorformat
- Write the results to a temporary file
- Load the results into Vim's quickfix list with
:cfile
or:cgetfile
, so that we can navigate through the results with:cnext
/:cprevious
or:copen
- Clean up temporary file
Here is what the final vimscript looks like:
function! FZF() abort
let l:tempname = tempname()
" fzf | awk '{ print $1":1:0" }' > file
execute 'silent !fzf --multi ' . '| awk ''{ print $1":1:0" }'' > ' . fnameescape(l:tempname)
try
execute 'cfile ' . l:tempname
redraw!
finally
call delete(l:tempname)
endtry
endfunction
" :Files
command! -nargs=* Files call FZF()
" \ff
nnoremap <leader>ff :Files<cr>
A quick breakdown:
-
let l:tempname = tempname()
- Generate a path to a temporary file and store it in a variable.
- See
:h tempname()
-
execute 'silent !fzf --multi ' . '| awk ''{ print $1":1:0" }'' > ' . fnameescape(l:tempname)
- Call
fzf
with--multi
to allow for selecting multiple files - Pipe to
awk
to append:1:0
to fzf results to make themerrorformat
-compatible. - Note: you can drop this
awk
command if youset errorformat+=%f
in your vimrc, but I found%f
to capture a lot of false-positives from other programs' outputs and therefore:cnext
/:cprevious
don't function on these false-positive results. - Finally, direct the results into the temp file
- Call
-
execute 'cfile ' . l:tempname
- Load results from temp file into quickfix list and jump to the 1st result.
- Note #1: you may use
:cgetfile
to only load results into quickfix list without jumping to the 1st result. - Note #2: you may replace
:cfile
/:cgetfile
with:lfile
/:lgetfile
to use location list instead of quickfix list. Location lists are window-specific, whereas quickfix lists are global. So if you prefer to have different set of results per vim window, then use:lfile
/:lgetfile
.
-
call delete(l:tempname)
- Clean up by deleting the temp file
-
command! -nargs=* Files call FZF()
- Invoke
FZF()
function when we call:Files
in vim
- Invoke
-
nnoremap <leader>ff :Files<cr>
- Normal-mode mapping so that we can trigger this flow with
<leader>ff
- Normal-mode mapping so that we can trigger this flow with
While :set grepprg=rg\ --vimgrep
is again sufficient for most of my use-cases, those who have used :Rg
in fzf.vim will appreciate the interactive fuzzy grepping experience and the ability to preview results before opening files in vim.
Well, here is a similar experience with pure vim (obviously, fzf
and rg
binaries are still required):
function! RG(args) abort
let l:tempname = tempname()
let l:pattern = '.'
if len(a:args) > 0
let l:pattern = a:args
endif
" rg --vimgrep <pattern> | fzf -m > file
execute 'silent !rg --vimgrep ''' . l:pattern . ''' | fzf -m > ' . fnameescape(l:tempname)
try
execute 'cfile ' . l:tempname
redraw!
finally
call delete(l:tempname)
endtry
endfunction
" :Rg [pattern]
command! -nargs=* Rg call RG(<q-args>)
" \fs
nnoremap <leader>fs :Rg<cr>
This offers the same experience where:
-
:Rg
without arguments will load all text into vim and allow users to interactively type and preview results before selecting files -
:Rg [pattern]
will pre-filter results to just ones that match[pattern]
before passing them tofzf
for further fuzzy searching.
In my testing, I found one major caveat that did not impact me too much, but it is still worth calling out here:
Executing shell commands in vim with bangs, like :!fzf
, is not meant to be interactive (at least, not out of the box). This could be a problem in GVim/MacVim. The vim docs mention the following workaround:
On Unix the command normally runs in a non-interactive shell. If you want an interactive shell to be used (to use aliases) set 'shellcmdflag' to "-ic".
Setting shellcmdflag=-ic
could incur a time penalty, depending on your shell startup/initialization times.
Vim is extremely versatile and customizable.
With some knowledge of vim concepts (e.g. quickfix, :cfile
/:lfile
) and a little bit of (vim & bash) scripting, you can achieve a richer experience and pleasant integrations to enhance your productivity in a way that suits you and your workflow.
I hope you enjoyed this post and I hope it inspires you to develop and share your productivity tips and tricks in vim.
Happy hacking!
28