これまでの お話し
Part 2 で execute に default_implementation() を足したことで、skkeleton の候補は正しく確定できるようになりました。ところが、それでもまだ <CR> で確定できないケースが残っていました。しかも今度は skkeleton の候補に限らず、buffer や rg などskkeleton と無関係なソースの候補でも同じ症状が出ます。
このシリーズで一番厄介だった、そして一番の見せ場のバグです。
症状
skkeleton が有効な状態(日本語入力モード)で補完メニューを開き、どのソースの候補を選んで <CR> を押しても確定できない。skkeleton を無効にすると問題なく確定できる。つまり原因は skkeleton 側にありそうだ、というところまでは切り分けられました。
真因: skkeleton は3種類の補完エンジンしか知らない
vim-skk/skkeleton 本体の autoload/skkeleton.vim に s:complete_info() という関数があります。これは「今どの補完エンジンのポップアップメニューが表示されているか」を判定するための関数で、次の順で調べています。
function! s:complete_info() abort
if exists('*pum#visible') && pum#visible()
return ['pum.vim', ...]
elseif has('nvim') && luaeval('... package.loaded["cmp"] ...')
return ['cmp', ...]
else
return ['native', complete_info(['pum_visible', 'selected'])]
endif
endfunction
見ての通り、skkeleton が認識できる補完エンジンは pum.vim / nvim-cmp(package.loaded["cmp"]の有無で判定) / Vimネイティブ補完 の3種類だけです。blink.cmp は影も形もありません。
blink.cmp は独自のフローティングウィンドウで補完メニューを描画するため、Vim組み込みの pumvisible() には反応しません。かつ、この設定では nvim-cmp 本体をアンインストール済みのため package.loaded["cmp"] も存在しません。結果として、skkeleton は必ず3番目の "native" 判定に落ちます。
"native" 判定のときに見ている complete_info(['pum_visible']) は Vim組み込みの補完ポップアップの状態を返すものなので、blink.cmp のメニューがどれだけ表示されていても常に false です。
なぜ「どのソースでも」確定できなくなるのか
denops 側の実装(denops/skkeleton/main.ts の handle())は、completeInfo.pum_visible が true のときだけ eggLikeNewline による <CR> 確定処理(handleCompleteKey)を呼ぶようになっています。この判定が常に false になる blink.cmp 環境では、その分岐が一度も実行されません。
すると <CR> は skkeleton 自身の通常のキー処理にそのまま飲み込まれてしまい、blink.cmp 側の <CR> キーマップまで届きません。この判定は候補がどのソース由来かとは無関係に行われるため、skkeleton 由来でも buffer 由来でも rg 由来でも、skkeleton が有効になっている間は等しく巻き込まれて確定できなくなっていた、というわけです。
対処: blink.cmp を「nvim-cmp のふり」をさせる
一番シンプルな対処は、skkeleton に package.loaded["cmp"] が存在すると誤認させることです。ちょうど blink.compat の impersonate_nvim_cmp = true オプションが、calc/emoji/spell/latex_symbols/rg のソース内部にある require("cmp") を解決するために、偽の cmp モジュールを package.loaded["cmp"] に用意してくれています。
この偽モジュールに、skkeleton が呼び出す3メソッド(visible() / get_active_entry() / confirm())だけを追加で生やしたのが lua/my/cmp/skkeleton_cmp_shim.lua です。
local M = {}
function M.setup()
local ok, cmp_shim = pcall(require, "cmp")
if not ok then
vim.notify_once(
"skkeleton_cmp_shim: require('cmp') に失敗しました。blink.compat が読み込まれているか確認してください。",
vim.log.levels.WARN
)
return
end
cmp_shim.visible = function()
local blink_ok, blink = pcall(require, "blink.cmp")
return blink_ok and blink.is_menu_visible() == true
end
cmp_shim.get_active_entry = function()
local list_ok, list = pcall(require, "blink.cmp.completion.list")
if not list_ok then
return nil
end
return list.get_selected_item()
end
-- skkeleton は <Cmd>lua require('cmp').confirm({select = true})<CR> を
-- 直接実行するので、引数の中身に関わらず blink.cmp 側の accept を呼べばよい
cmp_shim.confirm = function(_)
local blink_ok, blink = pcall(require, "blink.cmp")
if not blink_ok then
return
end
if not blink.accept() then
blink.accept({ force = true })
end
end
end
return M
ポイントは、package.loaded["cmp"] をまるごと自作のテーブルで置き換えるのではなく、blink.compat がすでに用意している既存のテーブルに3つのフィールドを追加するだけにしていることです。こうすることで、calc/emoji/spell/latex_symbols/rg のために blink.compat が用意している本来の cmp シムの機能(register_source など)を壊さずに済みます。
-
visible()—blink.cmpのメニューが表示中かどうかを、blink.is_menu_visible()で判定して返す -
get_active_entry()— 現在選択中の候補をblink.cmp.completion.listから取得する -
confirm()— skkeleton はrequire('cmp').confirm({select = true})を直接実行してくるので、中身の引数は見ずにblink.accept()を呼ぶだけでよい。選択されていない場合に備えてforce = trueでのリトライも入れている
前提条件: blink.compat を即時ロードにする
このシムが機能するには、require("cmp") が起動直後の時点ですでに解決できる必要があります。そのため、この設定では saghen/blink.compat から lazy = true を外し、即時ロードにしています。blink.compat を遅延ロードのままにしていると、skkeleton が先に require("cmp") を呼んだ時点でまだ package.loaded["cmp"] が存在せず、シムを仕込む前に失敗してしまいます。
まとめ
このシリーズで一番時間がかかったのは、実はコードを書く部分より「なぜ直らないのか」の切り分けでした。
-
executeでdefault_implementation()を呼んでいなかった (Part 2) → 修正してもまだ直らない - skkeleton 本体が「今どの補完エンジンが表示されているか」を pum.vim / nvim-cmp / native の3種類でしか判定していない、という skkeleton 側の仕様が本当の原因だった
-
package.loaded["cmp"]に最低限のメソッドを生やして nvim-cmp のふりをさせることで解決した
blink.compat のドキュメントや skkeleton 側のエラーメッセージだけを見ていても辿り着けず、両方のプラグインのソースコードを実際に読みに行って初めて分かった原因でした。
次回予告
最終回の Part 4 では、スニペット統合(LuaSnip を組み込み snippets プリセットではなくネイティブソース化した理由)と、コマンドライン補完まわりの細かい罠を紹介します。