Vimのgfコマンドをgit diff特有の出力でも上手く扱うようにする


2011年 04月 21日

問題

八百万あるVimのコマンドで特に有用なもののひとつとしてgfがあります。
このコマンドはカーソル下にあるファイル名らしき文字列を探し、
該当するファイルがあればそれを開くというものです。

gf はカーソル下にあるファイル名らしき文字列をそのまま使うだけでなく、
特定のディレクトリ下にあるかどうか検索(例えばC言語でなら /usr/include./include を検索)したり、
特定の拡張子を付加して検索(例えばJavaなら SomeClass のファイル名は SomeClass.java なので、 .java を付加して検索)することができ、
そこそこ賢く動いてくれます。

さて、日常的に git を使っている身としては
日常的に git diff の出力を眺める機会も多いです。
git diff の出力を眺めて変更のあったファイルを開く」ということも頻繁に行います。
これには gf を使えばよいと思うことでしょう。

ところが深遠なる事情により git diff の出力フォーマットでは変更のあったパスにプレフィックスとして a/b/ が付加されます。
例えば master/Foo.cs の変更を含む git diff の出力には
a/master/Foo.csb/master/Foo.cs といった名前が含まれます。
a/master/Foo.csb/master/Foo.cs の上で
gf を実行しても該当するファイルは存在しないためエラーとなってしまいます。
これは不便です。

一応、ビジュアルモードで a/b/ を含まないよう範囲選択をして gf を実行すれば、
選択範囲のファイル名に該当するファイルを開くことができます。
しかしいちいち範囲選択をするのも面倒な話です。

どうにかして git diff 固有の出力フォーマットでも gf で適切なファイルを開きたいところです。
しかしどうすればよいでしょうか。

解決方法

以下の設定を vimrc へ追加しましょう:

" git-diff-aware version of gf commands.
nnoremap <expr> gf  <SID>do_git_diff_aware_gf('gf')
nnoremap <expr> gF  <SID>do_git_diff_aware_gf('gF')
nnoremap <expr> <C-w>f  <SID>do_git_diff_aware_gf('<C-w>f')
nnoremap <expr> <C-w><C-f>  <SID>do_git_diff_aware_gf('<C-w><C-f>')
nnoremap <expr> <C-w>F  <SID>do_git_diff_aware_gf('<C-w>F')
nnoremap <expr> <C-w>gf  <SID>do_git_diff_aware_gf('<C-w>gf')
nnoremap <expr> <C-w>gF  <SID>do_git_diff_aware_gf('<C-w>gF')

function! s:do_git_diff_aware_gf(command)
  let target_path = expand('<cfile>')
  if target_path =~# '^[ab]/'  " with a peculiar prefix of git-diff(1)?
    if filereadable(target_path) || isdirectory(target_path)
      return a:command
    else
      " BUGS: Side effect - Cursor position is changed.
      let [_, c] = searchpos('\f\+', 'cenW')
      return c . '|' . 'v' . (len(target_path) - 2 - 1) . 'h' . a:command
    endif
  else
    return a:command
  endif
endfunction

これで a/master/Foo.cs のようなテキスト上で
gf を実行すると master/Foo.cs を開くようになります。
さらに a/master/Foo.cs が実在する可能性も考えられるので、
もし a/master/Foo.cs が実在する場合はそちらを優先して開くようにもなっています。

また、 gf には <C-w>f (新しくウィンドウを開いてから gf する)等のバリエーションがいくつかあるため、
他の gf 系コマンドでも同様に処理されるようにしてみました。

これで git diff が多い日も安心です。
やりましたね。

補足

{lhs} がタイプされた時、
{lhs} の代わりに {expr} の評価結果を実行するよう設定します。

カーソル下のファイル名らしき文字列を取得します。

カーソル下のファイル名らしき文字列の末尾の行番号や列番号を取得します
(厳密には異なるのですが、上記の設定例ではそういう挙動になるように実行される文脈を調整してあります)。

ファイル名らしき文字にマッチするVimの正規表現です。

「ファイル名らしき文字」とは何かを定義しているオプションです。

これまで散々「ファイル名らしき文字列」と表現してきましたが、
環境によってファイル名に使える文字が若干異なるため、
それに対応できるようVimでは専用のオプションや正規表現が用意されています。

別解としてこのオプションを使う方法もあるのですが、
本来の目的から外れた使い方になってしまうので取り止めています。

予告

  • 次回はEmacs編です。