FIVETEESIXONE

Emacs での Rails 開発を GNU GLOBAL でだいぶ快適にする


Rails アプリケーションの開発をするとき、頻繁にメソッドの定義内容や Gem のコードを参照することがあります。そこで、ソースコードタグ付けツールの GNU GLOBAL (gtags) を使ってその作業を少しでも楽に、快適にしたい、という話です。

タグ付けツールでは ctags が有名で、こっちは Ruby や Rails での利用例も結構見つかる (ほとんどは Emacs ではなく Vim ですが) のですが、GLOBAL は定義へのジャンプだけでなく、参照へのジャンプもできる点が優れています。ビルトインパーサーが対応している言語が少ないのが難点なのですが (Ruby も未対応)、プラグインパーサーとして先の ctags と Pygments を利用することで、多くの言語に対応させることが可能です。

やりたいこと

以下を実現するのが目的です。

  1. アプリケーションコードで「定義」「参照」「シンボル」のタグジャンプを使えるようにする
  2. アプリケーションが利用している Gem のコードに対しても同様にタグジャンプの対象にする
  3. GLOBAL の gtags インターフェースには helm-gtags を使う

これから、その手順を紹介します。ただ、

  • タグ付けは完璧じゃないので過信はできない。特に Ruby は動的な処理や DSL を多用するため、タグジャンプ先を間違うことも頻繁にある
  • 紹介する手順はまだ試行錯誤中だったりする
  • 例えば、JS が対象になったり、Slim などのテンプレートエンジンが対象にならないなど、本来は gtags の config ファイルを設定すべき点があるがスルーしてる
  • タグファイルの自動更新が未確認

などの注意点があることは認識しといてもらえれば、と思います。

GLOBAL をインストールする

GLOBAL の導入についてはすでにナイスな記事がいくつもありますので、そちらを読んでください。

その際、上記で書かれている手順にしたがってインストールすれば問題ないとは思いますが、必ず 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 を使わなくても、「あ、これぐらいのことはできるんだ。結構便利じゃん。」とか感じてもらえれば嬉しいです。