圧倒亭グランパのブログ

30年後の自分にもわかるように書くブログ

vimの関数ジャンプのかゆいところをMakefileと.vimrcで解決する

最近寒いので毛布を出した @at_grandpa です。

みなさん、関数ジャンプしてますか?

してますよね!

今までエラーで挫けていて導入していなかったのですが、最近本腰いれて解決に臨み、結果、素晴らしいライフチェンジングになりました。

偉大な先人の方々のツールは素晴らしい!巨人の肩の上に乗りまくりましょう。

ctags + vim

ググればたくさん出てきますが、簡単に導入方法と紹介を。

インストール

[debian系]
sudo apt-get install exuberant-ctags

[CentOS/RedHat系]
yum install ctags

[mac]
brew install ctags

tagsの生成

cd /path/to/target_dir
ctags -R

これで、tags というファイルが生成されます。

vimの設定

vimに読み込ませるtagsファイルを指定します。

:set tags=./tags;

;は、「親ディレクトリを探していく」というもので、tagsをプロジェクトのルートディレクトリに置いておけば参照できます。

ジャンプ!

/path/to/target_dir 以下で関数ジャンプが可能になります。

ジャンプしたいキーワードにカーソルを合わせ、<C-]> でジャンプします。

戻るのは<C-t>

これであなたもライフチェンジング。

 

かゆいところ

ですが、使っていて微妙にかゆいところがあるんですね。

蚊に刺されるほどかゆくはない。

少し布に触れてかゆくなる、そんな程度。

 

自分の環境は、結構大きなプロジェクトのリポジトリでして、複数言語が混じって構築されているんですね。

ここで ctags -R をすると、ctags君は一途にデータを集めて、対応言語全てで「たったひとつのtagsファイル」を作ってくれます。

こうなると、全言語が入り混じっているtagsファイルなので、ジャンプ候補に他言語が出てきてしまうのです。

例えばこんな感じです。(この候補から一番左の番号を選んでジャンプします)

  # pri kind tag               file
  1 F   f    FunctionName     /path/to/file.php
               public static function FunctionName() {
  2 F   m    FunctionName     /path/to/file.rb
               class:ClassName
               module FunctionName
  3 F   s    FunctionName     /path/to/file.pl
               sub FunctionName {
...

ちょっとかゆいですよね。

(「機能別に言語が分かれているだろうし、同じ名前を付けるのはナンセンス」というご意見もあるでしょうが、今は触れないこととします。)

 

Makefileと.vimrcで解決

やりたいことは「ジャンプ候補リストに他言語が混じらないようにする」です。

今回は、Makefile : 40行 + .vimrc : 15行 で解決しました。

 

Makefile

Makefileの役割は「言語別にtagsファイルを生成する」です。

言語別にtagsを生成すれば、ジャンプ候補リストに他言語が混じることはありません。

以下にその40行を示します。

# ----------------------------------------------------
#  tagを生成する
# ----------------------------------------------------

# 言語
# see also `ctags --list-languages`
lang := PHP    \
        Ruby   \
        Python \
        Perl

lower_lang := $(shell echo $(lang) | tr A-Z a-z)

# 各言語のtag対象ファイルの拡張子
# see also `ctags --list-maps`
ext := default       \
       .rb.ruby.spec \
       default       \
       default

TARGET_PATH  = $(PWD)  # ここは基本的に書き換える
git_toplevel = $(shell cd $(TARGET_PATH);git rev-parse --show-toplevel)
seq          = $(shell seq 1 $(words $(lang)))

ifeq ($(git_toplevel),)
    # gitリポジトリ管理ではない場合
    tags_save_dir = $(realpath $(TARGET_PATH))/tags
    tags_target_dir = $(realpath $(TARGET_PATH))
else
    # gitリポジトリ管理である場合
    tags_save_dir = $(HOME)/dotfiles/tags_files/$(shell basename $(git_toplevel))
    tags_target_dir = $(git_toplevel)
endif

.PHONY: create_tags $(seq)

create_tags: $(seq)

$(seq):
  mkdir -p $(tags_save_dir)
  ctags -R \
      --languages=$(word $@,$(lang)) \
      --langmap=$(word $@,$(lang)):$(word $@,$(ext)) \
      -f $(tags_save_dir)/$(word $@,$(lower_lang))_tags $(tags_target_dir)

これは、

make -f /path/to/Makefile create_tags TARGET_PATH=./

とすることで、カレントディレクトリ以下について、各言語($(lang)で指定してある言語)のtagsファイルを生成します。

こんな感じ。

ls ~/dotfiles/tags_files/{リポジトリ名}/
perl_tags
php_tags
python_tags
ruby_tags

簡単に言うと、以下のことをやっています。

  • $(lang)に設定された言語についてtagsファイルを生成
  • tagsファイルの名前は {言語名}_tags
  • git管理のリポジトリでない場合、カレントディレクトリに各tagsを配置
  • git管理のリポジトリである場合、~/dotfiles/tags_files/{リポジトリ名}/*に各tagsを集約

これだけです。

これで言語別にtagsファイルを生成できました。

.vimrc

.vimrcの役割は「ファイル毎に適切なtagsファイルを設定する」です。

以下にその15行を示します。

" ファイルタイプ毎 & gitリポジトリ毎にtagsの読み込みpathを変える
function! ReadTags(type)
    try
        execute "set tags=".$HOME."/dotfiles/tags_files/".
              \ system("cd " . expand('%:p:h') . "; basename `git rev-parse --show-toplevel` | tr -d '\n'").
              \ "/" . a:type . "_tags"
    catch
        execute "set tags=./tags/" . a:type . "_tags;"
    endtry
endfunction

augroup TagsAutoCmd
    autocmd!
    autocmd BufEnter * :call ReadTags(&filetype)
augroup END

以下のことをやっています。

  • BufEnterReadTags(&filetype)が発動
    • バッファに入ったときに発動。ウィンドウ移動やタブ移動などで発生。
  • execute "set tags=..."で読み込むtagsファイルを切り替え
    • git管理されているなら ~/dotfiles/tags_files/{リポジトリ名}の言語別のtagsを指定
      • &filetypeを使用することで、ファイルの言語別に読み込みが可能
    • git管理されていないなら、ルートディレクトリにある各言語のtagsを指定する

これで、ファイルの種類の応じて適切なtagsファイルを設定できます。

 

その他の設定

かゆいところを無くすために、キーマッピングも設定しました。

set notagbsearch

" [tag jump] カーソルの単語の定義先にジャンプ(複数候補はリスト表示)
nnoremap tj :exe("tjump ".expand('<cword>'))<CR>

" [tag back] tag stack を戻る -> tp(tag pop)よりもtbの方がしっくりきた
nnoremap tb :pop<CR>

" [tag next] tag stack を進む
nnoremap tn :tag<CR>

" [tag vertical] 縦にウィンドウを分割してジャンプ
nnoremap tv :vsp<CR> :exe("tjump ".expand('<cword>'))<CR>

" [tag horizon] 横にウィンドウを分割してジャンプ
nnoremap th :split<CR> :exe("tjump ".expand('<cword>'))<CR>

" [tag tab] 新しいタブでジャンプ
nnoremap tt :tab sp<CR> :exe("tjump ".expand('<cword>'))<CR>

" [tags list] tag list を表示
nnoremap tl :ts<CR>

自分の直感に近い感じで設定できたので、始めからストレス無く活用できました。

 

良かった点

tags読み込みの自動切り替えやキーマッピングを設定して、最終的に良かった点は以下です。

  • ジャンプ候補リストに他言語の候補が出なくなった
  • 既存リポジトリ内にtagsファイルを置くのではなく、dotfiles/tags_files/{リポジトリ名}に集約することで管理しやすい
  • .gitignoretagsと書かなくて良い
  • 直感的操作でタグジャンプできた
  • コマンド一発で設定できる

これでコードを読むストレスが格段に減り、いろんなコードを読みたくなりました。

これは良い傾向。

これらのコードはgithubに上げてあります。

github.com

 

もしかしたら車輪の再発明

もしかしたら、もっともっと簡単に解決できる方法があるかもしれません。

実は車輪の再発明だったりして...

良い方法、ご存知の方いらっしゃいましたら、コメントやTwitterで是非つぶやきましょう。
(確かneobundleのプラグインでtags系のものがあったと思いますが、vimが重くなったのでやめた記憶)

個人的にはvimscriptと戯れることができたので面白かったです。

時には車輪の再発明をしてみるのも良い経験になりそうですね。

 

周りのエンジニアに関数ジャンプについて聞いてみると、

  「IntelliJ IDEA 使ってるよー

IDE...

なにそれ美味しいの?( ^ω^)おっおっ

 

美味しいらしい...

味見してこよう...λ............トボトボ