Vim で C++ の宣言箇所へ飛ぶ


上記のサイトで初めて知ったんですが、llvm/clang に含まれている c-index-test という tools を使用すれば C++ で宣言位置を取得する事が出来るみたいなので、Vim script で簡単に実装してみました。
c-index-test.exe は、llvm/clang をビルドすれば出力されています。
clang 3.1(trunk) でテストしましたが多分他のバージョンでも問題ないと思います。
(ただし、C++11 のコードの場合は clang のバージョンに依存するので注意して下さい。

[動作環境]

  • clang
  • c-index-test
    • llvm/clang をビルドすれば生成されます
  • neocomplcache
    • インクルードされているヘッダーファイルを取得するために使用

[インクルードディレクトリ]

必要なインクルードディレクトリは set path に追加して下さい。
s:cpp_declared_options に直接 -I を書いても問題ありませんが、その際は neocomplcache 側の設定もおこなって下さい。

[ソース]

" c-index-test.exe のパス
let s:cpp_declared_c_index_test_cmd="c-index-test.exe"

" c-index-test のオプション
let s:cpp_declared_options = " -std=gnu++0x "

" 1 にすると c-index-test.exe の結果をコマンドに出力します
let s:cpp_declared_debug=0


function! s:c_index_test(filename, lnum, col)
    let file = a:filename
    let file_path = fnamemodify(a:filename, ":p")
    let options = join(map(split(&path, ","), "'-I'.v:val"), " ")
    let cmd = s:cpp_declared_c_index_test_cmd." -cursor-at=".file_path.":".(a:lnum).":".(a:col)."  ".options." ".s:cpp_declared_options." ".file
    return system(cmd)
endfunction


function! s:parse_declared_pos(str)
    let line = a:str
    let mx='\(\f\+\):\s*\(\d\+\):\s*\(\d\+\)'
    let l = matchstr(line, mx)
    let file = substitute(l, mx, '\1', '')
    let lnum = substitute(l, mx, '\2', '')
    let col = substitute(l, mx, '\3', '')

    if empty(lnum) || empty(col)
        return {}
    endif
    return { "lnum" : lnum+0, "col" : col+0 }
endfunction


function! s:cpp_declared(filename, lnum, col)
    let result = s:c_index_test(a:filename, a:lnum, a:col)
    if s:cpp_declared_debug
        echo result
    endif

    let declared_pos = s:parse_declared_pos(result)
    if empty(declared_pos)
        return {}
    endif

    let func_name = expand("<cword>")
    let includes = neocomplcache#sources#include_complete#get_current_include_files()
    for header in includes+[a:filename]
        let lines = readfile(header)
        if len(lines) >= declared_pos.lnum
\        && len(lines[declared_pos.lnum-1]) >= declared_pos.col
\        && match(lines[declared_pos.lnum-1], func_name, declared_pos.col-1) == declared_pos.col-1
            return {
        \        "funcname" : func_name,
        \        "lnum" : declared_pos.lnum,
        \        "col" : declared_pos.col,
        \        "filename" : header
        \    }
        endif
    endfor

    return {}
endfunction


function! s:cpp_declared_open(open_cmd, filename, lnum, col)
    echo "Search declared..."
    let declared = s:cpp_declared(expand("%"), getpos(".")[1], getpos(".")[2])
    if empty(declared)
        echo "Not found"
        return
    endif
    if a:filename != declared.filename
        execute a:open_cmd." ".declared.filename
    endif
    
    call cursor(declared.lnum, declared.col)
endfunction

" コマンド
" 自分が使いやすいように open_cmd を追加したりして下さい
command! -nargs=0 CppDeclaredOpen :call <SID>cpp_declared_open("buffer", expand("%"), getpos(".")[1], getpos(".")[2])

command! -nargs=0 CppDeclaredOpenTabDrop :call <SID>cpp_declared_open("tab drop", expand("%"), getpos(".")[1], getpos(".")[2])

" Visual Studio っぽいキーマッピング
nnoremap <silent> <F10> :CppDeclaredOpenTabDrop<CR>

とりあえず、生のソースコードです。
vimrc あたりにコピーして使用して下さい。
:CppDeclaredOpen でカーソル下の単語の宣言位置を開きます。
動作は多少遅いですが、精度はそこそこ。
まだコードは荒削りなので、そのうちちゃんとした Vimプラグインとして書きなおす予定。
と、いうか c-index-test で宣言の位置は出力されるんだけど宣言のヘッダーファイル名は出力されないんですかね…。

[注意]

コンパイルが通るソースコードでなければ動作しません。
例えば、

func(    // これだけで func の位置へは飛べない

ではコンパイルに失敗してしまい動作しないので引数までちゃんと定義する必要があります。

let s:cpp_declared_debug=1

で、c-index-test の出力結果を確認する事が出来るので、動作しない場合はこれで確認をおこなって下さい。


その他、わからないことがあればコメントか Twitter までお願いします。
あと今気づいたけどプリプロセッサを使って関数を生成している場合だと検索に引っかからないので動作しないですね…。
うーん、どうしようかな。