rbtagger

Ruby tagging tools


Keywords
languages, tools
License
GPL-3.0

Documentation

RbTagger

Build Status

A text-based Ruby tags system for Emacs that works better than average

RbTagger is an Emacs library based on ctags, ripper-tags, and xref.el. It indexes your entire Ruby project along with gems and provides smarter than average tag lookup. It aims to provide context-aware, accurate tag lookup by parsing the current Ruby file and an easy-to-use tags solution that works out of the box.

RbTagger is currently beta software extracted from my Emacs configuration.

Features

  • It indexes full projects along with gems;
  • It indexes full Ruby modules, thanks to ripper-tags extra options.
  • It has contextual tags lookup. RbTagger is aware of Ruby code and will try to jump to the most specific occurrence for the symbol at point.
  • It takes into account the full Ruby module when looking for definitions. If point is on ModOne::ModTwo, more specifically at ModTwo, the searched tag will be the full module name: ModOne::ModTwo.

Prerequisites

  1. Emacs 25 or greater.
  2. The ruby, gem, and bundler commands must readily accessible from within Emacs. If you're on macOS, I recommend installing the exec-path-from-shell package.

Installation

This package is not yet available on MELPA. In the meantime, you can:

  1. Clone this repository
  2. Add the folder where you cloned this repository to load-path:
    • (add-to-list 'load-path "~/folder/where/i/cloned/rbtagger")
  3. (require 'rbtagger)

The above code snippets can be saved in init.el.

Generating tags

To generate TAGS, make sure the current buffer belongs to a Ruby project with a Gemfile and git as VCS, then call M-x rbtagger-generate-tags.

The above command will:

  • Install the ripper-tags gem if not already installed,
  • Index the main project,
  • Index all dependencies declared on Gemfile,
  • Generate a single TAGS file and save it to the root of the project.

The first call to the command might take a few seconds to complete depending on the size of your project, but subsequent calls will be much faster because the script will skip gems whose tags have already been generated. If the gem is a local git project, it will only be reindexed if the commit hash has changed from the previous indexing operation.

Make sure to add the following files to your global .gitignore:

$ echo TAGS >> ~/.gitignore
$ echo .ruby_tags_commit_hash >> ~/.gitignore

Troubleshooting

M-x rbtagger-generate-tags will create two hidden buffers:

  • The message log, *rbtagger-log: PROJECT NAME*, where you can see what is being indexed,
  • The error log, *rbtagger-error-log: PROJECT NAME*.

You can switch to these buffers for troubleshooting even after the command finishes. These buffers will only hold the output of the last command.

A message will also be displayed in the minibuffer (or the *Messages* buffer) when the command finishes, or you can configure Custom Notifications.

Looking up tags

M-x rbtagger-find-definitions tries to find the best match for the symbol at point by computing a list of candidates ordered by specificity. It tries to follow Ruby's Constant lookup rules as closely as possible. Given the following Ruby module:

module Tags
  module Lookup
    module Nested
      def self.call(*args)
        # Some code here...
        Rule.call(*args)
      end
    end
  end
end

Assuming that point is on Rule, RbTagger will try four candidates in order:

  • Tags::Lookup::Nested::Rule
  • Tags::Lookup::Rule
  • Tags::Rule
  • Rule

If one of the candidates resolve to one or more matches, it will either:

  • Jump to the first occurrence when dealing with a single match;
  • Display a list of tags to choose from when dealing with more than one match.

Subsequent candidates will be skipped.

Recommended tag settings

I recommend the following settings for a smoother tags experience with no prompts. Save them in init.el:

(setq tags-add-tables nil)
(setq tags-revert-without-query 1)

Setting up a keyboard shortcut

The classic Emacs shortcut for looking up tags is M-., which calls xref-find-definitions. I recommend overriding this shortcut with rbtagger-find-definitions in your favorite Ruby mode:

;; For enh-ruby-mode
(with-eval-after-load 'enh-ruby-mode
  (define-key enh-ruby-mode-map (kbd "M-.") 'rbtagger-find-definitions))

;; For ruby-mode
(with-eval-after-load 'ruby-mode
  (define-key ruby-mode-map (kbd "M-.") 'rbtagger-find-definitions))

For popping back to where you were before, the command is still M-. or M-x xref-pop-marker-stack.

Git hooks

It is possible to automate tags generation with the help of emacsclient when commiting or running other git operations. Use the following shell script as the body of the post-commit, post-merge, and post-rewrite git hooks:

#!/usr/bin/env bash

emacsclient -e "(rbtagger-generate-tags \"$(pwd)/\")"

TIP: You can setup these hooks as git templates that are automatically copied over whenever you git init a project.

I recommend adding the following snippet to init.el to start the Emacs server when you launch Emacs:

(require 'server)
(unless (server-running-p)
  (server-start))

Custom notifications

RbTagger supports custom notifications via hooks. The hook's callable takes two arguments: success (boolean t or nil) and project-name (string). Here's an example of how it can be used on macOS to integrate with notification center:

(add-hook
 'rbtagger-after-generate-tag-hook
 (lambda (success project-name)
   (if success
       (notify-os (concat project-name " tags 👍") "Hero")
     (notify-os "Is this a Ruby project? Tags FAILED! 👎" "Basso"))))

This particular example assumes you have the following function:

(defun notify-os (message sound)
  "Send a notification to macOS's notification center.
Requires terminal-notifier to be installed via homebrew."
  (shell-command
   (combine-and-quote-strings
    (list "terminal-notifier" "-message" message "-sound" sound))))

Why a single TAGS file?

Can't we use tags-table-list to setup more than one tag file for lookup, i.e., smaller tag tables for each gem?

Certainly, but that could result in hundreds of junk buffers due to the way tags work in Emacs. In a project with 300 gems, Emacs would open 300 buffers while searching for a tag, which would greatly enlarge the buffer list. For that reason, my preference is a single TAGS file.

Where RbTagger can improve

RbTagger's source code is simple and concise on purpose and it works very well for my needs. However, it can improve in the following areas:

Contextual tag lookup

As you can see, contextual tag lookup isn't as efficient as it can be and there is room for improvement. In my experience, it will find the tag instantaneously (with 200+ gems) most of the time, but sometimes it will freeze for about 1 second. The eventual performance hit is negligible for me, but any improvements on performance, better usage of xref, or the algorithm itself would be hugely appreciated.

Tag Candidates list

Building up the candidates list is currently an indentation-based and regex-based algorithm that happens inside Emacs buffers. Ideally, it should work with static analysis but that would probably make the code more complex or add more dependencies. Being regex-based means it would not work properly without properly indented module declarations. I never found this to be a problem because all my Ruby files are indented, but again, any contributions on that front will be appreciated.

What else?

You are welcome to contribute with anything. Please send PRs!

Running the tests

Through the command line in batch mode:

$ cd test
$ emacs -Q -l rbtagger-test.el --batch -f ert-run-tests-batch-and-exit

Through Emacs:

  1. Open test/rbtagger-test.el
  2. Run M-x eval-buffer. Side-effect warning: this will add MELPA to package-archives.
  3. Press C-c C-r to run all tests.
  4. To run a single test, press M-x eval-expression and type (ert "name-of-the-test"). See ert docs for more options.

License

Copyright © 2019 Thiago Araújo Silva.

Distributed under the GNU General Public License, version 3