Vim
ctags
aratanaDay 13

CtagsでVimにタグジャンプ機能を追加する

ad_banner4.gif
このエントリーは、aratana Adventカレンダー13日目のエントリーです。

はじめまして!!
新卒一年目、Vim一年生の田村です。

前日は、@mt-kageさんの「はじめてのServerless ✕ Webpack ✕ TypeScript」というエントリーでした。
サーバーレスと聞いて、「サーバーはいらないけど、筋肉は必要」という迷言を思い出したのは私だけでしょうか。

私日々のコーディングは、Vimで行っており、操作や動作などで不満が出れば、解決しvimrcを育成しております。(vimrc Breederってやつです。聞いたこと無い

コードの処理を追っている中で、必ずと言ってもよいほど利用する定義元へのジャンプ
主要言語はだいたいプラグインを導入すればジャンプできますが、
マイナー言語のほとんどに、プラグインが作られておらず、定義元ジャンプができません。
grep使えばいいじゃんって声も聞こえて来そうですが、色々工程省ける定義元ジャンプのほうが(ry...
そこで、定義元ジャンプを使えるようにするCtagsを利用していきます。

やること

  • 標準で対応していない定義元ジャンプの設定
  • Vimで定義元ジャンプを実感!!
  • 楽するためのVim設定(関数とか書いてみる)

事前準備

  • Vim(まー標準で入っているでしょう)
  • Ctags(パッケージマネージャーなどで入れましょう)
  • ソースコードNaspSampleをダウンロード(DL直リンク
  • 解凍したNaspSampleディレクトリにいる状態 In 黒い画面
  • あ、対象は、Linux, Macとなっております(Windows翻訳できる方を除く)
  • ちょっとVimを触れる

ちなみに、Ctagsは

の2種類あります。

exuberant-ctagsの方は、更新が途絶えており、
universal-ctagsは、現在もメンテナンスされ続けています。
違いとしては、設定ファイルの場所とか、対応言語などです。
やっぱりオススメは、メンテナンスされているuniversal-ctagsですね。

Macなら、

exuberant-ctags
$ brew install ctags
universal-ctags
$ brew install --HEAD universal-ctags/universal-ctags/universal-ctags

universal-ctagsの主張が激しい。

事前vimrc

$HOME/.vimrcファイルに以下を追加設定しておいてください。
(文字コードを正しく認識して、動作不良を起こさないため)
※ 既に設定済みって方は不要です。

.vimrc
set fileformats=unix,dos,mac
set fileencodings=utf-8,sjis

Ctagsというすごいやつ

Ctagsは、ソースファイルを解析してインデックスファイルを作成するプログラムです。
簡単に言い換えれば、関数や変数などの定義元へジャンプするためのファイルを作成するプログラムです。
標準でいくつかのプログラム言語に対応しています。

Ctagsを使ってみよう

実際に標準のCtagsを使ってみましょう!!
NaspSampleディレクトリで以下のコマンドを実行してから、
インデックスファイル(以後タグファイル)を作成できます。

bash
$ ctags -R -f .tags

.tagsファイルをエディタで開いて中身確認しても良いかも)

コマンド解説

  • ctagsctags実行。(そのまんま)
  • -Rは、現在ディレクトリから再帰的にファイルを解析していくオプション指定。 つまり、全ファイル見ますモード
  • -f .tags出力ファイル名を設定するオプション。(デフォルトは、tags

たった、これだけで便利な機能が使えるようになっちゃいます。

Vimでジャンプしてみる

では、先程作成したタグファイルを元にVimで定義元ジャンプを実感しましょう!!

やり方

  1. vim script/@ImgList.aspで該当ファイルをVimで開く
  2. :set tags=.tagsでタグファイルのパスを指定して
  3. 50gg OR 50Gで、50行へ飛ぶ
  4. 文字列rstにカーソルが乗った状態で、Ctrl-]でジャンプします。

定義元のdim rst:へ移動していると思います。
(そんなんgdでもいけるやんは、聞こえなかったことにします:thinking::ear:
正直このレベルでは、大したことないですね〜。

標準の限界

今回、ジャンプさせたい定義元を確認してみます。
script/@ImgList.aspファイルの28行(つまり、28gg OR 28G

script/@ImgList.asp
function OnDraw_Images(oNasp, vParam)

呼び出し元も確認してみます。
wwwroot/ImgList.aspの23行

wwwroot/ImgList.asp
    (#%Images|

です。ここで、OnDraw_Imagesを呼び出しています。
Vimを一回落としている場合は、
:set tags=.tagsをしましょう。

Imagesにカーソル合わせて、Ctrl-]でジャンプ!?

ん?なんかエラー出ていますね。
E426: tag not found: Images Imagesというタグ無いですよって言われています。
Ctagsの標準では対応していないようですね。
では、Ctagsの設定をカスタマイズして飛べるようにしましょう!!

Ctagsカスタマイズ

Ctagsの設定ファイルを変更して、カスタマイズできます。
Ctagsの設定ファイルは、

  • exuberant-ctagsの場合は、$HOME/.ctags
  • universal-ctagsの場合は、$HOME/.ctags.d/configure.ctags

configure部分はなんでも良いっぽいです。

となっています。インストした方に合わせましょう。

考えられる解決方法は、

  1. OnDraw_Imagesをタグファイルへ登録
  2. ImagesOnDraw_Imagesを関連付ける

Ctags設定追加

では、設定ファイルに以下の一行を追加しましょう。

--regex-asp=/^[ \t]*(function)[ \t]+OnDraw_([a-zA-Z0-9_]+)/\2/r,ondraw/

この処理の意味をまとめると、

  • aspという言語に対してパターン追加
  • function OnDraw_hogehogeに一致する行をhogehogeとしてタグファイルに追加
  • ondrawというタグ種類で登録(識別子r

\2は、([a-zA-Z0-9_]+)を指しており、つまりhogehogeです。

これで、OnDraw_Imagesの箇所をタグファイルへ登録でき、Imagesで関連付け完了しました。

カスタマイズ後の確認

では、先程と同じことしてジャンプできるか確認してみましょう。

  1. ctags -R -f .tagsでタグファイル更新
  2. vim wwwroot/ImgList.asp
  3. :set tags=.tags
  4. 23ggで23行へ飛ぶ
  5. Images上でCtrl-]でジャンプ!!

パっと変わったと思いますパっと!!
ちなみに、Ctrl-tで元に戻れます。
Vimのタグジャンプは、スタックとして履歴残る感じなので、
Ctrl-]でpushして、Ctrl-tでpopする的な感じです。

無事ジャンプさせることができましたね!!
これを応用していけば、飛ばしたい場所に自由に飛べます。
IDEにも劣らぬジャンプ機能を手に入れることができました!!

ちなみに

インサートモードで、<Ctrl-x><Ctrl-]>を入力すると、タグリストの補完できます。
例えば、OnDrawまで入力した後に、<Ctrl-x><Ctrl-]>で、補完一覧がでてきます。
設定次第では、WordPressのアクションフック名の補完もできたりします。

問題点

ただ、いくつか面倒なところがありますね。。。

  • Vimを立ち上げる度に:set tags=.tags
  • ソースコードを変更する度に、タグファイル生成コマンド実行

さらに、極めつけには、ターミナル上でwwwrootへ移動した後、VimでImgList.aspを立ち上げ、同じようにすると、ジャンプ出来ません。
set tags=.ctagsと記入しているので、カレントディレクトリ直下の.tagsを探しており、wwwroot直下には、見つからないのでこのような現象になっています。こんなの使い物にならないじゃん!!

課題

とりあえず、現状の課題をまとめて見ました。

  • Vim立ち上げるたびset tags=.tags
  • タグファイルがあるディレクトリで、Vim立ち上げないとタグファイル読み込めない
  • ソース変更するたび、手動でインデックスファイル作り直し

では、これらすべて解決しましょう!

課題の解決

課題に対して一つずつ解決していきます!

vimrc設定

$HOME/.vimrcに記述すれば、Vim立ち上げ時に読み込まれるので、.vimrcに、set tags=.tagsを追加しましょう。

これで、立ち上げと一緒に実行されます。

が、しかし、カレントディレクトリにタグファイルがないといけません問題。

解決できそうな動作としては、
親ディレクトリを遡り.tagsがあるか探してから、あればそれを読み込む的な動作ができれば完璧ですね。
この動作は、Vimの標準でUpward searchあるので、それ使いましょう。
詳しくは、:h file-searchingの項目に書いてます。

以下の記述で、カレントディレクトリから、ホームディレクトリまで.tagsを探します。便利〜。

.vimrc
set tags=.tags;$HOME

OR

.vimrc
set tags=.tags;~

に変更しましょう。お好きな方で。

自動コマンド設定

次は、毎回タグファイル作るの面倒問題。
Vimには、指定イベントが行われたら自動的に実行されるコマンドを指定できる、
autocmdなるコマンドがあります。
これ使いましょう。

.vimrc
augroup ctags
  autocmd!
  autocmd BufWritePost * silent !ctags -R -f .tags
augroup END
  • augroupautocmd!は、簡単に説明すると、複数回autocmdが登録されるのを防ぐやつです。 詳しくは、おさらい autocmd/augroup
  • ファイル書き込み後にsilent !ctags -R -f .tags実行しますという意味
  • silentは、メッセージ出力させないよ。出力いらない。:spy:
  • !ctags -R -f .tags先頭にビックリマークで、外部プログラム実行するという意味

さらなる問題とその解決

これで、保存するたびインデックスファイル更新されるーっと思ったけど、
カレントディレクトリが変わったら、その直下に.tagsできちゃう問題発生。
これじゃ、ディレクトリ移動して保存するたびに.tags生まれちゃう!!

安心して作業できませんね。
読み込んでる.tagsファイルパスと同じ場所で生成できるようにしたいですね。

てことで関数作りました。

.vimrc
function! s:execute_ctags() abort
  " 探すタグファイル名
  let tag_name = '.tags'
  " ディレクトリを遡り、タグファイルを探し、パス取得
  let tags_path = findfile(tag_name, '.;')
  " タグファイルパスが見つからなかった場合
  if tags_path ==# ''
    return
  endif

  " タグファイルのディレクトリパスを取得
  " `:p:h`の部分は、:h filename-modifiersで確認
  let tags_dirpath = fnamemodify(tags_path, ':p:h')
  " 見つかったタグファイルのディレクトリに移動して、ctagsをバックグラウンド実行(エラー出力破棄)
  execute 'silent !cd' tags_dirpath '&& ctags -R -f' tag_name '2> /dev/null &'
endfunction

これを、保存毎に実行するようにすれば良いので、
autocmd BufWritePost * call s:execute_ctags()
に変更します。
これで、快適にタグジャンプ生活を送れます。

現状のままだと、execute_ctags関数は、.ctags無かったら何もしないようになっています。
解決方法として、.ctagsが見つからなかった場合、

  • プロジェクトのルートに自動生成するようにする(.gitと同じ階層とか)
  • カレントディレクトリに生成するようにする(以降、生成したタグファイルを更新)

など、自分好みに改良していただければと思います。

.vimrc

.vimrc
set fileformats=unix,dos,mac
set fileencodings=utf-8,sjis

set tags=.tags;$HOME

function! s:execute_ctags() abort
  " 探すタグファイル名
  let tag_name = '.tags'
  " ディレクトリを遡り、タグファイルを探し、パス取得
  let tags_path = findfile(tag_name, '.;')
  " タグファイルパスが見つからなかった場合
  if tags_path ==# ''
    return
  endif

  " タグファイルのディレクトリパスを取得
  " `:p:h`の部分は、:h filename-modifiersで確認
  let tags_dirpath = fnamemodify(tags_path, ':p:h')
  " 見つかったタグファイルのディレクトリに移動して、ctagsをバックグラウンド実行(エラー出力破棄)
  execute 'silent !cd' tags_dirpath '&& ctags -R -f' tag_name '2> /dev/null &'
endfunction

augroup ctags
  autocmd!
  autocmd BufWritePost * call s:execute_ctags()
augroup END

ちなみに...

Tagbarというプラグインを入れると、右とかにタグ登録されたリストが表示され、選択することでその場所へ飛べます。

tagbar.png

つまり、CtagsとTagbarの設定をカスタマイズすることで、表示させたいリストを自分好みに変更することができるようになります。
CSSなら、セレクタ一覧とか。

さらに画像の左下に、現在カーソルがどこの関数に含まれるか表示させることもできます。
便利ですね。

最後に

思っていたよりもたくさん書いてしまいました。
CtagsとVimの素晴らしさを少しでも感じて頂けましたでしょうか。
Ctagsをカスタマイズして快適なVimジャンプライフを送りましょう。
これからも、さらにvimrcを育成していきます。

明日(14日目)のaratana Advent Calendar 2017のエントリーは、期待の新人@sakochiさんのエントリーです!お楽しみに!