Neovim 0.10のwinfixbuf対応: dduで学ぶバッファ管理の実践
Hello, World!
既に一年以上前の話ではありますがneovimのバッファ管理でアップデートがありましたね[1]
(技術ブログを書くのが遅くなってしまって旬を逃してしまいましたが)
この記事では、その解決過程で学んだバッファ管理の概念と解決策を整理します。
TL;DR
- neovim 10で追加された
winfixbuf
とddu
の設定を解決 -
bwipeout!
を使って階層が深くても全てのfloating windowを確実に閉じる - 元のウィンドウでファイルを開くシンプルな実装
ことの発端
一仕事を終え気分転換にsudo pacman -Syu
を実行していたら、neovimが0.10.0にアップデートされていました。[2]
早速起動してみると、普段使っているddu.nvim
の挙動が違っています
-- エラーは見やすいように整形しています
Error 10:57:30 PM msg_show.rpc_error
Error detected while processing function ddu#ui#sync_action[1]
ddu#ui_sync_action[4]..
ddu#denops#_request[15]..
denops#request[1]..
denops#_internal#server#chan#request[6]..
denops#_internal#rpc#nvim#request:line1:
Error invoking 'invoke' on channel 4 (denops):
Error: Failed to call 'uiAction' API in 'ddu':
Error: Failed to call 'ddu#item_action' in.......
といったエラーが出てしまいました。
どうやらddu.nvim
のバッファ管理に関するエラーが出ており
winfixbufが有効な状態でバッファを閉じようとしたことが原因のようです。
背景: winfixbufとは何か
winfixbuf
は、Neovim 0.10.0で追加されたウィンドウローカルオプションで、ウィンドウを特定のバッファに「固定」する機能です。
このオプションが有効になってると、そのウィンドウでは別のバッファを開こうとしても拒否されます。
これにより、helpやquickfixウィンドウで誤って別のファイルを開くことを防げます
主な特徴
-
:edit
、:buffer
、:bnext
などのコマンドがエラーになる - プラグインがバッファを切り替えようとしても防がれる
- ファイラーやターミナルなど、専用ウィンドウの実装に便利
利用用途
- ファイラー (例: ddu, NERDTree, nvim-tree.lua)
- ターミナル (例: toggleterm.nvim)
- その他、特定のバッファを常に表示したい場合
ここで重要なのは、winfixbuf
が有効なウィンドウでは、バッファを切り替える操作が制限されるため、プラグインが意図した動作をしないことがある、という点です。
試した解決策とその問題
問題:dduでE1513エラーが発生
Error: Failed to call 'itemAction' API in 'ddu':
Vim(buffer):E1513: Cannot switch buffer. 'winfixbuf' is enabled
はい、そうです。
上記のエラーもその一例です。
私の場合はdduをflaoting windowのファイラーとして使っているので、dduのバッファを閉じようとした際にエラーが発生しました。
解決策1 単純にwinfixbufを無効化
setlocal nowinfixbuf
❌問題点:
いつもであればdduでファイルを選択してEnterを押すとdduのfloating windowのバッファが閉じて元のウィンドウに戻るのですが
今回は、そのbufferが閉じずそのbuffer内でファイルが開いてしまいました。
解決策2 dduのactionOptionsでquitアクションを使用
'actionOptions': {
'open': {
'quit': v:true,
},
}
問題点:
最後の1つのwindowしか閉じない
これはどういうことかというと、dduのfloating windowで階層が深くなっている場合に、1つのwindowしか閉じないので、結局元のウィンドウに戻れないことがありました。
:quit
を実行する
解決策3. 複数回let i = 0
while i < 10
call ddu#ui#do_action('quit')
let i += 1
endwhile
問題点:
の、脳筋!!!!余りにも脳筋すぎる!!!!!
エルデンリングの脳筋ビルドは好きですが、コードでやるのは違うと思います。[3]
階層によって必要な回数が変わるので、無駄にループ回数を増やすとパフォーマンスが悪くなります。
Vimのバッファ管理における階層性と選択基準
ここからが本題です。
quitとか使っていたのですが、Vimにはバッファを閉じるためのコマンドがいくつかあったので
整理してみました。
概念
バッファ操作コマンドには「強度」の違いがあって、winfixbuf
が設定されてる時でも動作するコマンドと、エラーになるコマンドがあるので、これを理解しておくと、プラグインのエラーにも対処しやすくなります。
階層構造(弱い → 強い)
-
:close
- ウィンドウのみ閉じる -
:quit
- 現在のウィンドウ/バッファを終了 -
:bdelete
- バッファを削除(ウィンドウは残る場合がある) -
:bwipeout
- バッファを完全に削除 -
:bwipeout!
- 強制的にバッファを完全削除(変更があっても)
実践的な選択基準
floating window を確実に閉じたい場合
→ bwipeout!
を選択
理由:
- バッファが完全に削除されることで、関連するウィンドウも連鎖的に閉じる
- 階層が深くても一回の実行で効果的
通常のファイル編集での終了
→ :quit
または :close
を選択
最終的な解決策
コンセプト
- 階層が深くても一回の実行で全てのfloating windowを閉じる
- 元のウィンドウでファイルを開く
実装のポイント
- ddu開始前に現在のウィンドウIDを保存
- ファイル選択時に元のウィンドウに移動してからファイルを開く
- bwipeout!で全てのddu windowを確実に閉じる
上記の整理を踏まえて、以下のように実装しました。
" DDU開始前のウィンドウIDを保存する変数
let g:ddu_prev_winid = 0
" DDU開始前に現在のウィンドウIDを保存
function! s:save_prev_winid() abort
let g:ddu_prev_winid = win_getid()
endfunction
" 前のウィンドウでファイルを開く関数
function! s:open_in_prev_window() abort
let item = ddu#ui#get_item()
if !empty(item) && has_key(item, 'action') && has_key(item.action, 'path')
if !item->get('isTree', v:false)
" ファイルの場合
let filepath = item.action.path
let ddu_bufnr = bufnr('%')
" 元のウィンドウに移動
if g:ddu_prev_winid != 0 && win_gotoid(g:ddu_prev_winid)
execute 'edit ' . fnameescape(filepath)
" bwipeout! で階層が深くても全てのddu windowを確実に閉じる
execute 'bwipeout! ' . ddu_bufnr
endif
else
" ディレクトリの場合は通常のnarrowアクション
call ddu#ui#do_action('itemAction', #{name: 'narrow'})
endif
endif
endfunction
" キーマッピング
nmap <silent> ,m <Cmd>call <SID>save_prev_winid()<CR><Cmd>call ddu#start({ 'name' : 'file' })<CR>
" ddu-filerの設定
autocmd FileType ddu-filer call s:ddu_filer_my_settings()
function! s:ddu_filer_my_settings() abort
" winfixbufを無効化
setlocal nowinfixbuf
" ファイル選択時に元のウィンドウで開く
nnoremap <buffer><silent> <CR>
\ <Cmd>call <SID>open_in_prev_window()<CR>
endfunction
まとめ
この実装により:
- ✅ winfixbufが有効でもdduが正常に動作
- ✅ ファイルは常に元のウィンドウで開かれる
- ✅ 階層が深くても全てのfloating windowが確実に閉じる
- ✅ シンプルで理解しやすいコード
上記コードの完全版はdotfilesで公開しています。
Discussion