Optimizing your Workflow with Vim

Optimizing your Workflow with Vim

Jerome Goodrich
Jerome Goodrich

January 17, 2017

When I first started working in Vim, reading Bram Moolenaar's Seven Habits of Effective Text Editing was very helpful. In it, he outlines three fundamental tips for gradually improving one's efficacy in the editor.

  1. While you are editing, keep an eye out for actions you repeat and/or spend quite a bit of time on.

  2. Find out if there is an editor command that will do this action quicker. Read the documentation, ask a friend, look at how others do this.

  3. Train using the command. Do this until your fingers type it without thinking.

These are so basic and simultaneously profound. In Vim there is always a quicker way to do things, and that doesn't just apply to editing text in a conventional sense—any adjacent processes can also be optimized.

Consider the process of TDD. There are plenty of ways we can use Vim to make the Red, Green, Refactor loop as tight and fast as possible. Let's take a look at each stage of the cycle and dive into some of those potential optimizations.

RED - Writing a Failing Test

All test suites require some basic boilerplate code. In the case of BDD style frameworks like Rspec and Speclj, we know we are going to be frequently writing code that looks like this:

(describe "The App"
		(context "Starting it up"
				(it "loads the thing we expect it to load"
						(let [something {:this "and that"}]
								(should= something something_else)))))

This isn't too much to write for one test, but as our suite grows we will be repeating it quite a bit. Let's make things easier by writing some basic snippets that will significantly reduce the amount of repeated boilerplate. One really easy way to do this is by creating some abbreviations in our .vimrc file.


iabbrev dci (describe " " <<CR>> (context " " <<CR>> (it " " <<CR>> #body here#)))

Now, by simply typing dci in insert mode, we insert all the boilerplate for the next test.

(describe " "
		(context " "
				(it " " #body here#)))

Considering we will probably want multiple it blocks per context and multiple context blocks per describe, we might also add the following abbreviations:

:iabbrev ci (context ""<CR>) (it ""<CR>) #body here#))
:iabbrev ib (it ""<CR>) #body here#))

We can wrap our abbreviation in a Vim autocommand to specify in which types of files these abbreviations work.

"" au is the shortened form of autocmnd
:au FileType clojure :iabbrev dci (describe " " <CR> (context " " <CR> (it " " <CR> #body here#)))
:au FileType ruby :iabbrev dci describe " " do <CR> context " " do <CR> it " " do #body here# <CR> end <CR> end <CR> end

By combining our abbreviations with autocommands, we can ensure that our abbreviations are only expanded in the file type we want. Also, we can now have the same abbreviation for multiple file types, but with different behavior.

Abbreviations work well for creating quick, on the fly expansions for text you write frequently; but for boilerplate code, they are, at best, a stop-gap solution. Much better options exist in the form of Plugins like SnipMate and UtiliSnips, which were explicitly created to help Vim users quickly and easily create small reusable "snippets" of code. I recommend starting off with the abbreviations first—they might turn out to be more than enough.

GREEN - Passing The Failing Test

Alright! We've now written our first failing test. How can we get to 'Green' as quickly as possible? First, we need to actually determine that our current test failed, which means two things.

  1. We want the time between writing our tests and running them to be as short as possible.
  2. We want to verify that our test failed with minimal effort.

This is a common workflow I've seen and have used before:

  • write a failing test
  • hit ctrl z to 'pause' Vim and go back to the terminal
  • type in test command, something like lein spec or hit the up arrow to get the most recent command
  • hit enter
  • wait for the test to run
  • look at the results
  • hit fg to return to Vim
  • probably switch back and forth a couple more times to reference the error message.

Not bad, but there's a lot of potential for error, and all things considered, this approach is pretty slow. Ideally, our tests should already have been run before we even have had time to think. Also, wouldn't it be nice to be able to see our failing test output without needing to switch contexts all the time? With the aid of a terminal multiplexer like tmux (though any will do), we can do both without much fuss. Using tmux split-panes, you can get code and test output on the same screen simultaneously. That's a huge step forward from the workflow described above, but we can still make it better.

It can be difficult to switch between Vim and tmux commands. I find many tmux defaults unintuitive and prefer to have a consistent experience across both programs. By adding the following lines to my .tmux.conf, I can solve this problem and use Vim-like navigation in tmux. I simply have to prepend each direction with the tmux prefix key.

setw -g mode-keys vi
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

Even though we've made moving around tmux a breeze, we don't want to have to switch over to our other tmux pane just to run our tests. Ideally, we should never have to leave Vim. With only a glance, we ought to be able to see the error message, and then go right back to working on making that test pass.

Creating A Test Autorunner

I like for my tests to run as soon as I save the file I'm currently editing. In Vim-speak that's called writing to the buffer, an autocommand event. Vim includes a multitiude of these types of triggers, including the FileType event that we saw when talking about abbreviations and snippets. BufWrite is what we'll use as our autocommand event, but if you prefer a different workflow, there are many options. You can see the full list of autocommand events by typing :help autocommand-events when in Vim.

Let's say we are working in Clojure and are unaware that Speclj has a -a option for auto-running our tests. Here's an autocommand we can use to replicate that functionality.

autocmd BufWrite *.clj :silent exec '!tmux send-keys -Rt 1 C-l "lein spec" Enter' | redraw!

We already know what BufWrite is about, but there's a lot going on here, so let's walk through the rest of it.

*.clj

This says to only run this autocommand when the file has a .clj extension

:silent exec

:silent runs the command without making us leave Vim and hit enter afterwards. exec tells Vim to evaluate the proceeding string as a command.

!tmux send-keys

This is the beginning of our command. First, ! puts us in the terminal, meaning whatever we write after it is executed by whatever shell we are running. This is equivalent to switching to the terminal and typing in the tmux send-keys command

-Rt 1

-t is an option passed to the tmux send-keys command -- it specifies to which pane the proceeding keys must be sent. -R will reload the pane prior to "lein spec" Enter being sent, so our tests are always run on a fresh screen.

"lein spec" Enter

These are the keys we are actually sending to pane we specified with the -t option. "lein spec" Enter is the command we would normally input into the terminal to run our tests.

| redraw!

Finally, | redraw! will clear the current Vim screen and redraw it. This is helpful because sometimes Vim can get in a funky state after doing an external command.

Awesome! Now whenever we write to a buffer, our tests will automatically run and we need only to check our periphery to know whether they pass or fail.

NB: Some people prefer using vim-dispatch in order to have more screen real-estate.

Quick Aside On Vim's "!" Operator

Vim is a Unix style program. It does one thing, text editing, exceedingly well. However, because it can interact with the shell via !, it also has access to anything you can do in the shell. Just as we are delegating to a test runner and tmux to run our tests, we can do something similar with any other command line application. For example, we could readily configure some sort of git workflow in which we never have to leave Vim!

Refactor - Stay Green, but Make Things a Little Nicer

We are green! Now, how can we make our code better?

Project-Wide Search and Replace

Through the course of a project, or even a day, a variable or function name may be renamed several times. This common refactoring is Vim's bread and butter. It has a very powerful substitution command, :s, that makes quick work of most renaming needs. For example, :%s/old-pattern/new-pattern/g replaces all occurrences of old-pattern with new-pattern in the current file. Furthermore, like many popular IDEs, Vim has a facility for quickly making changes across multiple files.

:grep foo
:cdo %s/foo/bar/g | update

:grep foo will find all instances of foo in your project, and populate them in what is called the quickfix window. :cdo then takes a command to apply to each instance in that window. In our case, the command we want to apply is a global substitution of foo with bar, but it could be anything. After making changes, we want those changes to take effect, so we use | update. Voila! With two quick commands we've managed to rename every instance of a variable or function in our entire project.

NB: The quickfix window isn't normally visible. To see it just use the :copen command. Also, Steve Losh has an awesome guide for creating a :grep "operator" in Vim. I highly recommend you check it out.

Wrapping Things Up

Vim is very well suited to help software professionals optimize their craft. For things that we do everyday, like TDD, it's helpful to take some time upfront to consider changes that can improve our approach. Hopefully, some of the things we discussed above inspire you to take a critical look at your current workflow and try out some new things.

Even if you aren't someone who uses Vim regularly, Bram's suggestions still apply. Get in the habit of not only identifying repetition, but actively working to reduce it. That might be just taking note after it dawns on you and then scheduling some time later to find a better way, or just stopping whatever you're doing and finding a better way right then and there. The point is, as professionals we should never be complacent, but instead always striving to find better ways of doing things.