Video

Want to see the full-length video right now for free?

Notes

You can download a cheat sheet and install instructions for all of the tools shown in this video.

Transcript

In the first half of this tutorial, we'll learn about a couple of commands that let us quickly jump to files by name. In the second half, I'll demonstrate how to set up your environment to get the most out of these two commands.

What gf can do

Here I've got the source code for appraisal, which is an open source project from thoughtbot.

tree lib

Appraisal follows the standard conventions for laying out a rubygem: the lib directory contains contains both a file and a folder named after the project.

Let's crack open the lib/appraisal.rb file. This is our entry point to the library, and we can read it like a table of contents.

Watch this: if I place Vim's cursor on a reference to another file, I can jump to the filename under the cursor using the gf mapping. It's almost like clicking a hyperlink on a web page!

Each time I use the gf command, Vim records my location in the jumplist. I can go back to where I came from with the ctrl-o mapping. To extend the web browser analogy: gf is like clicking a link, and <C-o> is like pressing the back button.

  • appraisal/version
  • appraisal/task
  • appraisal/file

Each of the files that are prefixed in the appraisal namespace belong to the current project. But here you see that this project is requiring a module from a 3rd party library:

  • rake/tasklib

The gf mapping works here too: it opens the specified file from the rake library.

[FIXME: actually, gf opens rake from standard library, 2gf opens bundled rake.]

Let's back up with ctrl-o, then dive a little deeper into the appraisal project using gf each time.

  • appraisal/file
  • appraisal/appraisal

This file requires the fileutils module, which is part of the ruby standard library. Once again, if I use the gf command it goes straight to the specified file.

Using the :find command

The gf mapping is complemented by the :find Ex command. For example, if I run:

:find fileutils

Vim jumps to the specified module, just the same as if I had placed my cursor on the word fileutils and used the gf mapping.

One nice feature of the :find command is that it hooks in to Vim's tab-completion behaviour.

For example, if I type "find appraisal-slash" then press control-dee:

:find appraisal/<C-d>

the <C-d> mapping reveals a list of all the ways this command-line could be completed. Pressing the <Tab> key cycles through the list of suggestions.

If you are familiar with rails.vim, you may already be accustomed to using commands such as :Econtroller, :Emodel, :Eview, and so on. Under the hood, these commands invoke Vim's built-in :find command, but they focus their search on a subdirectory of your rails app. I LOVE those commands, and prefer using them to using fuzzy-file plugins when possible. For more details, look up :h rails-type-navigation

Stripped down to nothing

Let's strip everything down to the basics, by launching Vim with a minimal vimrc:

vim -u minimal-vimrc -o lib/appraisal.rb minimal-vimrc
  • -u tells vim to use the specified file as a vimrc
  • -o causes the other specified files to be open in split windows

The top split contains the ruby source code, and the bottom split contains the minimal vimrc file:

As well as disabling Vi compatibility mode, I've disabled plugins so as to turn off some functionality that comes built-in. This is just for demonstration purposes. We'll add that functionality again later.

Now watch what happens if I try and use the gf command. We get an error message:

Can't find file "..." in path

That seems to offer a hint: maybe if we configure the path option, then Vim will be able to find the specified file. But actually, that won't help in this case. The problem is that Vim is looking for a file called appraisal/version - that is, a file with no extension.

Here's a quick-and-dirty fix: we can change the require statement to include the .rb suffix. Save the change, and now the gf command works just fine.

That's valid ruby, but it's not idiomatic. If we were to submit this as a patch to an open-source project, it would likely get rejected. As a general rule: your source code should leave no clues as to which text editor was used to author it.

We can do better than this. Let's revert those changes.

Instead, we'll configure the suffixesadd setting:

:h suffixesadd

This instructs Vim to try appending a file extension to the text under the cursor when the gf mapping is invoked. Adding .rb to the list of suffixes should do the trick:

:set suffixesadd+=.rb

Now we can use the gf command. That's a much neater solution than changing the source code.

Set suffixesadd per filetype

Configuring the suffixesadd option by hand in this manner has a side-effect. Watch this: if we create a new file with a different filetype, (JavaScript in this case):

:new example.js

It uses the same value for suffixesadd as we set for the ruby files.

:set suffixesadd?
   suffixesadd=.rb

That's not going to be very helpful when working with javascript.

Instead of setting the option globally by hand, we'll use an autocommand to set the suffixesadd option locally for each buffer containing ruby code. In this snippet of Vimscript:

augroup rubypath
  autocmd!
  autocmd FileType ruby setlocal suffixesadd+=.rb
augroup END

The setlocal command scopes the command to the current buffer. The autocommand triggers this setting each time we open (or create) a ruby file. But if we create a JavaScript file the autocommand will not apply. Of course, we could use a similar technique to configure Vim in a way that would be useful for JavaScript files, as well as other filetypes too.

Having saved those changes to our minimal-vimrc, let's quit and restart Vim. The gf command works in our ruby file, because the suffixesadd option includes the .rb extension, but if I create a JavaScript file, the suffixesadd setting is unaffected.

Nice one! We'll use this pattern again to apply local settings to ruby files.

Setting the path

Configuring the suffixesadd option makes the gf mapping work for the require statements in the lib/appraisal.rb file, but it turns out that's a bit of a fluke. The gf command still doesn't work for the require statements in other files. Once again, we're getting the error message:

Can't find file "..." in path

Now let's dig in to that path setting and see what's going on. We can inspect it by running:

:set path?
  path=.,/usr/include,,

That reveals a comma separated list containing three items. I've created a custom command :Path (with a big P):

:Path

that lists the paths one per line, which is a little easier to read.

  • the dot stands for the directory of the current file
  • the absolute path instructs Vim to search the /usr/include directory, and
  • the empty string stands for the current working directory

When we invoke the gf command, Vim searches each of these locations for a file who's path matches the text under the cursor. If successful, Vim opens the file at that path. Otherwise it displays an error message.

Let's walk through the hit and miss scenarios we've just witnessed.

go to file in path: hit

First, we opened the file lib/appraisal.rb and invoked the gf mapping on the appraisal/task string.

Current file:
    lib/appraisal.rb
<cWORD>:
    appraisal/task

Vim searches for the filepath appraisal/task.rb in each of the locations specified in the path option. It starts by looking relative to the directory of the current file (CFD - current file directory):

'.' - CFD
./lib/
    appraisal/task.rb

First time lucky! Vim opens the matching file at lib/appraisal/task.rb.

go to file in path: miss

Now we're in a different context: the path of the current file becomes: lib/appraisal. Here's what happens when we invoke gf on the appraisal/file string:

Current file:
    lib/appraisal/task.rb
<cWORD>:
   appraisal/file

First, Vim searches relative to the directory of the current file (CFD):

'.' - CFD
./lib/appraisal/
                appraisal/task.rb

Finding no match, Vim then looks in the /usr/include directory:

'usr/include'
/usr/include/
         appraisal/task.rb

Finally, Vim looks relative to the current working directory:

''  - cwd
./
  appraisal/task.rb

Having run out of paths to search, Vim shows an error message.

Setting the path for the current project (path+=lib)

We could fix this by adding the lib directory to Vim's path. That should match any of the files in the appraisal project, regardless of what the current file directory is.

[pause]

Let's do it! We'll use an autocommand to add the lib directory to Vim's path when working on ruby files:

autocmd FileType ruby setlocal path+=lib

We'll save those changes to our minimal-vimrc and relaunch Vim.

We can now use the gf command on any require statements that reference local files. Pretty neat!

But the gf command still doesn't work on require statements that reference third party code, such as the rake gem, or modules from the standard library, such as fileutils.

Let's look at how to fix those, one by one.

Browsing bundled gems

At present, the gf command fails when invoked on the string rake/tasklib. We should be able to fix that by updating our path to reference the lib directory of the bundled rake gem.

[suspend Vim: ctrl-z]

The bundle show command can reveal the path where a gem has been installed:

bundle show rake
/Users/drew/.rvm/gems/ruby-1.9.3-p374/gems/rake-10.1.0

If we inspect the lib directory within, we should find the tasklib file that was referenced in our code:

tree /Users/drew/.rvm/gems/ruby-1.9.3-p374/gems/rake-10.1.0/lib
├── rake
│   ...
│   ├── tasklib.rb
│   ├── testtask.rb
│   ...
└── rake.rb

[foreground Vim: fg]

Once again, we'll use an autocommand to add this directory to Vim's path setting:

autocmd FileType ruby setlocal path+=/Users/drew/.rvm/gems/ruby-1.9.3-p374/gems/rake-10.1.0/lib

We'll save those changes to our minimal-vimrc and relaunch Vim.

This time, when we run gf on the string rake/tasklib it takes us straight to the file in the bundled gem.

[suspend Vim: ctrl-z]

Now we can use Vim's gf mapping on any statement that requires a module from the rake library. But this project bundles over a dozen other rubygems:

bundle show --paths

If we added to Vim's path the lib directory for each of these, then we could dive into the code of all bundled gems using the gf and :find commands.

Browsing standard library

The gf command still doesn't work on modules from the standard library.

Let's fix that!

[suspend Vim: ctrl-z]

We can inspect Ruby's loadpath by running this one-liner:

ruby -e 'puts $LOAD_PATH'
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/site_ruby/1.9.1
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/site_ruby/1.9.1/x86_64-darwin12.2.1
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/site_ruby
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/vendor_ruby/1.9.1
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/vendor_ruby/1.9.1/x86_64-darwin12.2.1
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/vendor_ruby
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1
/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1/x86_64-darwin12.2.1

[Note: $: is a shorthand for $LOAD_PATH]

If we were to add each of these directories to Vim's path, then the gf command should be able to dive into all modules in the standard library.

I happen to know that the file we're looking for is in the ruby/1.9.1 directory.

[foreground Vim: fg]

So let's add that directory to our path:

autocmd FileType ruby setlocal path+=/Users/drew/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1

Save our minimal-vimrc and restart Vim... Now the gf command can successfully locate the fileutils module. Hurray!

Convenience: broaden the gf target to include require

Watch this: if I use the gf command on the word require, Vim attempts to locate a file called require.rb. To open the file that's actually being required, we have to position the cursor on top of the filepath. It's not a big deal, but it would be handy if the gf command went to the referenced file when invoked from the start of the line.

Setting the path automatically

Our autocommands configure Vim's path option for ruby files in this project, but we'll need a more flexible approach to manage Vim's path for multiple projects. Let's fire up Vim again, without using the minimal-vimrc:

vim -o lib/appraisal.rb minimal-vimrc

Now we're using my personal Vim configuration, which includes a few plugins that I'd recommend to all rubyists:

  • vim-ruby
  • vim-bundler, and
  • vim-rake

The vim-ruby plugin automatically configures the path setting to include each of the directories listed in ruby's $LOAD_PATH.

:Path

Even if you haven't installed any plugins, you probably already have vim-ruby, because it's included in the standard Vim distribution. Even so, I'd recommend installing it by hand to ensure you get the most up to date version.

The vim-ruby plugin includes an enhancement for the gf command. I can now position the cursor on the require keyword, and gf behaves as though the cursor were positioned on the required filepath. That's a bit more useful than the default behavior, which was to attempt to locate a file called require.rb.

The vim-bundler plugin automatically configures the path setting to include the lib directory for each gem referenced in your Gemfile.

:Path

The vim-rake plugin automatically configures the path setting to include the lib directory for libraries that follow the conventional layout of a rubygem. It's smart about configuring each buffer's path relative to its own codebase, so you can interact with more than one library at once:

  • open two splits, with appraisal/task.rb and rake/tasklib.rb (use bundled gem, not stdlib rake)
  • [use :find rake/tasklib]
  • run :Path in each split

These 3 plugins work together to ensure that Vim's path includes all of the directories that your ruby project knows about.

Rails.vim

If you work with rails, then Tim Pope's rails.vim should be high on your list of essential plugins. This plugin automatically configures your path to include the directories that make up a conventional rails app. (I encourage you to explore it for yourself!) For more details, check out thoughtbot's Vim for Rails Developers screencast.

Beware of load order

A handful of popular ruby libraries, such as minitest and rake, may be included in your ruby distribution. If you prefer to bundle these gems, so as to get a more up to date version of them, then you may have to take extra care when using the gf and :find commands, because both versions of the library will appear in your path.

[open lib/appraisal/task.rb in two split windows]

Watch this: in this first split, I'll use gf. Now I'm going to switch to the other split and use the mapping 2gf. That tells Vim to skip the first match and jump to the second one instead. At a glance, these two buffers look identical, but each one has a different filepath. The first is from the ruby distribution, and the second is from the bundled gem. In each window, if we look up the version:

:windo find rake/version

We'll see that the bundled gem is more recent.

Let's back up through the jumplist until we get back to where we started. Inspecting the path, you'll see that the core libraries appear earlier in the list than the bundled gems. That's why plain gf (without the count) goes to the older version of rake. In this context, we need to use 2gf to jump to the bundled version of rake.

That's worth watching out for, as it could lead to confusion!

Outro

Compared with rails.vim, the feature list for bundler.vim is quite brief. Blink, and you could miss the bullet point saying that:

'path' and 'tags' are automatically altered to include all gems from your bundle

We'll learn more about the tags option in the final part of this series. But I hope that this video has convinced you that having your path properly configured makes a big difference, and this actually a killer-feature of bundler.vim!