GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Write a Vim Plugin with TDD

My colleague Chris Toomey and I are writing a Vim plugin called vim-spec-runner that runs RSpec and JavaScript tests. We’re test-driving it and learned a lot.

Vimrunner

We’re using the excellent vimrunner Ruby gem. It provides a Ruby interface to run arbitrary commands in Vim by hooking into Vim’s client-server architecture. If Vim was compiled with +clientserver, you can launch a Vim process that acts as a command server, with a client that sends commands to it. Vimrunner launches a Vim server and then sends your commands to it.

We discovered that terminal Vim doesn’t work with vimrunner, but MacVim works perfectly. Vimrunner will pick up MacVim if it’s installed, so all you have to do is brew install macvim. If you’re using Linux/BSD, it might work out of the box, but we haven’t tried it.

Here’s how to use Vimrunner to sort a file:

VimRunner.start do |vim|
  vim.edit "file.txt"
  vim.command "%sort"
end

Vimrunner also comes with some neat RSpec helpers. You can look at vim-spec-runner’s full spec_helper, but here are the important bits:

require "vimrunner"
require "vimrunner/rspec"

ROOT = File.expand_path("../..", __FILE__)

Vimrunner::RSpec.configure do |config|
  config.reuse_server = true

  config.start_vim do
    vim = Vimrunner.start
    vim.add_plugin(File.join(ROOT, "plugin"), "spec-runner.vim")
    vim
  end
end

First, we require vimrunner and vimrunner/rspec, for RSpec support. Then we set the ROOT constant to the directory containing the plugin directory. We then configure Vimrunner’s RSpec integration:

  • config.reuse_server = true: Use the same Vim instance for every spec.
  • config.start_vim: This block is used whenever Vimrunner needs a new Vim instance. We’re using it to always add our plugin to the vim instance.

Customizing RSpec

We use RSpec’s custom matchers quite a bit in our specs. They reduce the amount of code we have to write and make our tests much more readable.

Here’s an example:

it "does not create a mapping if one already exists" do
  using_vim_without_plugin do |clean_vim|
    clean_vim.edit "my_spec.rb"
    clean_vim.command "nnoremap <Leader>x <Plug>RunCurrentSpecFile"
    load_plugin(clean_vim)

    expect(clean_vim).to have_no_normal_map_from("<Leader>a")
  end
end

using_vim_without_plugin and load_plugin are both plain Ruby methods defined in our main spec file. Here’s the have_no_normal_map_from matcher:

RSpec::Matchers.define :have_no_normal_map_from do |expected_keys|
  match do |vim_instance|
    mapping_output(vim_instance, expected_keys) == 'No mapping found'
  end

  failure_message_for_should do |vim_instance|
    "expected no map for '#{expected_keys}' but it maps to something"
  end

  def mapping_output(vim_instance, expected_keys)
    vim_instance.command "nmap #{expected_keys}"
  end
end

The outer block variable, expected_keys, is what we pass to expect, while the block variable for match is what we pass to the have_no_normal_map_from method.

We define failure_message_for_should so that if there is a mapping, we get a useful, human-formatted error message.

Travis CI

It took some work, but we got our plugin running on Travis CI. Vimrunner needs a Vim that was compiled with +clientserver, so we install the vim-gnome Ubuntu package. Vimrunner also needs an X server, so we use xvfb to start up a headless X environment. Here’s the result:

before_install:
  - "sudo apt-get update"
  - "sudo apt-get install vim-gnome"
  - "vim --version"
install: bundle
script: xvfb-run bundle exec rspec --format documentation

We use the --format documentation option to RSpec so we can see exactly which test failed on Travis. You can see our full .travis.yml file here and a sample test run here.

What’s next?

Try out vim-spec-runner! If you want to add tests to your own Vim plugin, check out the full spec file.