0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Neovim で skkeleton × blink.cmp を協調動作させる (3) skkeleton 本体が blink.cmp を認識しない問題との格闘

0
Last updated at Posted at 2026-07-20

これまでの お話し

Part 2executedefault_implementation() を足したことで、skkeleton の候補は正しく確定できるようになりました。ところが、それでもまだ <CR> で確定できないケースが残っていました。しかも今度は skkeleton の候補に限らず、bufferrg などskkeleton と無関係なソースの候補でも同じ症状が出ます。

このシリーズで一番厄介だった、そして一番の見せ場のバグです。

症状

skkeleton が有効な状態(日本語入力モード)で補完メニューを開き、どのソースの候補を選んで <CR> を押しても確定できない。skkeleton を無効にすると問題なく確定できる。つまり原因は skkeleton 側にありそうだ、というところまでは切り分けられました。

真因: skkeleton は3種類の補完エンジンしか知らない

vim-skk/skkeleton 本体の autoload/skkeleton.vims: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.tshandle())は、completeInfo.pum_visibletrue のときだけ eggLikeNewline による <CR> 確定処理(handleCompleteKey)を呼ぶようになっています。この判定が常に false になる blink.cmp 環境では、その分岐が一度も実行されません。

すると <CR> は skkeleton 自身の通常のキー処理にそのまま飲み込まれてしまい、blink.cmp 側の <CR> キーマップまで届きません。この判定は候補がどのソース由来かとは無関係に行われるため、skkeleton 由来でも buffer 由来でも rg 由来でも、skkeleton が有効になっている間は等しく巻き込まれて確定できなくなっていた、というわけです。

対処: blink.cmp を「nvim-cmp のふり」をさせる

一番シンプルな対処は、skkeleton に package.loaded["cmp"] が存在すると誤認させることです。ちょうど blink.compatimpersonate_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"] が存在せず、シムを仕込む前に失敗してしまいます。

まとめ

このシリーズで一番時間がかかったのは、実はコードを書く部分より「なぜ直らないのか」の切り分けでした。

  1. executedefault_implementation() を呼んでいなかった (Part 2) → 修正してもまだ直らない
  2. skkeleton 本体が「今どの補完エンジンが表示されているか」を pum.vim / nvim-cmp / native の3種類でしか判定していない、という skkeleton 側の仕様が本当の原因だった
  3. package.loaded["cmp"] に最低限のメソッドを生やして nvim-cmp のふりをさせることで解決した

blink.compat のドキュメントや skkeleton 側のエラーメッセージだけを見ていても辿り着けず、両方のプラグインのソースコードを実際に読みに行って初めて分かった原因でした。

次回予告

最終回の Part 4 では、スニペット統合(LuaSnip を組み込み snippets プリセットではなくネイティブソース化した理由)と、コマンドライン補完まわりの細かい罠を紹介します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?