Want to see the full-length video right now for free?
You can download a cheat sheet and install instructions for all of the tools shown in this video.
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.
gf
can doHere 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.
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:
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.
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.
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
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 windowsThe 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.
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.
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.
/usr/include
directory, andWhen 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.
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
.
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.
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.
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.
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!
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.
path
automaticallyOur 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:
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:
appraisal/task.rb
and rake/tasklib.rb
(use bundled gem, not stdlib rake)These 3 plugins work together to ensure that Vim's path includes all of the directories that your ruby project knows about.
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.
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!
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!