Silver Searcher Tab Completion with Exuberant Ctags

Josh Clayton

I’m a heavy Vim user and demand speedy navigation between files. I rely on Exuberant Ctags and tag navigation (usually Ctrl-]) to move quickly around the codebase.

There were times, however, when I wasn’t in Vim but wanted to use tags to access information quickly; most noticeable was time spent in my shell, searching the codebase with ag.

As a zsh user, I was already aware of introducing tab completion by way of compdef and compadd:

_fn_completion() {
  if (( CURRENT == 2 )); then
    compadd foo bar baz
  fi
}

compdef _fn_completion fn

In this example, fn is the binary we want to add tab completion to, and we only attempt to complete after typing fn and then TAB. By checking CURRENT == 2, we’re verifying the position of the cursor as the second field in the command. This will complete with options foo, bar, and baz, and filter the options accordingly as you start typing and hit TAB again.

Now that we understand how to configure tab completion for commands, next up is determining how to extract useful information from the tags file. Here’s the first few lines of the file from a project I worked on recently:

==      ../app/models/week.rb   /^  def ==(other)$/;"   f       class:Week
AccessToken     ../app/models/access_token.rb   /^class AccessToken < ActiveRecord::Base$/;"    c
AccessTokensController  ../app/controllers/access_tokens_controller.rb  /^class AccessTokensController < ApplicationController$/;"      c

The tokens we want to use for tab completion are the first set of characters per line, so we can use cut -f 1 path/to/tags to grab the first field. We then use grep -v to ignore autogenerated ctags metadata we don’t care about. With a bit of extra work (like writing stderr to /dev/null in the instance where the tags file doesn’t exist yet), the end result looks like this:

_ag() {
  if (( CURRENT == 2 )); then
    compadd $(cut -f 1 .git/tags tmp/tags 2>/dev/null | grep -v '!_TAG')
  fi
}

compdef _ag ag

With this in place, we can now ag a project and tab complete from the generated tags file. With ag AccTAB:

$ ag AccessToken
AccessToken             AccessTokensController

And the result:

[ ~/dev/thoughtbot/project master ] ✔ ag AccessToken
app/controllers/access_tokens_controller.rb
1:class AccessTokensController < ApplicationController
15:    @project = AccessToken.find_project(params[:id])

app/models/access_token.rb
1:class AccessToken < ActiveRecord::Base

db/migrate/20140416195446_create_access_tokens.rb
1:class CreateAccessTokens < ActiveRecord::Migration

db/migrate/20140718175701_add_index_on_access_tokens_project_id.rb
1:class AddIndexOnAccessTokensProjectId < ActiveRecord::Migration

spec/models/access_token_spec.rb
3:describe AccessToken, 'Associations' do
7:describe AccessToken, '.find_project' do
12:      result = AccessToken.find_project(access_token.to_param)
20:      expect { AccessToken.find_project('unknown') }.
26:describe AccessToken, '.generate' do
40:describe AccessToken, '#to_param' do
50:    expect(AccessToken.find(access_token.to_param)).to eq(access_token)

Voila! Tab completion with ag based on the tags file.

If you’re using thoughtbot’s dotfiles, you already have this behavior.