タグジャンプ

久し振りに Ruby に触れていて、色々と Web で検索しているうちに、新たな情報を入手しました。
情報自体は随分と古いものなんですが。

■ Emacs 使いの方へ etags

こちらで、Ruby のコードの TAGS ファイルを作成するスクリプトが紹介されていました。
慌てて man を引いて読み直してみたところ、適切な正規表現を指定すれば何にでも対応できるんですね。これは知りませんでした。英語ドキュメントだとちゃんと読まない (読めない) んですよねえ……
;; 開き直りか ;-p

で、早速作ってみました。
実はこれまで、Ruby でもタグジャンプを使いたいと思いながら、etags コマンドが対応してくれないとダメだと思っていたので、migemo にお世話になりながらコードを書いていたもので。

しかし、etags の正規表現は、特に指定していなくとも行頭からのマッチングになる様 (に見える) で、紹介されているパターンでは、`def', `class' のキーワードが行頭にある定義、つまりトップレベルで定義される関数とクラスにしか合致しませんでした。
その方が良いという考えもあろうかと思いますが、私はモジュールや、入れ子のクラス、メソッド、更にモジュール内の各種定義にも合致して欲しいので、

--regex='/[ \t]*\(def\|class\|module\)[ \t]\([^ \t]+\)/\2/'

の様に指定することにしました。

通常、タグファイルはスクリプトがあるディレクトリに作れば良いのですが、異なるディレクトリごとに毎回タグファイルを作らなければなりません。
その度に shell buffer でコマンドを投入するのは面倒なので、

(defvar etags-tag-rebuild-command-ruby
      (concat "find ./ -name \\*.rb | xargs etags --language=none --regex="
              "'/[ \\t]*\\(def\\|class\\|module\\)[ \\t]\\([^ \\t]+\\)/\\2/'")
      "Ruby スクリプト用のタグファイル作成コマンド。")

としてコマンドラインを定義しておき、

(defun etags-tag-rebuild-ruby (command)
  (interactive
   (list (read-shell-command "Etags command: "
                             etags-tag-rebuild-command-ruby))
   (list etags-tag-rebuild-command-ruby))

  (shell-command command))

なんてコマンドを定義してみました。

M-x etags-tag-rebuild-ruby

とすると、ミニバッファに定義した etags のコマンドが表示されるので、そのままで良ければそのまま C-m します。何か変更を加えたい場合には、その場で変更すれば良いだけです。

本当は、cperl-etags の様に、内部でコマンドを生成して call-process とかしようかとも思ったんですが、これはこれ、その場でコマンドを編集できるのも良いですし、何より簡単なのでこうしてます。
実は、C の TAGS もこんな感じで作ってあり、そちらはもう少し手が込んでます。それを流用しようかとも思ったのですが、そちらはもう随分と昔に定義した関数で、それこそ from scratch で直さないとならない程の代物なもので諦めました。

実はちょっとしたポイントもありまして、XEmacs の etags (コマンドではなく Emacs Lisp パッケージの方) は、`find-etag' などを実行した際、`buffer-tag-table-list' という関数の中で、カレントディレクトリからルートディレクトリまで上位に向って遡り、TAGS ファイルを探してくれます。(FSF Emacs だとどうなんでしょうか。ちょっと違った様な気がしますが……)
そのため、どこかのディレクトリで TAGS ファイルを作成するときに、サブディレクトリまで再帰的に *.rb を探して指定しておくと、それらのファイルを開いたバッファで M-x find-tag するときには、上位ディレクトリで作った TAGS ファイルを勝手に利用してタグジャンプしてくれます。

その様な理由もあって、上の `etags-tag-rebuild-command-ruby' では、find コマンドで下位ディレクトリも含めて *.rb なファイルを探している訳です。

タグファイルの階層化

上のエントリの最後に書きました様に、XEmacs では find-tag の際に、カレントディレクトリからルートディレクトリに向って、TAGS ファイルを探してくれます。そして、見付かった全ての TAGS ファイルの中から tag を探してくれます。
そのため、私の環境では、C 言語向けの TAGS ファイルについて、先ず自分の home ディレクトリに、システムのインクルードファイルを入力にして作成した TAGS ファイルを置いておき、実際に C のコードを書いたディレクトリでは、適宜、そのディレクトリ近辺の *.[ch] を入力にした TAGS が置いてあります。

システムのインクルードファイルについては、カレントディレクトリの TAGS に tag が無くても、カレントディレクトリが自分の home ディレクトリ配下であれば、どこからでも同じ TAGS ファイルを参照してタグジャンプできるという訳です。

さて、この find-tag の際の `buffer-tag-table-list' なんですが、実は、非常に嫌な問題を抱えています。上で紹介している様に、ディレクトリを遡って TAGS ファイルを探してくれるのは良いのですが、探す対象となる `TAGS' というファイル名を、何とハードコードして抱えてしまっているのです。

そうです、そのために Ruby のコードで M-. などしてタグジャンプしようとしたときに、直近の TAGS に存在しないと home ディレクトリに置いてある *C 言語の* TAGS を見て、そこに tag があるとそこに飛んでしまうのです。これがまた結構あるのですよ。

しかし、それを回避する方法もありました。鍵を握るのは `tag-table-alist' という連想リストです。
このリストに、

("\\.rb$" . "~/TAGS.d/ruby")

の様な cons cell を設定しておくことで、該当するファイル名を開いているバッファに於いては、上位ディレクトリを辿るだけではなく、指定されたディレクトリを TAGS ファイルの検索パスとして扱う様になります。

この仕組みを利用して、home ディレクトリ直下に TAGS を置くことを避け、各言語ごとに用意した特定のディレクトリにそれぞれの TAGS を置いておけば、異なる言語の TAGS ファイルを参照してしまうことを避けることができます。
私の場合はこんな感じですね。

(eval-after-load "etags"
  '(progn
     (add-to-list  'tag-table-alist
                   '("\\.rb$" . "~/TAGS.d/ruby"))
     (add-to-list  'tag-table-alist
                   '("\\.[ch]$" . "~/TAGS.d/c"))
     (add-to-list  'tag-table-alist
                   '("\\.scm$" . "~/TAGS.d/gauche"))
     (add-to-list  'tag-table-alist
                   '("\\.p[lm]$" . "~/TAGS.d/perl"))))

ふむ、もっとスマートに設定できないもんですかね。

ともあれ、この様にすることで、各言語ごとに階層化した TAGS ファイルを活用することができています。

しかし、C なんかは良いんですけど、RubyPerl などは、/usr/lib/{ruby,perl} 配下に置いてある様なファイルへの tag があったとしても、適切な tag にジャンプできることは少ないんですよね。find-tag が class や module を意識できないことには、数ある同名のメソッドを順に M-, で探して行かなければなりません。
RubyPerl では、オブジェクトの型を識別するのは無理でしょうから、せめて require や include, use などを見て、バッファ上のコードに関連するファイルを拾うとかでしょうかね。
こいつが解決できたら随分と快適になるとは思うのですが……

今のところは思っているだけです。

TAGS を作るコマンドライン

因みに、上のエントリで言語ごとに作成した、配布モジュールなどを入力とした TAGS ファイルを作成するコマンドは、それぞれこの様な感じにしてます。

Perl

$ find `perl -e 'print "@INC";'` -name \*.p[lm] | xargs etags -l perl

Ruby

$ find `ruby -e 'print $:.join(" ")'` -name \*.rb | \
  xargs etags --language=none --regex='/[ \t]*\(def\|class\)[ \t]\([^ \t]+\)/\2/'

Gauche

$ find `gosh -E'map (lambda (x) (display (string-append x " "))) *load-path*' -Eexit` -name \*.scm | \
  xargs etags

といった感じです。
それぞれ etags への言語の指定方法を変えてみました。いえ、特に意味はありません。

Gauche (gosh) の one liner って初めて書いたかも。
最初、`-Eexit' に気付けなかった。