Modern Vim Navigation with fasd and fzf

Just yesterday, I learned how not to hate CMake, after reading Henry Schreiner's excellent book, Modern CMake.

This book made me realize that Vim, like CMake, is decades old at this point and has a lot of plugins, snippets, configurations and setups scattered across the web. And, while I don't intend to write an authoritative source on what modern Vim should look like, I would like to share what I've learned after a decade of using both the awful and fantastic parts of Vim.

Today I want to look at file navigation. Getting Vim to open the files you want when you want them, and taking advantage of organized projects and directory structures instead of hating them.

The Vim Navigator's journey

Every Vim user goes through a file navigation journey along these lines:

  1. The Newb: Closing and reopening Vim every time you need to open a new file, through the command line.
  2. The Journeyman: Using :e and perhaps :cd.
  3. Plugin Enlightenment: NERDTree, vinegar.vim or some other file drawer/browser. IDE-like is good.
  4. Efficiency Epiphany: IDE-like is bad. The less I have to look at and think about the better. Some kind of fuzzy finder, I.E. fzf.vim, ctrl+p, Command-T, vim's built-in :vs **/*:
  5. Tag Nirvana: Gutentags, autotags, tag-browser, ctags, the works.

Yesterday, I found myself approaching Stage 5, after having been through every single one of these. I quickly found out there is a secret 6th level of enlightnment: LSP.

Before I go on, I encourage everyone to try out all of these methods (hence why I linked them), as they all have their own pros and cons, and you may find you like some better than others. Really, my setup is a combination of #3, #4 and my secret secret #6.

I suck at directory navigation

Everyone should evaluate their development environment every once in a while and decide what works and what doesn't. I realized that, in addition to a few other problems, I had trouble with directory navigation. Finding files was easy enough; fzf.vim does that fantastically and it's still my main driver, and has been for a while.

However, I had trouble jumping around my file system as a whole. I rely heavily on tab completions in the terminal, zsh's fish-style ctrl+f completion plugin, and when those fail me, I usually do this:

> cd .take/me/to/this/directory/
cd: no such file or directory: .take/me/to/this/directory/
> cd no\ wait/I/meant/this/directory
> cd ../../goddamnit
> vim

That's not very efficient. Furthermore, I would drop down to the terminal to use ripgrep any time I ever needed to find the definition of a symbol, or search for a string in a file. Then I'd use use fzf to navigate to that file once I figured out which one was the right one. This could also be improved.

Ranger, fasd, rg and fzf automate your job away

Ranger, fasd and fzf

At first, I thought trying to get a file browser like ranger to do that work for me, but I quickly realized that this was just wrapping up that cd turd in a pretty hjkl wrapper. The work was still repetitive, just less annoying. That being said, ranger is still part of this equation.

Then, I found out about fasd. fasd is like fzf but it's smart, remembers what you do (with Baynesian statistics!) and is meant to serve a specific purpose: Cut out the shitty parts of cd. It is meant as a command line tool, where it creates aliases like d fuzzy_dir_name, which will take you to the most-relevant directory, fuzzy_dir_name, as long as you've visited it at least once before.

So, fasd doesn't work in a vacuum. Luckily, ranger has a plugin that populates fasd with directories you visit! Furthermore, fzf can be used in the terminal to find files by using the shell extensions, which can be triggered with cd ctrl+t in the shell, or cd **<TAB> to change directories. Obviously it can also be composited with any other command too.

The reality is that, most of the time, I can just use fasd, as if I'm working in a directory, I've already visited it.


I still need to be able to find directories and files where I only know what the file contains, and have no idea where or what it might be. In the terminal, this can be done with rg thing | fzf | awk -F':' '{print $1}', which I've aliased to frg. In vim, this can be done with :Rg thing, a command that comes as a part of fzf.vim; I've bound this to <leader> frg.

If you aren't using ripgrep to search file contents, you're doing something wrong. It's stupid fast and I don't have to think at all about using it; it just works.

Bringing it all together

In summary:

And the best part: This workflow works both in Vim and the terminal. The muscle memory is different, but the behaviour is nearly identical:

action terminal vim
file browser ranger <leader>fF
find file/directory cd **<TAB> or vim **<TAB> <leader>ff
find in file frg <leader>frg
goto known file/directory z name to cd, f name for files <leader>fd for directories

In order to replicate this, you need to install the following packages:

Place the following in your init.vim:

" In plugin installation/configuration
Plug 'junegunn/fzf.vim' " And its vim bindings
let g:fzf_history_dir = '~/.local/share/fzf-history'
let $FZF_DEFAULT_COMMAND = 'rg --files --hidden'

Plug 'Lokaltog/neoranger'
let g:neoranger_viewmode='miller'

" bindings

nmap <leader>fr :History<CR>
nmap <leader>fF :RangerCurrentFile<CR>
nmap <leader>ff :Files<CR>
nmap <leader>fd :call fzf#run(fzf#wrap({'source': 'fasd -d -R', 'sink': { line -> execute('cd '.split(line)[-1]) }}))<CR>
nmap <leader>fgf :GFiles<CR> " For git files
nmap <leader>fb :Buffers<CR> " Search buffers
nmap <leader>frg :Rg 

And in your .zshrc/.bashrc/.aliases:

# Check and see if fasd is available before init
if [ -x "$(command -v fasd)" ]; then
  eval "$(fasd --init auto)"

export FZF_DEFAULT_COMMAND='rg --files --hidden'

CoC.nvim > Tags

I have an entire post coming about how awesome CoC.nvim is as a whole, but for now, I just want to fill in the void of IDE-like navigation. Many IDEs feature the ability to jump to various language-dependent symbols. For example, let's say I'm fixing a bug in the following trivial project that's causing problems:

// library.hpp
string giveMeHoneyBaby() {
  return babify("honey");
// library2.hpp
string babify(string obj) {
  return obj + " baby!";
// main.cpp
int main(int argc, char **argv) {
  cout << giveMeHoneyBaby();

Usually terminal text editors fill this void with tags. However, there is a better way: Language Servers provide language-specific implementations of many IDE-like features so that you can navigate and manipulate your code in a language-agnostic way. Whether you're working in PERL, Python or C++, your editor will behave the same as long as you have an available language server that supports those features.

With my current setup alone, I'd have to comb through ripgrep output to figure out where exactly my bug is. This would involve first rg giveMeHoneyBaby(), combing through library.hpp to understand the function, then rg babify(, then combing through library2.hpp. That, or I could look it up in the documentation, but not every project is documented that thoroughly.

With CoC.nvim, I just highlight giveMeHoneyBaby(), <leader>jd, read the function, highlight babify(...), <leader>jd, and that's it. No ripgrepping, very little combing, no dropping to terminal.

CoC.nvim provides a feature usually only found in IDEs called jump to definition. This does what it sounds like; it jumps to where a function is defined. It works for classes, methods, fields, etc. across any language with a Language Server.

CoC.nvim and LSPs have a few gems like this:

There's also the symbol list:

nnoremap <silent> <leader>js :<C-u>CocList -I symbols<cr>

This opens up a fuzzy finder on the list of all registered symbols available, including functions, fields, classes, etc.

If that isn't IDE-like functionality in vim, I don't know what is.


I've filled all the gaps in my Neovim file-navigation ability. I can discover new directories easily, I can return to directories I've discovered easily, I can navigate my projects in a language-aware manner, and I can do this in a way that interoperates between Vim and the terminal. Mission accomplished. Notice I never really touched on tags, because language servers just do it so much better.

This is my daily driver for working with any language in any project. I don't have to futz with language-specific plugins, I don't have to bother with configuring my completer, I usually have to do a few lines of configuration on a per-project basis (often this is just something like cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON), and I get IDE features, arbitrary, efficient system-wide file navigation and I can do all of this in the terminal and in Vim.