Twitter GitHub Facebook Instagram dirv.me

Daniel Irvine on building software

Generating source and spec files in Vim with UltiSnips and Projectionist

23 February 2015

Noah Frederick’s post File Templates with UltiSnips and Projectionist recently inspired me to wire up “generate” functionality for creating source and test file pairs within Vim.

For any TDDist, source files come in pairs: the primary source file, and its accompanying test file. Creating a new class in Java, for example, often (but not always) requires creating a test class first and subsequently, once the first test is written, the new class under test.,

In Clojure, I use Speclj for testing. The rest of this post looks at that in particular, but this technique isn’t language specific.

If my desired Clojure namespace is foo/bar then I end up initializing a source file src/foo/bar.clj with this content:

(ns foo-clj.core
  )

And also a spec file spec/foo/bar_spec.clj that looks like this:

(ns foo-clj.core-spec
  (require [speclj.core :refer :all]
           [foo-clj.core :refer :all]))

In my last post I showed how I use UltiSnips to initialize the content of these files using expanding snippets, using VimL functions to fill in the correct namespace strings. In addition to UltiSnips, I also use Projectionist to create test/source file pairs.

Projectionist simplifies creating source files of given project types. You need to configure it initially, but the general idea is that it saves you typing out full file paths if it can make some assumptions about your project structure.

For the Clojure example above, I can type :Esrc foo/bar to have it create src/foo/bar.clj (assuming that I have configured that to happen—see below).

Projectionist also supports alternate files, so I can easily pair up src and spec files for the languages that I use test-driven development for. :Etest foo/bar takes me to spec/foo/bar_spec.clj, and then typing :AV will take me back to src/foo/bar.clj in a new split.

Projectionist configuration

This is my configuration, which works for standard Java Maven projects and Clojure projects:

let g:projectionist_heuristics = {
      \ "src/**/*.java" : {
      \   "src/main/java/*.java": {"alternate": "src/test/java/{}.java",
      \                            "type": "src"},
      \   "src/test/java/*.java": {"alternate": "src/main/java/{}.java",
      \                            "type": "test"},
      \   },
      \ "project.clj" : {
      \   "src/*.clj": {"alternate": "spec/{}_spec.clj",
      \                 "type": "src"},
      \   "spec/*_spec.clj": {"alternate": "src/{}.clj",
      \                       "type": "test"}
      \   }
      \ }

With this config I can use the Ex command :Esrc foo/bar or :Etest foo/bar and Projectionist will instantiate files in the right places depending on the current project type, either Java or Clojure.

Putting it together: UltiSnips and Projectionist

I defined the following function and user command that creates both source and test files using Projectionist, and then uses UltiSnip to populate the files with initial content. Finally it calls :AV which opens the alternate file—the test file—in a vertical split. Since the test file is opened last, it ends up with focus.

function! CreateTestAndSrcFile(file)
    exec "Etest " . a:file
    normal itest	
    w
    exec "Esrc " . a:file
    normal ins	
    w
    AV
endfunction

command! -nargs=1 -complete=dir CreateBoth :call CreateTestAndSrcFile("<args>")

The upshot of all this is that I can type :CreateBoth foo/bar and have src/foo/bar.clj and spec/foo/bar_spec.clj created with the correct content, as shown below.

Sample Clojure content

This has worked well for me so far, and I hope this post encourages you to give it a go too.

About the author

Daniel Irvine is a software craftsman at 8th Light, based in London. These days he prefers to code in Clojure and Ruby, despite having been a C++ and C# developer for the majority of his career.

For a longer bio please see danielirvine.com. To contact Daniel, send a tweet to @d_ir or use the comments section below.

Twitter GitHub Facebook Instagram dirv.me