FuzzyFinder Teaser Functions (for finding Files)

Posted by Derek Wyatt on August 25, 2009

I’ve been playing with FuzzyFinder recently. I’ve had it on my list of “things to understand better” for a while now, but I’ve also had a couple of people asking smart questions like this:

“Now that I am only using One Vim, how do you expect me to manage this jungle of files and buffers I’ve wound up with?”

Well, an app like FuzzyFinder is great for helping you tame that particular beast. I’ll be putting up a video tutorial about it once I’ve got a better handle on the ins and outs of it, but for now, I’ll give you a little teaser on how it can make life a bit better. Let’s say you’ve got a code layout like this (which is a convention I’ve luckily managed to see happen where I work):

/root/of/code/tree/component_name
/root/of/code/tree/component_name/include/{directory per namespace}
/root/of/code/tree/component_name/src
/root/of/code/tree/component_name/test
/root/of/code/tree/component_name/test/include
/root/of/code/tree/component_name/test/src

You can probably imagine that I bounce around the component_name/include/{whatever}, and the component_name/src and the component_name/test/src areas a lot (I always write tests as pure implementation files - no headers). The FSwitch plugin helps a lot for switching between the header and the cpp file but that’s not all you want to do… For the rest, FuzzyFinder is what’s useful. However, even FuzzyFinder is a pain right out of the box because it’s really not much better than just using the command-line. Yeah it’s a bit smarter but the initial navigation is still a pain. But it would be really snazzy if you could just give it a hint to get it started, and I’ve written a few functions to help out with that:

"
" SanitizeDirForFuzzyFinder()
"
" This is really just a convenience function to clean up
" any stray '/' characters in the path, should they be there.
"
function! SanitizeDirForFuzzyFinder(dir)
    let dir = expand(a:dir)
    let dir = substitute(dir, '/\+$', '', '')
    let dir = substitute(dir, '/\+', '/', '')

    return dir
endfunction

"
" GetDirForFF()
"
" The important function... Given a directory to start 'from', 
" walk up the hierarchy, looking for a path that matches the
" 'addon' you want to see.
"
" If nothing can be found, then we just return the 'from' so 
" we don't really get the advantage of a hint, but just let
" the user start from wherever he was starting from anyway.
"
function! GetDirForFF(from, addon)
    let from = SanitizeDirForFuzzyFinder(a:from)
    let addon = expand(a:addon)
    let addon = substitute(addon, '^/\+', '', '')
    let found = ''
    " If the addon is right here, then we win
    if isdirectory(from . '/' . addon)
        let found = from . '/' . addon
    else
        let dirs = split(from, '/')
        let dirs[0] = '/' . dirs[0]
        " Walk up the tree and see if it's anywhere there
        for n in range(len(dirs) - 1, 0, -1)
            let path = join(dirs[0:n], '/')
            if isdirectory(path . '/' . addon)
                let found = path . '/' . addon
                break
            endif
        endfor
    endif
    " If we found it, then let's see if we can go deeper
    "
    " For example, we may have found component_name/include
    " but what if that directory only has a single directory
    " in it, and that subdirectory only has a single directory
    " in it, etc... ?  This can happen when you're segmenting
    " by namespace like this:
    "
    "    component_name/include/org/vim/CoolClass.h
    "
    " You may find yourself always typing '' from the
    " 'include' directory just to go into 'org/vim' so let's
    " just eliminate the need to hit the ''.
    if found != ''
        let tempfrom = found
        let globbed = globpath(tempfrom, '*')
        while len(split(globbed, "\n")) == 1
            let tempfrom = globbed
            let globbed = globpath(tempfrom, '*')
        endwhile
        let found = SanitizeDirForFuzzyFinder(tempfrom) . '/'
    else
        let found = from
    endif

    return found
endfunction

"
" GetTestDirForFF()
"
" Now overload GetDirFF() specifically for
" the test directory (I'm really only interested in going
" down into test/src 90% of the time, so let's hit that
" 90% and leave the other 10% to couple of extra keystrokes)
"
function! GetTestDirForFF(from)
    return GetDirForFF(a:from, 'test/src/')
endfunction

"
" GetIncludeDirForFF()
"
" Now specialize for the 'include'.  Note that we rip off any
" '/test/' in the current 'from'.  Why?  The /test/ directory
" contains an 'include' directory, which would match if we 
" were anywhere in the 'test' directory, and we don't want that.
"
function! GetIncludeDirForFF(from)
    let from = substitute(SanitizeDirForFuzzyFinder(a:from),
                        \  '/test/.*$', '', '')
    return GetDirForFF(from, 'include/')
endfunction

"
" GetSrcDirForFF()
"
" Much like the GetIncludeDirForFF() but for the 'src' directory.
"
function! GetSrcDirForFF(from)
    let from = substitute(SanitizeDirForFuzzyFinder(a:from),
                        \  '/test/.*$', '', '')
    return GetDirForFF(from, 'src/')
endfunction

Now, you can use these by making some mappings. I have the following three:

nnoremap ,ff :FuzzyFinderFile<cr>
nnoremap ,ft :FuzzyFinderFile <c-r>=GetTestDirForFF('%:p:h')<cr><cr>
nnoremap ,fi :FuzzyFinderFile <c-r>=GetIncludeDirForFF('%:p:h')<cr><cr>
nnoremap ,fs :FuzzyFinderFile <c-r>=GetSrcDirForFF('%:p:h')<cr><cr>

Clearly this requires that you’re already working on a file somewhere inside the /root/of/code/tree/component_name directory but that’s my convention most of the time when working on a library so this works great for me. Cheers!