Rails アプリケーションの開発をするとき、頻繁にメソッドの定義内容や Gem のコードを参照することがあります。
そこで、ソースコードタグ付けツールの GNU GLOBAL (gtags) を使って少しでも楽に、快適にしたい、という話です。
タグ付けツールでは ctags が有名で、こっちは Ruby や Rails での利用例も結構見つかるのですが、GLOBAL は定義へのジャンプだけでなく、参照へのジャンプもできる点が優れています。ビルトインパーサーが対応している言語が少ないのが難点なのですが (Ruby も未対応)、プラグインパーサーとして先の ctags と Pygments を利用することで、多くの言語に対応させることが可能です。
やりたいこと
以下を実現するのが目的です。
- アプリケーションコードで「定義」「参照」「シンボル」のタグジャンプを使えるようにする
- アプリケーションが利用している Gem のコードに対しても同様にタグジャンプの対象にする
- GLOBAL の gtags インターフェースには helm-gtags を使う
これから、その手順を紹介します。ただ、
- タグ付けは完璧じゃないので過信はできない。特に Ruby は動的な処理や DSL を多用するため、タグジャンプ先を間違うことも頻繁にある
- 紹介する手順はまだ試行錯誤中だったりする
- 例えば、JS が対象になったり、Slim などのテンプレートエンジンが対象にならないなど、本来は gtags の config ファイルを設定すべき点があるがスルーしてる
- タグファイルの自動更新が未確認
などの注意点があることは認識しといてもらえれば、と思います。
GLOBAL をインストールする
GLOBAL の導入についてはすでにナイスな記事がいくつもありますので、そちらを読んでください。
- GNU GLOBALの対応言語を大幅に増やすPygmentsパーサーを導入する
- Go, Ruby, PythonでGNU GLOBAL(gtags)+α
- GNU GLOBALへのPygmentsパーサー取り込みでソースコード読みが信じられないくらいに捗るはず
その際、上記で書かれている手順にしたがってインストールすれば問題ないとは思いますが、必ず pygments および Exuberant Ctags との連携を有効にするようにしてください。
アプリケーションコードのタグを生成する
アプリケーションコードのタグを生成するのは簡単です。Rails アプリケーションのルートディレクトリ (Rails.root
) でタグを生成するためのコマンドを実行します。
$ cd /path/to/rails/root
$ gtags --gtagslabel=pygments
これで、タグファイルが作られました。
- GPATH
- GRTAGS
- GTAGS
という3つのファイルが正しくできていることを確認してください。また、普通はこれらのファイルは VCS の対象にはしないと思うので、誤ってこれらのファイルを登録しないように .gitignore
などに追加しておいた方が良いでしょう。
※このとき、Gem のインストール先を vendor
以下などの Rails ルート配下にしている場合は、上記を実行した際に Gem のタグも生成されてしまいます。むしろここで Gem のタグが生成されればそれはそれで良いのですが、あくまでも本記事は Gem のインストール先を問わずに Gem のコードにタグジャンプすることを目的としています。したがって、ここで Gem のタグが作られた場合には、後の手順で重複してタグを生成することになることにご注意ください。
Gem のタグを生成する
Gem のタグは、各 Gem で個別に作ります。bundle show
で Gem のインストールディレクトリを参照して、そこで gtags
コマンドを実行することによりタグを生成します。
1つずつやるのはさすがにつらいので以下のような感じで実行してください。
for i in `bundle show --paths`
do
pushd $i >/dev/null
echo "Creating tags for `basename $i`"
gtags --gtagslabel=pygments
popd >/dev/null
done
上記の実行後、Gem ごとにタグファイルが生成されます。
$ ls `bundle show rails`/G*
/Users/5t111111/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rails-4.2.3/GPATH
/Users/5t111111/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rails-4.2.3/GRTAGS
/Users/5t111111/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rails-4.2.3/GTAGS
環境変数 GTAGSLIBPATH を設定する
ここまでで、
- アプリケーションコードのタグファイル
- 各 Gem のタグファイル
ができました。このままではそれぞれ別々に存在しているだけなので、アプリケーションコードから Gem のコードにジャンプする、といったことはできませんが、GLOBAL には嬉しいことにライブラリのタグを読み込む機能があり、環境変数 GTAGSLIBPATH
にライブラリのパスを指定することで利用できます。
以下のようにして GTAGSLIBPATH
を設定します。
for i in `bundle show --paths`
do
gtags_lib_path="${gtags_lib_path}:${i}"
done
export GTAGSLIBPATH="${gtags_lib_path#*:}"
Gem のディレクトリへのパスがコロン区切りで設定されます。
$ echo $GTAGSLIBPATH
Emacs 側で GTAGSLIBPATH を読み込む
コンソール上で Emacs を起動するなど、環境変数が引き継がれるのであれば特に気にしなくてもいいかもしれませんが、自分の場合は GUI の Emacs をサーバーとして利用していることもあって、Emacs 上で GTAGSLIBPATH
を読み込むのがちょっと厄介な点でした。
いくつかアプローチを考えましたが、結局以下の方法でやることにしました。
- 環境変数の設定には direnv を利用する
- Emacs 側で direnv を読み込む関数を用意する
direnv のインストール方法などはここでは記載しません。
direnv で環境変数を設定する
Rails のルートディレクトリに .envrc
を用意し、その中で前述の環境変数設定スクリプトを動かします。
$ direnv edit .
.envrc
for i in `bundle show --paths`
do
gtags_lib_path="${gtags_lib_path}:${i}"
done
export GTAGSLIBPATH="${gtags_lib_path#*:}"
これで、このディレクトリ内でのみ GTAGSLIBPATH
が有効になります。
Emacs で direnv を読み込む
Emacs 側で direnv を読み込むために、以下の関数を用意しました。
;; Most functions are taken from zph/direnv.el https://gist.github.com/zph/50c766d6c7aa212896b5
(require 'cl-lib)
;; Depends on s.el
(defun direnv-data (dir)
;; TODO: use dir for folder or smart current-project-dir variable
(let ((cmd (concat "$SHELL -i -c '" "cd " dir " && direnv export bash'")))
(shell-command-to-string cmd)))
;;(direnv-data "~/src/direnv")
(defun commands-from-direnv (text)
(cl-remove-if 's-blank?
(split-string (first (last (split-string text "\n"))) ";")))
(defun line->pair (line)
(split-string (s-join " " (rest (split-string line " "))) "="))
(defun remove-$-and-quotes (val)
(s-with val
(s-chop-prefix "$")
(s-chop-prefix "'")
(s-chop-suffix "'")
(s-chop-prefix "\"")
(s-chop-suffix "\"")))
(defun line->kv (line)
(let* ((pair (line->pair line))
(key (first pair))
(value (remove-$-and-quotes (first (last pair)))))
(list key value)))
(defun is-export? (str)
(s-starts-with? "export" str))
(defun is-ignored-key? (ls)
(let ((key (first ls)))
(or
(s-starts-with? "DIRENV" key)
(s-starts-with? "PATH" key))))
(defun extract-exports (cmds)
(mapcar 'line->kv
(cl-remove-if-not 'is-export?
(commands-from-direnv cmds))))
(defun commands->list (cmds)
(let ((exports (extract-exports cmds)))
(cl-remove-if 'is-ignored-key? exports)))
(defun setenv-pair (pair)
(let* ((k (first pair))
(v (first (last pair))))
(setenv k v)))
(defun set-env-from-direnv (dir)
(let* ((data (direnv-data dir))
(pairs (commands->list data)))
(mapcar 'setenv-pair pairs)))
(defun load-direnv ()
"Load direnv .envrc for the current buffer."
(interactive)
(let ((direnv-root-dir (locate-dominating-file default-directory ".envrc")))
(unless direnv-root-dir
(error "Error: .envrc is not found."))
(set-env-from-direnv direnv-root-dir)))
これを init.el
に書いておくなどしてロードすれば、Rails のアプリケーションコードがカレントバッファの状態で M-x load-direnv
とすれば自動的に direnv の環境変数が読み込まれ、GTAGSLIBPATH
が正しく Emacs 上に反映されます。
helm-gtags を設定する
これで Emacs で GLOBAL を実行する準備ができました。インターフェースには helm-gtags
を使うと便利なので、設定していきます。
helm-gtags のインストール
helm-gtags は helm インターフェースで gtags を利用することができます。MELPA に登録されているので、以下でインストール可能です。
M-x package-install [RET] helm-gtags [RET]
とりあえず最低限の設定は以下のような感じです。
;; automatically start gtags-mode on ruby-mode
(add-hook 'ruby-mode-hook (lambda () (helm-gtags-mode)))
;; key bindings
(eval-after-load "helm-gtags"
'(progn
(define-key helm-gtags-mode-map (kbd "M-t") 'helm-gtags-find-tag)
(define-key helm-gtags-mode-map (kbd "M-r") 'helm-gtags-find-rtag)
(define-key helm-gtags-mode-map (kbd "M-s") 'helm-gtags-find-symbol)
(define-key helm-gtags-mode-map (kbd "C-c <") 'helm-gtags-previous-history)
(define-key helm-gtags-mode-map (kbd "C-c >") 'helm-gtags-next-history)
(define-key helm-gtags-mode-map (kbd "M-,") 'helm-gtags-pop-stack)))
詳しくは以下を参照ください。
https://github.com/syohex/emacs-helm-gtags
タグジャンプする!
ようやくこれでタグジャンプができます。モデルでもコントローラーでもいいので、何かアプリケーションのコードを Emacs で開いて試してみましょう。もし環境変数 GTAGSLIBPATH
が設定されていなければ、前述のように M-x load-direnv
を実行して読み込んでおきます。
それでは、何か適当なメソッド (例えば validates
とか redirect_to
とか…) にカーソルを置いて M-t
を押してください。メソッド定義 (候補が複数あれば helm で選択する) にジャンプできましたか?できましたね?やばい!
M-r
を押せば参照へのジャンプもできます。ただし、Gem 内からアプリケーションの参照位置は探せなかったり、メソッドの呼び出し方によっては参照が見つけられなかったりするので、あくまで補助的なものぐらいで考える方が良いかもしれません。
その他に基本的な機能として、
-
M-s
でシンボルの参照 (これも Ruby だといまいちなので補助的な利用に留める) -
C-c <
でジャンプ前の位置に戻る -
C-c >
で進む
をよく利用すると思います。
おわりに
何だか「Ruby だと微妙…」という点が多かったので、もしかしたら使いものにならないと思われてしまったかもしれません。微妙というのは確かにその通りです。ただ個人的には、もうこれがないとやってられないレベルでは必須のツールになっていますのでそれくらいの価値はあります (わかりにくい) 。
Rails 開発者の方はタグジャンプを使わない人が多い印象ですが、使ったことがない人は一度試してみて欲しいと思います。動的な言語でも、IDE を使わなくても、「あ、これぐらいのことはできるんだ。結構便利じゃん。」とか感じてもらえれば嬉しいです。