この記事は Vim Advent Calendar 2016の6日目の記事です。
Vim scriptのエラーが出たときにエラーをquickfixに表示して、エラー箇所に飛べるといいなと思いますが、標準の機能ではないようなので、コマンドを作りました。
先行事例
調べてみると、kanno_kannoさんが書いた記事
- Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会
- 関数対応版:Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会
- 辞書関数対応版:Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会
や、Stack Exchange
に参考となる方法が書いてありました。
kanno_kannoさんの記事では自らsourceしたときにでるエラーメッセージをquickfixに表示する方法が、Stack Exchangeにはtry catchで例外をキャッチしてquickfixに表示する方法が書いてありました。
それらを参考にして、すでにエラーメッセージとして出たエラーを:messages
で取得し、quickfixに表示するコマンドを作ります。
エラーメッセージの構造
まず、エラーを出すために、以下のようなファイルを用意します。
one
function! Hoge()
two
endfunctioncall Hoge()function!s:fuga()
three
endfunctioncalls:fuga()function!s:piyo()calls:fuga()endfunctioncalls:piyo()lets:dict= {}
function!s:dict.aaa()
four
endfunctioncalls: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のエラーは
- ○○ の処理中にエラーが検出されました:
- 行 <行番号>:
- <エラー番号>: <内容>
一つの単位であることがわかります。
それぞれのエラーを詳しく見ていきます。
/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が以下になります。
function!s:qf_messages()let str_messages =''redir=> str_messages
silent!messagesredir END
let qflist =s:parse_error_messages(str_messages)call setqflist(qflist,'r')cwindowendfunctionfunction!s:parse_error_messages(messages) abort
" 戻り値。setqflistの引数に使う配列let qflist = []
" qflistの要素になる辞書let qf_info = {}
" qflistの要素となる辞書の配列。エラー内容がスタックトレースのときに使用let qf_info_list = []
" 読み込んだファイルの内容をキャッシュしておくための辞書letfiles= {}
ifv:lang=~# 'ja_JP'let regex_error_detect ='^.\+\ze の処理中にエラーが検出されました:$'let regex_line ='^行\s\+\zs\d\+\ze:$'let regex_last_set ='最後にセットしたスクリプト: \zs\f\+'elselet 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\+'endiffor 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
letfilename= matchstr(verbose_func, regex_last_set)letfilename= expand(filename)if!has_key(files,filename)letfiles[filename] = readfile(filename)endifif 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\+'foriin range(1, max_line -2)let func_lines[i] ='^\s*' . matchstr(func_lines[i],'^\d\+\s*\zs.*')endforlet func_lines[max_line -1] ='^\s*endf[unction]'let lnum =0while1let lnum =match(files[filename], func_lines[0], lnum)if lnum <0throw'No dictionary function'endiflet find_dic_func =1foriin range(1, max_line -1)iffiles[filename][lnum +i] !~# func_lines[i]
let lnum = lnum +ilet find_dic_func =0breakendifendforif find_dic_func
breakendifendwhilelet func_name = matchstr(files[filename][lnum], func_lines[0])let lnum +=1+ offset
elselet func_name = substitute(func_name,'<SNR>\d\+_','s:','')let lnum =match(files[filename],'^\s*fu\%[nction]!\=\s\+' . func_name)+1+ offset
endifcall add(qf_info_list, {
\ 'filename': filename,
\ 'lnum': lnum,
\ 'text': func_name,
\})endforelse " <filename> の処理中にエラーが検出されました:letfilename= expand(matchstr(line, regex_error_detect))let qf_info.filename= expand(filename)endifelseif line =~# regex_line
" 行 1:let lnum = matchstr(line, regex_line)if len(qf_info_list)>0let qf_info_list[0]['lnum'] += lnum
elselet qf_info.lnum = lnum
endifelseif line =~# '^E' " E492: エディタのコマンドではありません: onelet [nr, text] = matchlist(line,'^E\(\d\+\): \(.\+\)')[1:2]
if len(qf_info_list)>0if len(qf_info_list)==1let qf_info_list[0]['nr'] = nr
let qf_info_list[0]['text'] ='in ' . qf_info_list[0]['text'] . ' | ' . text
elseleti=0for val in qf_info_list
let val['nr'] = nr
let val['text'] ='#' . i . ' in ' . val['text'] . (i==0 ? (' | ' . text) : '')leti+=1endforendiflet qflist += qf_info_list
elselet qf_info.nr = nr
let qf_info.text = text
call add(qflist, qf_info)endiflet qf_info = {}
let qf_info_list = []
endifendforreturn qflist
endfunction
command!-nargs=0 QfMessages calls: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
を初期化しています。
おわり
ぜひ使ってみて下さい。