コンパイルエラーへジャンプする際に、絶対パスで移動する

C++でプログラミングしていると、当然ですがコンパイルエラーが発生します。その時、C-x `とかでコンパイルエラーの箇所にジャンプするのですが、ソースファイルが複数ディレクトリに跨っていたりすると、上手くジャンプできない場合が多々発生していました。

これはなんとかせんとあかんやろ、と思いつつ、調べても満足に情報が見つからなかったため(探し方が悪いのかもしれません)、なんとかしてみることにしました。

とりあえず、C-x `に割りあてられているnext-errorよりも、Compilationモードで登録されている、compile-goto-errorから調べてみることにします。

ocmpile-goto-errorは、実際にファイルを開き、該当のエラー箇所へとジャンプするための関数で、標準のcompile.elにて定義されています。

とりあえず、実際にジャンプさせている部分は以下です。関数を実行した時点のpropertyからdirectoryが取得できた場合、別のウィンドウにそのディレクトリを表示するようになっていますが、今回はそれじゃないので以降の処理がポイントです。

  (if (get-text-property (point) 'directory)
      (dired-other-window (car (get-text-property (point) 'directory)))
    (push-mark)
    (setq compilation-current-error (point))
    (next-error-internal)))

push-markで現在位置にマークを設定し、現在位置を現在エラーとして登録し、next-error-internalを呼びだしています。
じゃあnext-error-internalはどうなっているのかというと、これはsimple.elに定義されています。
何をする関数なのかというと、next-errorメッセージに一致するソースコードの位置に移動する、と読めます。

で、この中で一番重要なのが以下の部分。

  ...
(funcall next-error-function 0 nil)
  ...

このnext-error-functionは、simple.elの中でバッファローカル変数として定義されています。これを定義して設定することで、色々できるようになっている、という寸法のようです。

で、compile.elにこれに対応する関数が「compilation-next-error-functon」として定義されています。
この中で色々ごちゃごちゃやっているんですが、一番重要なのは以下の部分です。

(with-current-buffer (compilation-find-file marker (caar (nth 2 loc))
   (cadr (car (nth 2 loc))))

このあたり、レベルの低い自分にはちと理解できていませんが、locというのは、
(COLUMN LINE FILE-STRUCTURE)
となっているリストのことで、この部分の前に、compile-next-error関数から取得できます。とりあえずそれはおいておいて。
FILE-STRUCTUREは同様に、以下のようなリストです。
*1で、上のlocのリストの2なので、(caar ((FILENAME . DIRECTORY) FORMAT (LINE LOC) ...))となって、 実行時の行にあるファイル名を取得しています。
(cadr (car (nth 2 loc)))は、上とほぼ同じで、

      (cadr (car ((FILENAME . DIRECTORY) FORMAT (LINE LOC) ...)))
      ↓
      (cadr (FILENAME . DIRECTORY))
      ↓
      DIRECTORY

ということで、ディレクトリを取得しています。
以降、このディレクトリを元に検索するのですが、じゃあこのディレクトリは一体どこで設定しているのか、ということになります。

えー、この部分が非常にやっかいなものになってまして・・・。

つまり上記のlocを設定する際か、検索の際に常に絶対パスでファイルを指定するようにすればいいんですが、上記のlocは、compile-next-errorの中で、

(msg (get-text-property pt 'message))

として取得されています。font-lockはfaceだけじゃなくて、その場所に情報を埋め込むことまでできるよう(よくわかってない)ですが、そこから取得していることがわかります。

このfont-lockは、compilation-mode-font-lock-keywordsというそのまんまの名前の関数内で設定されています。
この関数の中で、ディレクトリを取得しているのは、compilation-mode-font-lock-keywords中の次の処理なんですが、

(defvar compilation-directory-matcher
  '("\\(?:Entering\\|Leavin\\(g\\)\\) directory `\\(.+\\)'$" (2 . 1))
  ...)
...
(if compilation-directory-matcher
	 `((,(car compilation-directory-matcher)
	    ,@(mapcar (lambda (elt)
			`(,(car elt)
			  (compilation-directory-properties
			   ,(car elt) ,(cdr elt))
			  t t))
		      (cdr compilation-directory-matcher)))))

このcompilation-directory-matcherは、Makefileでmakeした時に出る「Leaving ...」とか「Entering ...」を補足しています。上の処理だと(font-lock-add-keywordsの処理も絡みますが)、compilation-directory-matcherの正規表現にマッチした場合に、compilation-directory-matcherの(2 . 1)にmapcar以下の処理を適用する、と読めます。

で、私ここまで来て気付きました。「・・・あれ、自分の*compilation*のメッセージ、Leaving ...とか出てないよな・・・?」

なんとなくオチは掴んでいただけたかとは思いますが、恐る恐る M-x compile の後に `LC_ALL=C make` とやってみました。コンパイルエラーが発生した状態で、エラーメッセージの上でEnter・・・。

ちゃんと飛んだよorz

ということでした。つまりはロケールが日本語になっていて、compilation-directory-matcherの正規表現に引っかからず、結果としてエラーが発生したファイルと同じディレクトからしか検索できず、ディレクトリを跨ってジャンプできなくなっていた、というオチです。

つまりは、compileする際には、常にロケールを `LC_ALL=C`と設定するようにすればいい、という感じになるんですが、これまた面倒ですので、Makefile中に記述してしまうことにしました。
本当は自前で利用しているcompile関数の中で、自動的にLC_ALLを設定するようにしてしまえばいいのかもしれませんが、一々それをやるのも面倒ですので。
私はautomakeを利用しているので、Makefile.amに `LC_ALL=C`と記述しておくことにしました。


あー、この現象を調べながらこれを書くのに二時間くらいかかってしまった・・・。うむぅ。

*1:FILENAME . DIRECTORY) FORMAT (LINE LOC) ...) markerはマーカーオブジェクト、というものらしいですが、これまたよくわかりません。 (caar (nth 2 loc