Vim scriptのエラーメッセージをパースしてquickfixに表示する

この記事は [Vim Advent Calendar 2016](http://qiita.com/advent-calendar/2016/vim) の6日目の記事です。

Vim scriptのエラーが出たときにエラーをquickfixに表示して、エラー箇所に飛べるといいなと思いますが、標準の機能ではないようなので、コマンドを作りました。

# 先行事例

調べてみると、kanno_kannoさんが書いた記事

- [Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会](http://kannokanno.hatenablog.com/entry/20120723/1343147069)
- [関数対応版:Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会](http://kannokanno.hatenablog.com/entry/20120724/1343146895)
- [辞書関数対応版:Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会](http://kannokanno.hatenablog.com/entry/20120726/1343321506)

や、Stack Exchange

- [Is it possible to populate the quickfix list with the errors of vimscript functions? - Vi and Vim Stack Exchange](http://vi.stackexchange.com/questions/6019/is-it-possible-to-populate-the-quickfix-list-with-the-errors-of-vimscript-functi)

に参考となる方法が書いてありました。

kanno_kannoさんの記事では自らsourceしたときにでるエラーメッセージをquickfixに表示する方法が、Stack Exchangeにはtry catchで例外をキャッチしてquickfixに表示する方法が書いてありました。
それらを参考にして、すでにエラーメッセージとして出たエラーを`:messages`で取得し、quickfixに表示するコマンドを作ります。

# エラーメッセージの構造

まず、エラーを出すために、以下のようなファイルを用意します。

```vim:error.vim
one

function! Hoge()
    two
endfunction
call Hoge()

function! s:fuga()
    three
endfunction
call s:fuga()

function! s:piyo()
  call s:fuga()
endfunction
call s:piyo()


let s:dict = {}
function! s:dict.aaa()
    four
endfunction
call s:dict.aaa()
```

これを`:source error.vim`してやると

```
/path/to/error.vim の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません: one
function Hoge の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     two
function <SNR>253_fuga の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     three
function <SNR>253_piyo[1]..<SNR>253_fuga の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     three
function 250 の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     four
```

のようなエラーメッセージが出ます。Vimのエラーは

1. ○○ の処理中にエラーが検出されました:
2. 行   <行番号>:
3. <エラー番号>: <内容>

一つの単位であることがわかります。

それぞれのエラーを詳しく見ていきます。

```
/path/to/error.vim の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません: one
```

1つ目のエラーは関数の中に入っていないときに例外が発生したときのエラーです。この場合は○○の部分はファイル名になり、行の部分は例外が発生したファイル内の行番号になります。

```
function Hoge の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     two
```

2つ目のエラーはグローバル関数の中で例外が発生したときのエラーです。この場合は○○の部分は関数名になり、行の部分は例外が発生した関数内の行番号になります。

```
function <SNR>253_fuga の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     three
```

3つ目のエラーはスクリプトローカルな関数の中で例外が発生したときのエラーです。関数名のs:が`<SNR>数字_`に変換されて表示されます。

```
function <SNR>253_piyo[1]..<SNR>253_fuga の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     three
```

4つ目のエラーは関数内で呼び出した関数内で例外が発生したときにエラーです。[]の中に例外が発生した関数を呼び出した行番号が入り、..でつながります。行の部分は例外の発生源の関数内の行番号です。例外の発生源がさらに深い場合は..でさらにつながります。

```
function 250 の処理中にエラーが検出されました:
行    1:
E492: エディタのコマンドではありません:     four
```

5つ目は辞書関数で例外が発生したときのエラーです。関数名は数字になります。

# 作ったコマンド

これらのエラーをパースして、quickfixに表示するためのVim scriptが以下になります。

```vim
function! s:qf_messages()
  let str_messages = ''
  redir => str_messages
  silent! messages
  redir END

  let qflist = s:parse_error_messages(str_messages)
  call setqflist(qflist, 'r')
  cwindow
endfunction

function! s:parse_error_messages(messages) abort
    " 戻り値。setqflistの引数に使う配列
    let qflist = []
    " qflistの要素になる辞書
    let qf_info = {}
    " qflistの要素となる辞書の配列。エラー内容がスタックトレースのときに使用
    let qf_info_list = []
    " 読み込んだファイルの内容をキャッシュしておくための辞書
    let files = {}

    if v:lang =~# 'ja_JP'
        let regex_error_detect = '^.\+\ze の処理中にエラーが検出されました:$'
        let regex_line = '^行\s\+\zs\d\+\ze:$'
        let regex_last_set = '最後にセットしたスクリプト: \zs\f\+'
    else
        let regex_error_detect = '^Error detected while processing \zs.\+\ze:$'
        let regex_line = '^line\s\+\zs\d\+\e:$'
        let regex_last_set = 'Last set from \zs\f\+'
    endif

    for line in split(a:messages, "\n")
        if line =~# regex_error_detect
            " ... の処理中にエラーが検出されました:'
            let matched = matchstr(line, regex_error_detect)
            if matched =~# '^function'
                " function <SNR>253_fuga の処理中にエラーが検出されました:
                " function <SNR>253_piyo[1]..<SNR>253_fuga の処理中にエラーが検出されました:
                let matched = matchstr(matched, '^function \zs\S*')
                let stacks = reverse(split(matched, '\.\.'))
                for stack in stacks
                    let [func_name, offset] = (stack =~# '\S\+\[\d')
                    \ ? matchlist(stack, '\(\S\+\)\[\(\d\+\)\]')[1:2]
                    \ : [stack, 0]

                    " 辞書関数の数字は{}で囲む
                    let func_name = func_name =~# '^\d\+$' ? '{' . func_name . '}' : func_name

                    redir => verbose_func
                    execute 'silent verbose function ' . func_name
                    redir END

                    let filename = matchstr(verbose_func, regex_last_set)
                    let filename = expand(filename)

                    if !has_key(files, filename)
                        let files[filename] = readfile(filename)
                    endif

                    if func_name =~# '{\d\+}'
                        let func_lines = split(verbose_func, "\n")
                        unlet func_lines[1]
                        let max_line = len(func_lines)
                        let func_lines[0] = '^\s*fu\%[nction]!\=\s\+\zs\S\+\.\S\+'

                        for i in range(1, max_line - 2)
                            let func_lines[i] = '^\s*' . matchstr(func_lines[i], '^\d\+\s*\zs.*')
                        endfor

                        let func_lines[max_line - 1] = '^\s*endf[unction]'

                        let lnum = 0
                        while 1
                            let lnum = match(files[filename], func_lines[0], lnum)

                            if lnum < 0
                                throw 'No dictionary function'
                            endif

                            let find_dic_func = 1
                            for i in range(1, max_line - 1)
                                if files[filename][lnum + i] !~# func_lines[i]
                                    let lnum = lnum + i
                                    let find_dic_func = 0
                                    break
                                endif
                            endfor

                            if find_dic_func
                                break
                            endif
                        endwhile

                        let func_name = matchstr(files[filename][lnum], func_lines[0])
                        let lnum += 1 + offset
                    else
                        let func_name  = substitute(func_name, '<SNR>\d\+_', 's:', '')
                        let lnum = match(files[filename], '^\s*fu\%[nction]!\=\s\+' . func_name) + 1 + offset
                    endif

                    call add(qf_info_list, {
                    \   'filename': filename,
                    \   'lnum': lnum,
                    \   'text': func_name,
                    \})
                endfor
            else
                " <filename> の処理中にエラーが検出されました:
                let filename = expand(matchstr(line, regex_error_detect))
                let qf_info.filename = expand(filename)
            endif
        elseif line =~# regex_line
            " 行    1:
            let lnum = matchstr(line, regex_line)
            if len(qf_info_list) > 0
                let qf_info_list[0]['lnum'] += lnum
            else
                let qf_info.lnum = lnum
            endif
        elseif line =~# '^E'
            " E492: エディタのコマンドではありません: one
            let [nr, text] = matchlist(line, '^E\(\d\+\): \(.\+\)')[1:2]
            if len(qf_info_list) > 0
                if len(qf_info_list) == 1
                    let qf_info_list[0]['nr'] = nr
                    let qf_info_list[0]['text'] = 'in ' . qf_info_list[0]['text'] . ' | ' . text
                else
                    let i = 0
                    for val in qf_info_list
                        let val['nr'] = nr
                        let val['text'] = '#' . i . ' in ' . val['text'] . (i == 0 ? (' | ' . text) : '')
                        let i += 1
                    endfor
                endif
                let qflist += qf_info_list
            else
                let qf_info.nr = nr
                let qf_info.text = text
                call add(qflist, qf_info)
            endif

            let qf_info = {}
            let qf_info_list = []
        endif
    endfor

    return qflist
endfunction

command! -nargs=0 QfMessages call s:qf_messages()
```

このVim scriptをvimrcにかいて、エラーメッセージが出たら、`:QfMessages`と打てば、quickfixにエラーが表示されるようになります。例えば、`error.vim`のエラーに対しては以下のようになります。

```
error.vim|1 error 492| エディタのコマンドではありません: one
error.vim|4 error 492| in Hoge | エディタのコマンドではありません:     two
error.vim|9 error 492| in s:fuga | エディタのコマンドではありません:     three
error.vim|9 error 492| #0 in s:fuga | エディタのコマンドではありません:     three
error.vim|14 error 492| #1 in s:piyo
error.vim|21 error 492| in s:dict.aaa() | エディタのコマンドではありません:     four
```

`:messages`で表示される内容は`:messages clear`で消すことができます。

# 解説

Vim scriptの内容について見ていきます。

## :QfMessages

コマンド`:QfMessages`は`s:qf_messages()`を呼びます。

## s:qf_messages()

`s:qf_messages()`は`messages`の内容を変数に入れて、`s:parse_error_messages()`を呼び出して、戻り値をquickfixにセットして、quickfixのwindowを開きます。

## s:parse_error_messages()

`s:parse_error_messages`はエラーメッセージをパースする関数です。

最初の部分で必要な変数を宣言しています。

日本語と英語のエラーメッセージにマッチする正規表現を用意しています。英語のエラーメッセージは

```
Error detected while processing /Users/tmsanrinsha/.cache/vim/junkfile/2016/10/2016-10-29-200145.vim:
line    1:
E492: Not an editor command: one
Error detected while processing function Hoge:
line    1:
E492: Not an editor command:     two
Error detected while processing function <SNR>253_fuga:
line    1:
E492: Not an editor command:     three
Error detected while processing function <SNR>253_piyo[1]..<SNR>253_fuga:
line    1:
E492: Not an editor command:     three
Error detected while processing function 250:
line    1:
E492: Not an editor command:     four
```

のようにな感じです。

for文でメッセージを行ごとに処理していきます。

エラー文は3種類あるので、それぞれif文の中で処理をしています。

### ○○ の処理中にエラーが検出されました

「○○ の処理中にエラーが検出されました」の行はさらに関数名かファイル名かで分岐します。

#### 関数名の場合

関数のエラーの場合、関数の中で呼んでいる関数が例外を出すと、..でつながっていくので、..で`split()`します。原因の方が最初に来るように`reverse()`しています。

分割した文字列ごとに関数名とエラーが発生した行番号（オフセット）を取り出します。発生源の関数のエラー発生箇所はエラーメッセージの次の行の「行 <行番号>:」に書かれため、ここでは0に仮置きしておきます。

辞書関数の数字は{}で囲みます。次の`verbose function`を実行するときに必要だからです。

`:verbose function Hoge`などとうつと

```
   function Hoge()
        最後にセットしたスクリプト: /path/to/error.vim
1      two
   endfunction
```

のような結果が表示されます。ここから関数が定義されたファイル名を取得することができます。

関数が定義された位置を取得するため、ファイルを読み込みます。

辞書関数に関しては数字しかわからないため、verboseに表示された関数の内容と一致する箇所をファイル内から愚直に探しています。

その他の関数については、関数名が書かれている行をファイルから見つけます。スクリプトローカルな関数については`<SNR>数字_`を`s:`に変更してから探します。
見つけたら`offset`部分を加えて、`qf_info_list`に情報を付け加えます。

これを関数分繰り返します。

#### ファイル名の場合

ファイル名を単に`qf_info.filename`に入れます

### 行   <行番号>:

行番号を取り出します。`qf_info_list`に要素があるときは、関数内の行番号を示しているので、発生源の関数の情報を入れている`qf_info_list[0]['lnum']`に行番号を足します。

### <エラー番号>: <内容>

ここではエラー番号とエラー内容を取り出します。関数の場合は`in <関数名>`をエラー内容を加えます。また、スタックがある場合は`#<数字>`を追加します。

また、この行はエラーの塊の最後の行なので、`aflist`に加え、`qf_info`、`qf_info_list`を初期化しています。

# おわり

ぜひ使ってみて下さい。