Fast Project Finding With Fzf

fzf is a fantastic utility, written by an author with a history of writing useful things. He’s also a vim user, and in addition to his other vim plugins he has created an “enhancement” plugin called fzf.vim.

One of the neat things fzf.vim does is make it easy to create new commands for fuzzy searches. If you’re like me, you probably have some absurd number of project repositories you keep around and jump to, as necessary. Not everything is in the same directory (e.g. ~/work/), naturally, and with a laptop, desktop, and a couple other machines the less-frequently used repos may be where one least expects them to be — or not present at all.

It’s not hugely annoying, just a sort of mild pain to have to spend several extra seconds doing a fuzzy search manually, rather than having fzf do it. But we do have fzf, and it’s not difficult at all to build out a new search, so there’s really no reason to keep on inflicting that pain.

Create a :Projects command

Let’s create a new command in my vimrc, :Projects, that invokes fzf to search through all the different work directories I have.

1
2
3
4
5
6
command! -nargs=0 Projects
    \ call fzf#run(fzf#wrap('projects', {
    \   'source': 'find ~/work ~/.vim/plugged -name .git -maxdepth 3 -printf ''%h\n''',
    \   'sink': function('rsrchboy#fzf#FindOrOpenTab'),
    \   'options': '-m --prompt "Projects> "',
    \}, <bang>0))

What does this do?

  1. Defines a new vim command, :Projects

    No surprises here.

  2. Invokes fzf#run() to run a fzf search

    fzf#run() handles the actual execution and presentation of fzf, as well has dispatching the results back to the sink. fzf#wrap() is neat. It allows a command to take advantage of fzf.vim’s option handling – or not, by simply omitting it.

  3. Uses find to look for repositories

    We know roughly where to look(~/work/, ~/.vim/plugged) and how deep to look. Just about everything I do is backed by git, so we can look for repositories and return the parent of the found .git back to fzf.

    Note that the find invocation deliberately omits a -type d argument. I do use git worktrees, meaning .git may well be a file (a “gitlink”).

  4. Calls out to rsrchboy#fzf#FindOrOpenTab() with the project selected

    The sink option tells fzf#run() what to do with the results. In our case we have provided fzf#run() with a callback function, but you can also use built-ins as sinks.

The callback “sink” function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun! rsrchboy#fzf#FindOrOpenTab(work_dir) abort

    " loop over our tabs, looking for one with a t:git_workdir matching our
    " a:workdir; if found, change tab; if not fire up fzf again to find a file
    " to open in the new tab

    for l:tab in (gettabinfo())
        if get(l:tab.variables, 'git_workdir', '') ==# a:work_dir
            exe 'tabn ' . l:tab.tabnr
            return
        endif
    endfor

    call fzf#run(fzf#wrap('other-repo-git-ls', {
        \   'source': 'git ls-files',
        \   'dir': a:work_dir,
        \   'options': '--prompt "GitFiles in ' . a:work_dir . '> "',
        \   'sink': 'tabe ',
        \}, 0))

    return
endfun

In general, I use one tab per project (repository) in vim. For me, this is a nice balance of utility and sanity. It also allows me to do things like set t:git_dir and t:git_workdir to the git and workdir, respectively, of the repository associated with the tab.

Our callback function first attempts to find an open tab with the workdir requested; if found, it just switches to it and returns. (It should probably admonish me to read the tab line before invoking :Projects.)

If not found. the callback function invokes fzf#run() again. This time we use git ls-files to generate the source list for fzf, allowing us to pick a file to be opened by the given sink: tabe.

Hey, that wasn’t too hard!

Easier than writing this post, I’d say ;)

Happy hacking!

0%