A text-based Ruby tags system for Emacs
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.
- It indexes full projects along with gems, including the Ruby standard library;
- 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 atModTwo
, the searched tag will be the full module name:ModOne::ModTwo
.
- Emacs 25 or greater.
- The
ruby
,gem
, andbundler
commands must be readily accessible from within Emacs. If you're on macOS, I recommend installing the exec-path-from-shell package.
RbTagger is available on the two major package.el
community
maintained repos - MELPA Stable and
MELPA. If you want to use MELPA stable,
add the following repository to package-archives
:
;; This code snippet should be saved to init.el
(add-to-list 'package-archives
'("melpa-stable" . "https://stable.melpa.org/packages/") t)
If you want to stay on the bleeding edge:
(add-to-list 'package-archives
'("melpa" . "https://melpa.milkbox.net/packages/") t)
After that, you can install RbTagger with the following command:
M-x package-install
[RET] rbtagger
[RET]
Use the following code in init.el to enable rbtagger-mode
in all
Ruby buffers:
;; For ruby-mode
(add-hook 'ruby-mode (rbtagger-mode))
;; For enh-ruby-mode
(add-hook 'enh-ruby-mode (rbtagger-mode))
You can generate tags for the current Ruby project with M-x
rbtagger-generate-tags
. I strongly recommend setting up automation
either through an after-save
hook or git hooks for a
better experience.
After enabling the minor mode, you can find definitions for the symbol
at point with M-., which is a shortcut for M-x
rbtagger-find-definitions
. The above keybinding replaces Emacs'
keybinding for xref-find-definitions
.
You can also force displaying a prompt of tags to choose from with the
universal argument: C-u M-.
.
To pop back to where you were before, the command is still
M-, or M-x xref-pop-marker-stack
, which is a
default xref
command.
Here is a list of commands:
Keybinding | Description |
---|---|
M-. | rbtagger-find-definitions |
M-, | xref-pop-marker-stack |
C-u M-. | rbtagger-find-definitions (displays prompt) |
C-c C-. | rbtagger-find-definitions-other-window |
C-u C-c C-. | rbtagger-find-definitions-other-window (displays prompt) |
C-c M-. | rbtagger-find-definitions-other-frame |
C-u C-c M-. | rbtagger-find-definitions-other-frame (displays prompt) |
I strongly recommend reading up this guide for more details on how to best use this package.
TIP: In the tag prompt, both TAB
and ?
are set to trigger
autocomplete or display the available tag completions. To insert a
literal question mark in the completion prompt (which is a valid
character for Ruby methods), type C-q ?
.
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 the Ruby standard library,
- Index all dependencies declared in
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 faster because the script will skip directories 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 patterns to your global .gitignore
:
$ echo "TAGS*" >> ~/.gitignore
$ echo ".TAGS" >> ~/.gitignore
$ echo .ruby_tags_commit_hash >> ~/.gitignore
M-x rbtagger-generate-tags
will create two hidden buffers
that can be accessed with the following commands:
- M-x
rbtagger-stdout-log
: The message log of what's being indexed; - M-x
rbtagger-stderr-log
: The error log.
You can watch the output of these buffers live for troubleshooting,
or after indexing. Note that they will only hold the output of the
last rbtagger-generate-tags
.
A message will also be displayed in the minibuffer (or the
*Messages*
buffer) when the command finishes, or you can configure
Custom Notifications.
I recommend installing the
projectile
package (also
available on MELPA) and enabling
(projectile-mode)
globally in your init.el to visit a project's TAGS
file
automatically when switching buffers (on Emacs' find-file-hook
,)
otherwise you'll have to manually manage the active tags table with
M-x visit-tags-table
.
M-x rbtagger-find-definitions
or M-. (provided
that rbtagger-mode
is enabled) 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.
I recommend the following settings for a smoother tags experience with
no prompts. Save them in init.el
:
;; Make tag search case sensitive. Highly recommended for
;; maximum precision.
(setq tags-case-fold-search nil)
;; Reread TAGS without querying if it has changed
(setq tags-revert-without-query 1)
;; Always start a new tags list (do not accumulate a list of
;; tags) to keep up with the convention of one TAGS per project.
(setq tags-add-tables nil)
You can automate tags generation with an after save hook. If you want to update your TAGS every time you save a Ruby file, you can setup a hook like this in your Emacs config:
(add-hook 'after-save-hook
(lambda ()
(if (eq major-mode 'enh-ruby-mode)
(call-interactively 'rbtagger-generate-tags))))
If you use ruby-mode
instead of enh-ruby-mode
, just replace
enh-ruby-mode
in the above snippet.
It is possible to automate tags generation with the help of
emacsclient
when committing 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))
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))))
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.
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:
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.
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!
Through the command line in batch mode:
$ make test
Through Emacs:
- Open
test/rbtagger-test.el
- Run M-x
eval-buffer
. Side-effect warning: this will add MELPA topackage-archives
and install dependencies. - Press
C-c C-r
to run all tests. - To run a single test, press M-x
eval-expression
and type(ert "name-of-the-test")
. Seeert
docs for more options.
The make
command with no arguments will compile rbtagger.el
and
run checkdoc
over it. Any warnings will make the command fail.
Copyright © 2021 Thiago Araújo Silva.
Distributed under the GNU General Public License, version 3