Interactive Fuzzy Finding in Vim without Plugins

Table of Contents

Overview

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?

The Setup

This setup is simple and it leverages a feature in vim called quickfix.

The flow is:

  1. Call fzf
  2. Format the output to make it compatible with errorformat
  3. Write the results to a temporary file
  4. 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
  5. 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 them errorformat-compatible.
    • Note: you can drop this awk command if you set 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
  • 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
  • nnoremap <leader>ff :Files<cr>
    • Normal-mode mapping so that we can trigger this flow with <leader>ff

Bonus

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 to fzf for further fuzzy searching.

Caveat

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.

Summary

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