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 以外でも外部コマンドを使用してコード補完を行う場合は流用できると思います。