A text-based Ruby tags system for Emacs that works better than average
RbTagger is an Emacs library based on ctags,
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.
- It indexes full projects along with gems;
- It indexes full Ruby modules, thanks to
- 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:
- Emacs 25 or greater.
bundlercommands must readily accessible from within Emacs. If you're on macOS, I recommend installing the exec-path-from-shell package.
This package is not yet available on MELPA. In the meantime, you can:
- Clone this repository
- Add the folder where you cloned this repository to
(add-to-list 'load-path "~/folder/where/i/cloned/rbtagger")
The above code snippets can be saved in
TAGS, make sure the current buffer belongs to a Ruby
project with a
git as VCS, then call M-x
The above command will:
- Install the
ripper-tagsgem if not already installed,
- Index the main project,
- Index all dependencies declared on
- Generate a single
TAGSfile 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
$ echo TAGS >> ~/.gitignore $ echo .ruby_tags_commit_hash >> ~/.gitignore
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
Looking up tags
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
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
(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-.,
xref-find-definitions. I recommend overriding this
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-.
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-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 inita project.
I recommend adding the following snippet to
to start the Emacs server when you launch Emacs:
(require 'server) (unless (server-running-p) (server-start))
RbTagger supports custom notifications via hooks. The hook's callable
takes two arguments:
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-listto 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
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
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.
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
- Run M-x
eval-buffer. Side-effect warning: this will add MELPA to
C-c C-rto run all tests.
- To run a single test, press M-x
(ert "name-of-the-test"). See
ertdocs for more options.
Copyright © 2019 Thiago Araújo Silva.
Distributed under the GNU General Public License, version 3