###

alias hbin me

The Ultimate Solution of Emacs Finding Tags in a Rails Project

Emacs is my favorite text editor. I do a lot of Ruby on Rails programming using it. It’s really handy, useful and hacky for me ;)

Sometimes, I want to jump to the definition of a method or a class/module, I will use the ctags tool to build a TAGS file in the root of my project.

1
ctags -e -R --extra=+fq --exclude=db --exclude=doc --exclude=log --exclude=tmp --exclude=.git --exclude=public . $(rvm gemdir)/gems

The appending $(rvm gemdir)/gems will including Rails build-in method(or class/module) definitions, it’s very useful to browse Rails source code.

Formerly, I use the Emacs build-in find-tag command which bounding to M-. to find the tags. It works, but what annoy me is that everytime I find-tag, it prompt to choose a tag, even the first one always what I want. So I write a command to take the place of default find-tag:

1
2
3
4
(defun hbin-find-tag ()
  (interactive)
  (find-tag (find-tag-default)))
(global-set-key (kbd "M-.") 'hbin-find-tag)

It seems that things goes well. But I found that If a method defined in many places. find-tag only jump to one of them, and without any prompt. This misguide me occasionally.

After some google search, I found a package Etags-Select provides a feature to find tag from multiple tag files, and if there are multiple matching tags, it will open a selection window for you to choose the one you want.

Sounds great! eh?

After give it a try, I found it’s not that perfect(not perfect for ruby code at least). It can’t jump to the definition of a method whose name ending with a question mark! e.g. signed_in?. After a wandering around the source code of Etags-Select, I decide to ‘fix’ it by myself. And finally, I made it! Yeah!

It’s showtime ;)

Firstly, git clone my forked Etags-Select version and add it to your Emacs load-path, you may also need to install eproject and s.el first.

1
2
3
;;; Installation of Etags-select
(add-to-list 'load-path "/path/to/my-etags-select")
(require 'etags-select)

Secondly, add following snippets to your emacs dotfile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
(require 'thingatpt)

(defun thing-after-point ()
  "Things after point, including current symbol."
  (if (thing-at-point 'symbol)
      (save-excursion
        (let ((from (beginning-of-thing 'symbol))
              (to   (end-of-thing 'line)))
          (and (> to from)
               (buffer-substring-no-properties from to))))))

(defun ruby-thing-at-point ()
  "Get ruby thing at point.
   1. thing at 'current_user'   get current_user;
   2. thing at '!current_user'  get current_user;
   3. thing at 'current_user!'  get current_user!;
   4. thing at 'current_user='  get current_user=;
   5. thing at 'current_user =' get current_user=;
   6. thing at 'current_user ==' get current_user;
   7. thing at 'current_user ||=' get current_user=;
   Otherwise, get `find-tag-default symbol."
  (if (member (symbol-name major-mode)
              '("ruby-mode" "rhtml-mode" "haml-mode" "slim-mode"))
      (let ((symbol (thing-at-point 'symbol))
            (remain (thing-after-point)))
        (if (and symbol remain)
            (let ((sym (s-chop-prefixes '("!!" "!") symbol))
                  (rem (s-chop-prefixes '("!!" "!") remain)))
              (if (s-matches? (concat "^" sym "\\( *\\(||\\)?=[^=]\\)") rem)
                  (concat sym "=")
                sym))
          (find-tag-default)))
    (find-tag-default)))

(defun visit-project-tags ()
  (let ((tags-file (concat (eproject-root) "TAGS")))
    (visit-tags-table tags-file)
    (message (concat "Loaded " tags-file))))

(defun hbin-build-ctags ()
  "Build ctags file at the root of current project."
  (interactive)
  (let ((root (eproject-root)))
    (shell-command
     (concat "ctags -e -R --extra=+fq "
             "--exclude=db --exclude=doc --exclude=log --exclude=tmp --exclude=.git --exclude=public "
             "-f " root "TAGS " root)))
  (visit-project-tags)
  (message "TAGS built successfully"))

(defun hbin-etags-find-tag ()
  "Borrow from http://mattbriggs.net/blog/2012/03/18/awesome-emacs-plugins-ctags/"
  (interactive)
  (if (file-exists-p (concat (eproject-root) "TAGS"))
      (visit-project-tags)
    (hbin-build-ctags))
  (etags-select-find (ruby-thing-at-point)))

(global-set-key (kbd "M-.") 'hbin-etags-find-tag)

Thirdly, The Forgotten snippets

1
2
3
4
5
;; Modify syntax entry
(defun hbin-ruby-mode-init ()
  (modify-syntax-entry ?? "w")
  (modify-syntax-entry ?! "w"))
(add-hook 'ruby-mode-hook 'hbin-ruby-mode-init)

Notice!! According to which template language you prefer, you may need to modify-syntax-entry for that major mode. For example to rhtml

1
2
3
4
(defun hbin-rhtml-mode-init ()
  (modify-syntax-entry ?? "w")
  (modify-syntax-entry ?! "w"))
(add-hook 'rhtml-mode-hook 'hbin-rhtml-mode-init)

DONE and ENJOY IT!

Let me show you some scenes.

1
2
3
4
5
6
7
8
9
current_user              # jump to current_user definition
!current_user.nil?        # jump to current_user definition
current_user == User.find # jump to current_user definition

signed_in?                # jump to signed_in? definition
self.current_user = nil   # jump to current_user= definition
current_user ||= User.new # jump to current_user= definition

current_user!             # jump to current_user! definition

It’s awesome! Cheer up!

At last

I’m not good at writing emacs lisp code, any bug report and bug fix pull request are welcome!

Comments