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:
  • 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 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!

    45

    This website collects cookies to deliver better user experience

    Interactive Fuzzy Finding in Vim without Plugins