Vim で非同期で Clang を使用したコード補完を行う
この記事は Vim Advent Calendar 2012 277日目の記事になります。
さて、Vim は単体では非同期処理を行うことができません。
しかし、vimproc.vim プラグインを使用すれば外部コマンドを非同期で実行し、結果を後から(autocmd CursorHold 等で)受け取る事ができます。
これは quickrun.vim などでも利用されています。
今回はこれを使用して実験的に Clang のコード補完を非同期で行ってみました。
Clang はコンパイラの実行バイナリからコード補完の結果を取得することができます。
今回行った処理の具体的な流れは以下の様な感じです。
- . や :: を入力した時に外部コマンド clang を vimproc.vim を使用して非同期で実行させる
- clang がコード補完処理を行っている間は Vim のプロセスはブロックされない(入力可能)
- clang の処理が終了したら結果をコード補完としてポップアップする
また、今回の目的はあくまでも非同期でコード補完の処理を行うのであって、補完時間を短く(むしろ 'updatetime' によっていは長く)するわけではありません。
[必要なプラグイン]
reunions.vim はわたしが開発中の vimproc.vim をラップしたライブラリ系プラグインです。
これを使用すれば比較的簡単に非同期で外部コマンドを実行して後から結果を取得する事ができます。
せっかくなので、今回はこれを利用します。
また、絶賛開発中の為、今後仕様変更される可能性があります。
[ソースコード]
function! s:command(file, line, col, args) if !filereadable(a:file) return "" endif return printf("clang.exe -cc1 -std=c++11 -fsyntax-only %s -code-completion-at=%s:%d:%d %s", a:args, a:file, a:line, a:col, a:file) endfunction function! s:parse(output) return map(split(a:output, "\n"), 'matchstr(v:val, ''COMPLETION: \zs.*\ze :'')') endfunction function! s:tempfile(filename) if writefile(getline(1, "$"), a:filename) == -1 return "" else return a:filename endif endfunction function! s:include_opt() return "-I".join(filter(split(&l:path, ','), 'v:val !~ ''\./'''), ' -I') endfunction let s:complete_cache = {} function! CompleteClear() if exists("s:process") call s:process.kill() unlet s:process endif let s:complete_cache = {} endfunction function! CompleteStart() echo "Complete start." call CompleteClear() let s:complete_cache.pos = getpos(".") let tempfile = s:tempfile("clang_completion.cpp") let s:process = reunions#process(s:command(tempfile, line("."), col(".")+1, s:include_opt())) function! s:process.then(output, ...) let result = s:parse(a:output) if empty(result) echo "Not found." return endif let s:complete_cache.result = result call feedkeys("\<C-x>\<C-o>", 'n') unlet s:process endfunction let s:process.tempfile = tempfile let s:process.base_kill = s:process.kill function! s:process.kill() echom "Process killed." call delete(self.tempfile) call self.base_kill() endfunction " すぐに終了する場合は待ち時間を設定した方がよいかもしれない " call s:process.wait_for(0.5) endfunction function! Complete(findstart, base) if a:findstart if empty(s:complete_cache) echo "Empty cache." return -3 endif let pos = s:complete_cache.pos if pos[1] != line(".") || pos[2] >= col(".") echo "Failed cursor pos." return -3 endif return s:complete_cache.pos[2] endif echo "Complete end." return { "words" : filter(copy(s:complete_cache.result), 'v:val =~ "^".a:base') } endfunction function! CompleteSetup() " neocomplete.vim を無効に if exists(":NeoCompleteLock") :NeoCompleteLock endif " updatetime を短くする " set updatetime=500 setlocal omnifunc=Complete inoremap <silent><buffer> . .<Esc>:call CompleteStart()<CR>a inoremap <silent><buffer> :: ::<Esc>:call CompleteStart()<CR>a inoremap <silent><buffer> -> -><Esc>:call CompleteStart()<CR>a augroup clang-complete autocmd! autocmd InsertLeave <buffer> call CompleteClear() augroup END endfunction augroup cpp autocmd! autocmd FileType cpp call CompleteSetup() augroup END
[動作]
こんな感じですね。
コード補完を開始してから終了するまでの入力で絞り込みが行われるのはよいですね。
これは clang_complete にはない機能です。
また、今回使用したコードは 'updatetime' に設定されている時間に依存します。
試してみる場合は、'updatetime' の設定を小さくしてみるとよいでしょう。
と、いうような感じで外部コマンドでコード補完を処理するのであればこのように非同期でコード補完を行うことが出来ました。
ただし、外部コマンドの出力結果をパースする場合は Vim 側で行う必要があるのでそこら辺はもう少し工夫する必要がありますね。
(関数のシグネチャを表示するとか。
Clang 以外でも外部コマンドを使用してコード補完を行う場合は流用できると思います。