50行くらいでvim-qfedit的なquickfix編集機能を作る
この記事はVim駅伝の2024-05-24の記事です。
前回の記事はkuuoteさんのHとLをいい感じにするマッピングをvimrc読書会で見つけたので魔改造してみたです。
次回の記事はuga-rosaさんのnvim-treesitter は Python のインデントを壊すです。
vim-qfeditというプラグインがあります。
Vimのquickfixリストを編集できるようにするプラグインです。
quickfixの勉強を兼ねて、自分なりに再実装してみました。
途中からコード短縮も楽しくなってきたのでなるべく短くしました。
let s:qed_setlocal = {-> execute('setlocal modifiable nomodified noswapfile')}
let s:is_loc = {-> get(get(getwininfo(win_getid()), 0, {}), 'loclist', 0)}
let s:setlist = {items -> s:is_loc() ? setloclist(0, items) : setqflist(items)}
let s:delpat = {str, pattern -> substitute(str, pattern, '', '')}
let s:split_two = {str, pattern -> (split(str, pattern) + ['', ''])[:1]}
let s:extract = {str, pattern -> matchlist(str, $'^\({pattern}\v)\s*(.*)')[1:2] ?? ['', str]}
function s:parseline(line) abort
let [mid, text] = s:extract(a:line->trim('', 1), '^[^|]*|[^|]*|')
let [filename, mid] = split(mid, '\s*|\s*', v:true)[:1]
let bufnr = empty(filename) ? 0 : bufnr(filename)
if bufnr < 1
unlet bufnr
endif
let [lnum_part, mid] = s:extract(mid, '\v\d+%(-\d+)?')
let [lnum, end_lnum] = s:split_two(lnum_part, '-')
let [col_part, mid] = s:extract(mid, 'col\s*\v\d+%(-\d+)?')
let [col, end_col] = s:split_two(s:delpat(col_part, '^col\s*'), '-')
let [type, nr] = s:split_two(mid, '\s\+')
unlet mid lnum_part col_part
return l:
endfunction
function s:on_change() abort
defer setpos('.', getcurpos())
let items = []
for item in getline(1, '$')
try
call add(items, s:parseline(item))
catch
" skip add() if s:parseline() throws error
endtry
endfor
call s:setlist(items)
call s:qed_setlocal()
endfunction
function qed#start() abort
call s:qed_setlocal()
augroup qed_change
autocmd! TextChanged <buffer> keepjump call s:on_change()
augroup END
endfunction
上記のコードをqed.vim
として保存し、runtimepath
に登録すると使えるようになります。
こんな感じでautocmdを使ってquickfixウィンドウに入ったときにqed#start()
を呼んで実行します。
autocmd BufReadPost quickfix call qed#start()
これで、quickfixリストの項目を削除したり順序を入れ替えたりできるようになります。
コード解説
50行くらいしかないので全部解説します。
構造
このプラグインは以下のように動作します。
- quickfixウィンドウに入ると(前述のautocmdで)
qed#start()
が呼ばれる - 変更に反応して
s:on_change()
が呼ばれる - 各行が
s:parseline()
でパースされる - パースされたリストが反映される
ヘルパー関数群
冒頭に定義しているヘルパー関数を紹介します。
quickfixを編集できるよう、オプションを設定する関数↓
let s:qed_setlocal = {-> execute('setlocal modifiable nomodified noswapfile')}
現在開いているのがquickfixリストかlocationリストかを返す関数↓
let s:is_loc = {-> get(get(getwininfo(win_getid()), 0, {}), 'loclist', 0)}
itemをquickfixリスト(またはlocationリスト)へ反映する関数↓
let s:setlist = {items -> s:is_loc() ? setloclist(0, items) : setqflist(items)}
文字列から指定されたパターンを削除する関数(substituteのラッパー)↓
let s:delpat = {str, pattern -> substitute(str, pattern, '', '')}
" example:
echo s:extract('12men', '\d\+')
" 'men'
文字列を指定されたパターンで分割する関数↓
let s:split_two = {str, pattern -> (split(str, pattern) + ['', ''])[:1]}
" example:
echo s:split_two('1-1', '-')
" ['1', '1']
echo s:split_two('1', '-')
" ['1', '']
echo s:split_two('', '-')
" ['', '']
文字列から指定されたパターンにマッチする部分としない部分に分割する関数↓
let s:extract = {str, pattern -> matchlist(str, $'^\({pattern}\v)\s*(.*)')[1:2] ?? ['', str]}
" example:
echo s:extract('12men', '\d\+')
" ['12', 'men']
エントリーポイント
quickfixに入ったときにautocmd経由で実行されるのが以下のqed#start()
です。
この関数の内部でローカルオプションを設定し、編集を可能にします。
また、バッファローカルなautocmdで、変更時にs:on_change()
を呼び出すよう設定します。
function qed#start() abort
call s:qed_setlocal()
augroup qed_change
autocmd! TextChanged <buffer> keepjump call s:on_change()
augroup END
endfunction
on_change関数
quickfixのバッファを編集するとこの関数が呼ばれ、各行についてs:parseline()
を実行します。
その後、そのリストをquickfixに反映し、再度ローカルオプションを更新します。
function s:on_change() abort
defer setpos('.', getcurpos())
let items = []
for item in getline(1, '$')
try
call add(items, s:parseline(item))
catch
" skip add() if s:parseline() throws error
endtry
endfor
call s:setlist(items)
call s:qed_setlocal()
endfunction
なお、パースがエラーになった(編集したことで書式が崩れた)場合、その行はスキップするようにしています。
パース関数
quickfixの行は以下のような形式になっています。
filename |line-endline col-endcol error num| contents
これを直接efmに設定できれば楽だったのですが、どの項目もオプショナルだったので、簡単に設定することはできなさそうでした。
仕方なくひとつひとつパースすることにしたのですが、前述のヘルパーのおかげで少しは短縮できたかなと思います。
function s:parseline(line) abort
let [mid, text] = s:extract(a:line->trim('', 1), '^[^|]*|[^|]*|')
let [filename, mid] = split(mid, '\s*|\s*', v:true)[:1]
let bufnr = empty(filename) ? 0 : bufnr(filename)
if bufnr < 1
unlet bufnr
endif
let [lnum_part, mid] = s:extract(mid, '\v\d+%(-\d+)?')
let [lnum, end_lnum] = s:split_two(lnum_part, '-')
let [col_part, mid] = s:extract(mid, 'col\s*\v\d+%(-\d+)?')
let [col, end_col] = s:split_two(s:delpat(col_part, '^col\s*'), '-')
let [type, nr] = s:split_two(mid, '\s\+')
unlet mid lnum_part col_part
return l:
endfunction
limitation
s:parseline()
はデフォルトのquickfixのフォーマットにあわせています。
通常はこれで問題ないと思われますが、'quickfixtextfunc'
によって独自フォーマットが適用されている場合、パースが失敗します。
この値はオプション以外にもsetqflist()
で設定可能であり、さらに関数なのでどのようなフォーマットが適用されているかを判断するのは無理そうでした。このため、デフォルト以外のフォーマットに対応するのは諦めています。
ちなみにこのパース失敗は内部エラーを引き起こし、Vimが操作不能になったのでissueを作りました。
Discussion