Josh Clayton

Automatic Projections for Quick Vim Navigation

rails.vim is a Vim plugin allowing for templated file creation and quick navigation to areas of a Rails codebase that are context-based (e.g. navigating to a controller or migration) rather than filename-based. With tab-completion, one could navigate to e.g. app/models/person.rb in Vim by running :Emodel person (which tab-completes!)

While there are built-ins for common Rails navigation in rails.vim, I found myself:

  • looking for additional Rails navigation
  • wishing for a way to navigate similarly in non-Rails applications

projectionist.vim

projectionist.vim is a Vim plugin that brings templates, commands, and file patterns forward in a more abstract way via a configurable JSON format.

Imagine JSON configuration for a more agnostic RSpec project, decoupled from Rails, that lives in $APP_DIR/.projections.json:

{
  "spec/*_spec.rb": {
    "command": "spec",
    "template": [
      "require \"spec_helper\"",
      "",
      "RSpec.describe {camelcase|capitalize|colons} do",
      "end"
    ]
  }
}

Within vim, then, one could type :Espec thing and be taken to an existing spec/thing_spec.rb. Adding :Espec thing! would create that file using the template provided, with appropriate string transformations (so, the third line of the file would describe Thing).

Building JSON via Composable Project Types

The problem I face is I work with a few different languages and frameworks. While most of my time is spent in Rails, there are a handful of other types of projects I regularly work in.

I was looking for tooling that would build up these JSON files based on the project type, ideally in a composable way (e.g. Ruby + RSpec + Rails, assuming all project types were applicable), but nothing existed. I set out to build something from scratch, with three areas I'd identified:

  1. A directory of JSON files - one per project type - that could be combined
  2. A script to determine, based on a directory structure, what project type(s) were relevant
  3. A hook into zsh that would run this script and, if a .projections.json was generated, save to disk

The Directory of JSON files

I found building the JSON files for projectionist.vim the most straightforward portion of this exercise. It seems unlikely there would be name conflicts around e.g. "routes" as you wouldn't have a Rails and Phoenix app coexist in the exact same directory.

My list of JSON files can be found in my dotfiles.

Composing JSON Payloads

Determining applicable project types is naive and based on presence of particular files. While rudimentary, it suits my needs.

The mechanisms for this are:

  1. build an empty list of project types
  2. look for various different files, and push the corresponding project type onto the list
  3. map over these project types to the actual JSON files from the projections directory
  4. use jq to reduce the JSON files into a single JSON payload and write to STDOUT

Roughly speaking, the net result is something like this:

#!/usr/bin/env bash

PROJECTIONS_PATH="$HOME/.projections"

list=()

if [ -f "mix.exs" ]; then list+=('elixir'); fi
__router=(lib/*/router.ex lib/router.ex)
if [ -f "app/Main.hs" ]; then list+=('haskell'); fi
if [ -f "Rakefile" ]; then list+=('rake'); fi
if [ -f "spec/spec_helper.rb" ]; then list+=('rspec'); fi
if [ -f "config/routes.rb" ]; then list+=('rails'); fi
__gemspec=(*.gemspec)
if [ -f "${__gemspec[0]}" ]; then list+=('ruby'); fi

files=''
count=0
while [ "x${list[count]}" != "x" ]; do
  files="$files $PROJECTIONS_PATH/${list[count]}.json"
  count=$((count + 1))
done

if [ $count -eq 0 ]; then exit 1; fi

jq -s 'reduce .[] as $item ({}; . * $item)' $files

The source of this file can be found in my dotfiles.

Hooking into Zsh to Write JSON on cding into a Directory

The final step in wiring this together is hooking into zsh's navigation to build JSON files automatically.

zsh makes this possible with the chpwd function.

The relevant JSON generation machinery for zsh is:

function chpwd {
  local v=$(projections)
  if [[ $? -eq 0 ]]; then
    echo $v > .projections.json
  fi
}

The source of this can be found in my dotfiles.

Wrapping Up

Now, every time I cd into a project, bin/projectionist gets run, generating a new .projections.json file. This ensures the commands made available via projectionist.vim are always relevant, making working with different types of projects a breeze!