Skip to content

Instantly share code, notes, and snippets.

@wikimatze
Last active April 7, 2022 23:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save wikimatze/6119389 to your computer and use it in GitHub Desktop.
Save wikimatze/6119389 to your computer and use it in GitHub Desktop.
Browsing Padrino's Code Base With Ctags in Vim

Working effectively with ctags has always been a topic I missed for a long time because I was too lazy to invest time to learn about the it.

I was working on on my application and was constantly consulting Padrino's API doc in the browser. It would have been more effective if I can do the searching directly the Padrino's code on GitHub. Benefit I don't have to leave the terminal and can focus on my task.

What is ctags

ctags is a tool which make it easy for you to shift through in no time. It does this with indexing classes, methods, variables and other things. All this information are stored in a tags file - one line in this file contains one tag.

ctags makes it easy to browse a large code base like Padrino in which you are unfamiliar where to find what you are searching for. Jumping forward and backward between variables, classes, modules, and methods are essential to get understand a code base.

Installing ctags

Depending on your operation system, you can install it with easiness:

$ sudo apt-get install exuberant-ctags

Why I'm installing exuberant-ctags instead of ctags? Ctags was originally written by Ken Arnold (the author of the "Rogue video game") and was first introduced in BSD Unix. Exuberant ctags is a variant of ctags and was distributed with Vim 6.0. The main benefit of exuberant ctags is that it support over 40 languages and has regular expression support which make it easier to create your own custom language parser for creating the tags file. The tags contains the collected information about the things you want to know.

Generating ctags for the Padrino's code base

Just clone Padrino and run the following command at the root of the project:

$ git clone https://github.com/padrino/padrino-framework
$ cd padrino-framework
$ ctags -R .

The command will run recursively through the directory and will tag all sources and files it can find. During running the ctags -R . command I got the following error:

ctags: Warning: ignoring null tag in padrino-admin/lib/padrino-admin/generators/templates/assets/javascripts/bootstrap/bootstrap.min.js

Okay, we are actually interested in all the things, except JavaScript. Let's exclude those files with the following command:

$ ctags -R --exclude="*.js" .

If you are done with the command you have tags file created in the current directory - there is no output if everything went well. Let's open the generated tags file and try to understand the basic nature of the file:

!_TAG_FILE_FORMAT	2	/extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED	1	/0=unsorted, 1=sorted, 2=foldcase/
!_TAG_PROGRAM_AUTHOR	Darren Hiebert	/dhiebert@users.sourceforge.net/
!_TAG_PROGRAM_NAME	Exuberant Ctags	//
!_TAG_PROGRAM_URL	http://ctags.sourceforge.net	/official site/
!_TAG_PROGRAM_VERSION	5.9~svn20110310	//
<<	padrino-core/lib/padrino-core/logger.rb	/^    def <<(message = nil)$/;"	f
==	padrino-core/lib/padrino-core/mounter.rb	/^    def ==(other)$/;"	f	class:Padrino.Mounter
AbstractFormBuilder	padrino-helpers/lib/padrino-helpers/form_builder/abstract_form_builder.rb	/^      class AbstractFormBuilder # @private$/;"	c	class:Padrino.Helpers.FormBuilder
AbstractHandler	padrino-helpers/lib/padrino-helpers/output_helpers/abstract_handler.rb	/^      class AbstractHandler$/;"	c	class:Padrino.Helpers.OutputHelpers
AccessControl	padrino-admin/lib/padrino-admin/access_control.rb	/^    module AccessControl$/;"	m	class:Padrino.Admin
AccessControlError	padrino-admin/lib/padrino-admin/access_control.rb	/^    class AccessControlError < StandardError # @private$/;"	c	class:Padrino.Admin
Account	padrino-admin/test/fixtures/data_mapper.rb	/^class Account$/;"	c
Actions	padrino-admin/lib/padrino-admin/generators/actions.rb	/^      module Actions$/;"	m	class:Padrino.Generators.Admin
...

The header of each tag file gives you basic information about the creation of the file:

!_TAG_FILE_FORMAT	2	/extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED	1	/0=unsorted, 1=sorted, 2=foldcase/
!_TAG_PROGRAM_AUTHOR	Darren Hiebert	/dhiebert@users.sourceforge.net/
!_TAG_PROGRAM_NAME	Exuberant Ctags	//
!_TAG_PROGRAM_URL	http://ctags.sourceforge.net	/official site/
!_TAG_PROGRAM_VERSION	5.9~svn20110310	//

As you can see, the tags are sorted, folded, and you can see the version of ctags in which they were created. If you take a look close in the example above, you can see detect a tag name schema: Let's consider our first tag:

<<	padrino-core/lib/padrino-core/logger.rb	/^    def <<(message = nil)$/;"	f

First of all you have the tagname <<, then a tab as separator (it isn't visible in the code examples above), then tagfile padrino-core/lib/padrino-core/logger.rb where the tag can be found, followed by a tab, and finally the tagaddress the exact location of the tag inside the tagfile. The tagaddress is a regular expression - the special characters in a search pattern are ^ (begin-of-line) and $ (indicates the end of the line). Finally a term marked with ;" which indicates if the tag is either a class (c), module (m), or function (f). The term may differ for which language constructs you are going to create your tags.

Jumping into our first tag

Let's open the padrino-core/lib/padrino-core.rb file and place your cursors under the server name:

require 'sinatra/base'
require 'padrino-core/version'
require 'padrino-core/support_lite'
require 'padrino-core/application'

require 'padrino-core/caller'
require 'padrino-core/command'
require 'padrino-core/loader'
require 'padrino-core/logger'
require 'padrino-core/mounter'
require 'padrino-core/reloader'
require 'padrino-core/router'
require 'padrino-core/server'
require 'padrino-core/tasks'
require 'padrino-core/module'

# The Padrino environment (falls back to the rack env or finally develop)
PADRINO_ENV  = ENV["PADRINO_ENV"]  ||= ENV["RACK_ENV"] ||= "development"  unless defined?(PADRINO_ENV)
# The Padrino project root path (falls back to the first caller)
PADRINO_ROOT = ENV["PADRINO_ROOT"] ||= File.dirname(Padrino.first_caller) unless defined?(PADRINO_ROOT)

module Padrino
  ...
end

Now press <C-]>- you'll directly to the Server class in the padrino-core/lib/padrino-core/server.rb file. Awesome, that is just what you know now what you want to know and you can jump back to your previous position with <C-t> or <C-o>.

You can also jump to the tag you want the commandline mode in Vim and use the :tag command. Type in the name of the class or method you want to jump look into. For example, if you type in :tag Padrino you will jump to a tag. But you allready have a smell in mind that the Padrino exists more than one time. You may think this is a mistake but ctags keeps up a list of all the tags you have explored so far.

If you searched after :tag Padrino again you can message line beyond in your command window: tag 1 of 81 or more. There are 81 browsable matchings tags available. Per default Vim will take the first matching tag if your search for the first time in your vim session after :tag Padrino. Use :tselect followed by a number to jump to the tag you want:

  # pri kind tag               file
> 1 F C m    Padrino           padrino-admin/lib/padrino-admin.rb
               module Padrino
  2 F   m    Padrino           padrino-admin/lib/padrino-admin/access_control.rb
               module Padrino
  3 F   m    Padrino           padrino-admin/lib/padrino-admin/generators/actions.rb
               module Padrino
  4 F   m    Padrino           padrino-admin/lib/padrino-admin/generators/admin_app.rb
               module Padrino
  5 F   m    Padrino           padrino-admin/lib/padrino-admin/generators/admin_page.rb
  ...

So you can chose what you want to have. There a bunch of more commands you can use to navigate multiple tags

  • :tnext (or :tn): goes to the next tag in the :tselect list
  • :tprev (or :tp): goes to the previous tag in the :tselect list
  • :tfirst (or :tf): goes to the first tag in the :tselect list
  • :tlast (or :tl): goes to the last tag in the :tselect list

Before going to add the tags for the Padrino project as well as any other sources you are using on your project, we need to understand how we can get a big tag file of all our installed gems and after that limit our scope to these parts we only need.

Generating Ctag for all installed gems

Let's say you are using rbenv to manage your ruby versions and you want to get a global tag file of all your installed gems. Please run the following command:

ctags -R -f gems.tag * ~/.rbenv/versions/<your-ruby-version>/lib

I have 200 installed gems on my system (use gem list | wc -l) and it took around 7 seconds to generate a tag file with over 200.000 lines. The chances are high that you have errors in your tags file occur and I really don't want to load such a big tag file into my vim session. Please note, that the generated tag file gems.tag instead of tags.

Generating Ctags project specific ones gems

If you think about a ruby project, it is very likely that you will have Gemfile with all the specified extensions you need for your project. My friend Andrew Radev has created a small ruby snippet that uses Bundler API for retrieving the used Gemfile in your project and builds a tag file. Instead of using the script you can also perform the following one-liner on the route of your application:

ctags -R -f gems.tags `bundle show --paths`

Let's say you have a Padrino project (like my Job Vacancy) with the following Gemfile:

source 'https://rubygems.org'

# Server requirements
gem 'thin', '1.5.1'

# Project requirements
gem 'rake', '10.0.4'
gem 'uglifier', '2.1.1'
gem 'yui-compressor', '0.9.6'

# Component requirements
gem 'erubis', '~> 2.7.0'
gem 'activerecord', '~> 3.2.9', :require => 'active_record'
gem 'sqlite3', '~> 1.3.6'

# Test requirements
group :test do
  gem 'rspec' , '2.13.0'
  gem 'factory_girl', '4.2.0'
  gem 'rack-test', '0.6.2', :require => 'rack/test'
end

gem 'guard-rspec'
gem 'libnotify'

# Security
gem 'bcrypt-ruby', '3.0.1', :require => 'bcrypt'

# Padrino Stable Gem
gem 'wirble', '0.1.3'
gem 'pry', '0.9.12'
gem 'tilt', '1.3.7'

# Padrino edge
gem 'padrino', :git => "git://github.com/padrino/padrino-framework.git"

bundle show --paths will print the absolute path of the used Gems in the following form:

/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/open4-1.3.0
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino-admin
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino-cache
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino-core
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino-gen
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino-helpers
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-framework-0c1317b0c897/padrino-mailer
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/bundler/gems/padrino-sprockets-8f4f6150b2d9
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/polyglot-0.3.3
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/pry-0.9.12
/home/helex/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/rack-1.5.2

When you start vim in the root of your project, it will only load the tags file but not the gems.tags file. Just open the commandline in vim and type in :set tags+=gems.tags.

If you find yourself doing it over and over again for different project, you have to setup the following setting in your .vimrc

set tags=tags,./tags,gems.tags,./gems.tags

Vim will search for the file named tags, starting with the directory of the current file and then going to the parent directory and then recursively to the directory one level above, till it either locates the tags file or reaches the root directory. It will do exactly the same for the gems.tags file.

Autogenerate tags with Autotags.vim

During the coding you are going to change the name of the method or class and have to generate the tags by hand. Do you really want to leave the terminal and generate the tags on your own? Tada, there is Autotags.vim. It deletes all tags that are no longer present and calls ctags -a to append the new tags to your existing tag file. To try it out, just create a new directory with a the following class definition inside the file:

class Ctags

end

Now create the tags with ctags -R . search after the tag Ctags with :tag Ctags, after that change the class name from Ctags to VimCtags. If this doesn't work for you, it means that you don't have python support enabled for your vim version. You can check it with vim --version | grep python - a +python means that you have it. I have written a blog post about how to install vim with python support for the case you have problems with this.

Fuzzy like tag search with Ctrlp plugin

As we have learned before it is possible that we partialy know the name of the class or method name we want to use. This is wherre you have to use ctrlp.vim plugin. Ones installed it gives you the :CtrlPTag tag command which make it possible to do a fuzzy like tag search.

If you find yourself using this command very often, you have to add the following mapping into your vimrc:

nnoremap <leader>. :CtrlPTag<cr>

if the Ctrlp doesn't provide you enough hits, please use :tselect.

Further Reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment