February 21, 2017

CTags in Ruby: Make code feel like a wiki

In a browser you markup for links. A wiki builds on this behavior to describes topics, linking nouns to pages in context that link to other nown pages.

Browsing a wiki allows you to dive into information and build up your mental model of a subject area– which is kind of addictive. So why can’t we do the same thing in our code?

Code has syntax which are just links between files. We often do this linking in our heads, but our editor should be helping you out more– at some stage there’s a black box that you’ve never stepped into because it’s outside of your project.

There have been attempts to make code behave like links through static and dynamic analysis. Here are some of the projects on my radar that pull this off:

  1. Resource Navigation integrated throughtout the Eclipse IDE

  2. Ctags integration for vim

  3. Go Guru integration via the vim-go plugin

  4. Sourcegraph integartion via the vim-sourcegraph plugin

If you’re hacking on an opensource project, then sourcegraph is the natural choice at the moment as it gives you access to community and examples you may not have even checked out.

However I’m a Rubyist on a closed source thing at work, and I don’t think I can convince my managers just yet to shell out for a sourcegraph subscription. This leaves me with ctags.

Ctags create a “tags” file which Vim knows how to navigate out of the box with Ctrl + ]. Those tags are a snapshot of static analysis mapping the links between files in your project.

If you don’t want to mess up your project files though, and you have vim-fugitive, then I recommend you specify your tags file location in .git/tags.

Initially I found plugins that ran this command on every file write. However this quickly became unweildy as it builds a massive head of background procs that turn your responsive computer into a slow hulking mass of rage.

A better way to think about this is “when you think code is ready enough to share”. This happens for me when I run git commands, specifically git commit and git checkout.

You can run actions post by adding executable scripts to .git/hooks/ with special names. There’s a great overview on githooks.com, but for this exersise I’m only interested in post-commit, post-checkout and post-rewrite. There are others, but these are the ones I’m starting with.

First, lets make all these scripts the same

ln -sf .git/hooks/{post-commit,post-checkout}
ln -sf .git/hooks/{post-commit,post-rewrite}
ls -al .git/hooks

Now lets hack together .git/hooks/post-commit:

#!/usr/bin/env ruby

require 'time'

class TagBuilder
  CTAGS_TMP = ".git/ctags.tmp"
  CTAGS_FILE = ".git/ctags" 

  def initalize
    if !ctags?
      puts "ctags not present on system"
      exit(0)
    end

    if !gemfile?
      puts "cowardly exiting a project without a gemfile"
      exit(0)
    end
  end

  def call
    create_tmp_tags_for_project and make_tmp_tags_current
  end

  private

  def create_tmp_tags_for_project
    puts "building #{CTAGS_TMP}"
    system("ctags -R -f #{CTAGS_TMP} #{ctags_paths}")
  end

  def make_tmp_tags_current
    puts "updating #{CTAGS_FILE}"
    system("mv #{CTAGS_TMP} #{CTAGS_FILE}")
  end

  def ctags?
    system("which ctags > /dev/null")
  end

  def gemfile?
    File.exist?('.gemfile')
  end

  def ctags_paths
    "#{cwd} #{gem_path}"
  end

  def cwd
    "."
  end

  def gem_path
    "#{home}/.rbenv/versions/#{version}/lib/ruby/gems"
  end

  def home
    ENV["HOME"]
  end

  def version
    `rbenv version`.chomp.split(' ').first
  end
end

TagBuilder.new.call

Make sure you update permissions to executable!

chmod +x .git/hooks/post-commit

This script compiles ctags in your current checkout and gemfiles installed and managed by rbenv. And yeah, it’s triggered after a commit!

Test this in vim with with ctrl + ] to “follow a link”, and ctrl + o to return back to the buffer you came from.