読者です 読者をやめる 読者になる 読者になる

gf-user で gf の動作を拡張する

Vim Advent Calendar 2013 の 114 日目の記事です。

gf の問題について

gf は、カーソル下のファイルを開く便利機能です。例えば、"foo.c" とバッファに書かれていた場合、ここにカーソルをのせて gf を押すと、foo.c を開いてくれます。更に gF を使うと、"foo.c:23" みたいになっていた場合に、foo.c の 23 行目を開いてくれるので、更に便利です。
便利なんですが、これ、Windows だとうまく動きません。なぜかと言うと、Windows では 'isfname' に : が含まれているから。
'isfname' は、ファイル名として認識する文字のセットを指定するオプションで、gf はこれを使ってカーソル位置のファイルを認識します。しかし Windows ではドライブレター(C:とか)の関係で : が isfname に含まれているため、foo.c:23 全体をファイル名として認識してしまい、結果としてファイルを開くことができません。
この問題について、本体を修正する動きもありますが、いつ入るか、そもそも入るのかがわかりません。それが待てない場合、プラグインで解決するしかなさそうです。

gf-user

gf-user は、gf の動作を拡張するためのフレームワークです。textobj-user や operator-user を作った kana さん謹製です。

https://github.com/kana/vim-gf-user

gf には gF も含めて派生のキーマッピングがいくつかあり、全部対応しようとすると大変です。このフレームワークを使えば、関数を1つ定義するだけで簡単に対応可能です。


それでは、上記の問題を解決すべく、設定を書いてみましょう。
ちなみに gf-user の help では、拡張を書く際は autoload 関数にして gf#{ext}#find と言う名前にすべきとありますが、今回は vimrc に直接書いてしまいます。

function! GfFile()
  let path = expand('<cfile>')
  let line = 0
  if path =~# ':\d\+:\?$'
    let line = matchstr(path, '\d\+:\?$')
    let path = matchstr(path, '.*\ze:\d\+:\?$')
  endif
  if !filereadable(path)
    return 0
  endif
  return {
  \   'path': path,
  \   'line': line,
  \   'col': 0,
  \ }
endfunction
call gf#user#extend('GfFile', 1000)

expand('') で、本来の isfname でのファイルを取得した後、最後が :{数値} で終わっていた場合はそこを行番号として抜き出しています。
これで無事、Windows でもジャンプできるようになりました。

ちなみに既存の拡張として、gf-diff があります。これは diff 上で gf すると、その差分のオリジナルのファイルにジャンプできます。非常に便利です。

余談 - 更なる問題

ところで、gf で開く時に、微妙に上位層も見てくれると便利そうです。
例えば、カレントディレクトリが /foo/project/src だったとして、カーソルが、"src/hoge.c" と言うテキストの上にあった場合、通常なら gf すると /foo/project/src/src/hoge.c を探してしまって見付からないのですが、ここは気を利かせて /foo/project/src/hoge.c を開いて欲しいですよね。
先程の設定に1行追加します。

function! GfFile()
  let path = expand('<cfile>')
  let line = 0
  if path =~# ':\d\+:\?$'
    let line = matchstr(path, '\d\+:\?$')
    let path = matchstr(path, '.*\ze:\d\+:\?$')
  endif
  let path = findfile(path, getcwd() . ';')  " 追加
  if !filereadable(path)
    return 0
  endif
  return {
  \   'path': path,
  \   'line': line,
  \   'col': 0,
  \ }
endfunction
call gf#user#extend('GfFile', 1000)

これで無事、上の階層にあるファイルも開けそうです。
これらの機能は便利なので、プラグイン化して配布したいところなのですが、ここで問題が出てきます。
Windows で foo.c:23 に対応する機能」と、「上の階層を見にいく機能」は、どう考えても別物の機能なので、1つのプラグインに入れるのは不自然です。しかし、これらを別々のプラグインとして提供してしまうと、「上の階層を見に行きつつ foo.c:23 を Windows で開く」と言ったことができなくなってしまいます。
現状ではどうしようもなさそうなので私は vimrc に書いてしまっていますが、今後こういうのも可能になってくると更に便利になりそうです。