Ctagsを使って、クラス名や関数名のインデックスを記述したtagsファイルを作っておくと、vimでキーワード上にカーソルをおいて、を打つと、そのキーワードが定義された場所に飛んでくれて便利。

しかし、新たにキーワードが増えるたびにctagsを手動で実行するは面倒なのでGitのhookを使うことにする。

また、複数のプロジェクトがあるとき、各プロジェクトごとにtagsファイルを分けておくと、余計な候補が出なくてよいが、うまくやらないとプロジェクトを変えるたびに:set tagsでtagsファイルの指定を変更なくてはならないし、tagsファイルをGitのディレクトリ以下に置くと、.gitignoreで無視してやらなくてはならなかったりするので、その辺を解決させる。

まぁ、vimプラグインで有名なtpopeのtbaggery – Effortless Ctags with Git設定をほぼ訳した感じで、Vim – ctagsと連携するように環境を構築する – Qiitaでかなりまとめられている話しであるが…。

hookをつくる

まず、gitのバージョンが1.7.1以上であれは、git initやcloneした時に、templatedirで指定したディレクトリ以下のファイルを.git以下にコピーしてくれるのでそれを作る。

git config --global init.templatedir '~/.git_template'
mkdir -p ~/.git_template/hooks

次にctagsを実行するシェルスクリプトを~/.git_template/hooks/ctagsというファイル名で作る。

#!/usr/bin/env bash
set -e
PATH="$HOME/local/bin:/usr/local/bin:$PATH"
cd "$(dirname "${BASH_SOURCE:-$0}")"; cd ../../
trap "rm -f .git/tags.$$" EXIT
ctags --tag-relative -R -f .git/tags.$$ --exclude=.git
mv .git/tags.$$ .git/tags

$PATHはお好みで。

cdの部分で、gitのルートディレクトリに移動している( bash/zshでsourceされたスクリプト内で、ファイル自身の絶対パスをとるシンプルな記法 – Qiita)。hookで使われる場合は実行ディレクトリはgitのルートディレクトリなので必要ないが、それ以外でも実行したい場合に必要。

trapの部分は、このシェルスクリプトが終了時したときにtags.$$という一時ファイルを消すという意味。

$$はこのシェルスクリプトのPID。同時にシェルスクリプトが実行された時のことを考慮してる?

5行目でctagsを実行している。

–tag-relativeは生成するtagsファイルに記すパスをtagsファイルからの相対パスにするオプション。これをつけないと、ctagsを実行したディレクトリからの相対パスになってしまい、うまく扱えなくなる。ちなみにこのオプションを使う代わりにctagsの引数に` pwd`をつけるとtagsファイル内のパスが絶対パスになり、tagsファイルを移動させても、問題がなくなる。

-Rはディレクトリ以下から再帰的にインデックスを作るオプション

-fは作るtagsファイルの名前の指定。指定しないとtagsというファイル名になる

–exclude=.gitは.gitディレクトリ以下のファイルをインデックスの対象外にするオプション。

最後に一時ファイルを.git/tagsにリネームしている。.git以下にtagsファイルを置くことで.gitignoreで無視する手間を省いている。

gitディレクトリ以下だけでなく、他のディレクトリのファイルのインデックスを加えたいなら、このシェルスクリプトに-a(–apend=yes)のオプションをつけておき、さらに手動で

ctags --tags-relative -aR -f <gitディレクトリ>/.git/tags <他のディレクトリ>

みたいに実行する。

スクリプトを手動で実行するには

git config --global alias.ctags '!.git/hooks/ctags'

とaliasを設定しておくと、git ctags で実行できる。

で、このスクリプトを実行する hookを作る。

.git_template/hooks以下にpost-checkout, post-commit, post-mergeをいうファイルを作り、それぞれに

#!/bin/sh
.git/hooks/ctags >/dev/null 2>&1 &

と書いておく。post-rewriteはgit commit –amendとgit rebaseを行った時に発火されるが、前者はpost-commitでカバーされているので、rebase 使用時に限定する。

#!/bin/sh
case "$1" in
  rebase) exec .git/hooks/post-merge ;;
esac

あとは、ファイルに実行権限を付けて、git cloneするか、すでにリポジトリがあるなら、git initすれば、.git/hooks以下にファイルがコピーされる。

set tagsの設定

デフォルトだと

:set tags
  tags=./tags,tags

なので、カレントバッファのファイルがあるディレクトリにtagsファイルがあるかを探して、なければカレントディレクトリ(変更しなければvimを立ち上げたディレクトリ)のtagsファイルを探す。

例えば、

:set tags+=.git/tags

とすると、カレントディレクトリがgitのルートディレクトリでないとtagsファイルと見つけてくれない。

tpope作のGitを扱うvimプラグイン、fugitiveをいれると、かってにtagsのパスを設定してくれる(Why does fugitive modify my tags variable? · Issue #104 · tpope/vim-fugitive)。

:verbose set tags
  tags=~/git/tmsanrinsha/dotfiles/.git/vim.tags,~/git/tmsanrinsha/dotfiles/.git/tags,./tags,tags
        Last set from ~/.vim/bundle/vim-fugitive/plugin/fugitive.vim

これは自分のdotfilesリポジトリの.vimrcを開いた状態での設定例だが、.git以下の$lt;filetype$gt;.tagsとtagsを絶対パスで指定しているので、カレントディレクトリを気にする必要がない。(実はこのverbose set tagsでfugitiveが設定していることを知り、上のisuueからtpopeのブログに辿り着いた)

ファイルを保存した時もCtagsを実行する

tpopeのブログのコメント欄に書いてあるが、fugitiveをインストールしているなら、b:git_dirという変数が設定されるので

autocmd BufWritePost *
      \ if exists('b:git_dir') && executable(b:git_dir.'/hooks/ctags') |
      \   call system('"'.b:git_dir.'/hooks/ctags" &') |
      \ endif

と.vimrcに書いてやると良い。

補足

unite-tagを使うと、タグを絞り込めなかった時にuniteインターフェースで絞れて良い。

ctagsは開発がずっと止まっていたので、フォークされたb4n/ctagsのリポジトリの各ブランチを使うと、機能が追加されているのでよい。

PHPブランチの変更点や、インストールの仕方が、vimのPHPのomni補完をを向上させるプラグイン、phpcomplete.vimのWiki(Patched ctags · shawncplus/phpcomplete.vim Wiki )に書いてあるので参考にすると良い。

古いものを使う場合は、PHPのクラス定数などがインデックスされないので。.ctagsに以下のように正規表現で指定してやると、インデックスすることができる。

--append=yes
--tag-relative
--recurse=yes
--php-kinds=cidfv
--exclude=.svn
--exclude=.git
--langmap=PHP:+.inc.tpl
--regex-php=/^[ \t]*const[ \t]+([a-z0-9_]+)/\1/d/i
--regex-php=/abstract class ([^ ]*)/\1/c/
--regex-php=/interface ([^ ]*)/\1/c/
--regex-php=/(public |static |abstract |protected |private )+function ([^ (]*)/\2/f/