Neovim + Skkeleton で快適に入力できるようになるまで

## 最初に謝罪

この記事は[闇の魔術に対する防衛術のカレンダー | Advent Calendar 2021 - Qiita][闇の魔術] 25日目の記事です。

[闇の魔術]: https://qiita.com/advent-calendar/2021/yaminomajutu

ふと気付いたらこの一年、全く記事を書いていなかったのでこの記事を書いています。12月24日の真夜中になってアドベントカレンダーの空きを探すという無謀なことに挑戦したのですが、案の定、関連がありそうなところは全部埋まってまして、なんとかこじつければ合っていそうなこちらにエントリーした次第。

ガチな Vimmer なら「闇の魔術」って何か分かりますよね？ そう。↓これです。

> [Hello vim people, what do you mean by "dark powered" ? : vim](https://www.reddit.com/r/vim/comments/d9c4n1/hello_vim_people_what_do_you_mean_by_dark_powered/)

## では改めて……

2021 年を振り返りますと、僕の脳内 Vim 界においては ***Lua! Lua! Neovim!*** の一年でしたのでそっち方面のこと色々書こうかと思ったのですが、それだと適当な分量に纏められる気がしません（さらに、闇の魔術とも関係ありません）。ここは手頃なネタとして、ここ数ヶ月ハマっている ddc.vim + Skkeleton 周りの小ネタについて書きます。

*Skkeleton とは何か？* については先日作者様から公開された[こちら][]を読んでいただくのが適当なので置いといて、今日のお題は以下のようなものです。

[こちら]: https://zenn.dev/kuu/articles/vac2021-skkeleton

* 拙作プラグイン [skkeleton\_indicaator][delphinus/skkeleton_indicator.nvim] について。
* [ddc.vim][]（これが闇の魔術です）で [pum.vim][] を使っているときのワークアラウンド。
* 左右のコマンド（⌘）キーで Skkeleton を切り換える方法。

[ddc.vim]: https://github.com/Shougo/ddc.vim
[pum.vim]: https://github.com/Shougo/pum.vim

なお、Skkeleton 自体は Neovim / Vim 両対応のプラグインなのですが、僕自身は Neovim しか使っていません。この記事では Vim のことを考慮していませんので悪しからず。

## skkeleton\_indicator

[delphinus/skkeleton_indicator.nvim][] のトップ画像がもう全てを表しています。Skkeleton は非常に大人しい IM（Input Method）でして、入力モードが切り替わっても特に表示は変わりません。Skkeleton には `skkeleton-mode-changed` という自動コマンドイベントが提供されていますので、これに応じてモードを表示するのがこのプラグインの役目です。

[delphinus/skkeleton_indicator.nvim]: https://github.com/delphinus/skkeleton_indicator.nvim

![Kapture 2021-10-24 at 21 07 04](https://user-images.githubusercontent.com/1239245/138593442-0ff34ccc-72a9-4e07-8b84-92e26b79f288.gif)

なんか普通の IM と遜色ないリッチな UI でテンション上がりますよね？ そうでもない？

## ddc.vim + pum.vim 使ってるあなたに

ddc.vim には pum.vim という相棒がいまして、こいつを使うと補完ウィンドウを細かくカスタマイズしてリッチな見た目に出来るんです。これに関してもやはり[作者様の記事][]がありますのでそちらを読んでいただくとして、こいつと Skkeleton を組み合わせる際には大きな問題があります。

[作者様の記事]: https://zenn.dev/shougo/articles/ddc-vim-pum-vim

> (現状私の怠慢のせいで pum.vim を使用している場合 <Tab> での補完ハンドリングを除く skkeleton が上書きするマッピングと被っている場合機能しなくなります、すみません)

[上掲の記事][こちら]で書かれてるこれですね。これは Skkeleton の（現在の）仕組みを考えると容易に解決できる問題ではないようで、pum.vim ユーザーの僕としては**「普段は pum.vim を有効にしつつ、Skkeleton のときだけネイティブな補完ウィンドウに戻す」**という対策を採ることにしました。

僕の設定内容は[この辺][]なのですが、俺々ライブラリとか使ってるので読みにくいと思います。以下では同様のコードを書いて説明します。

[この辺]: https://github.com/delphinus/dotfiles/blob/1935343b014f3ef15ec2aac8c4bd8f027bc9f87a/.config/nvim/lua/packers/ddc.lua#L152-L170

```lua
local prev_buffer_config
--local pum_close_timer

function _G.skkeleton_enable_pre()
  prev_buffer_config = vim.fn['ddc#custom#get_buffer']()
  vim.fn['ddc#custom#patch_buffer']{
    completionMenu = 'native',
    sources = {'skkeleton'},
  }
  -- この部分は要らなくなりました（後述）
  --[[
  pum_close_timer = vim.fn.timer_start(
    100,
    function()
      if fn['pum#visible']() == 1 then fn['pum#close']() end
    end,
    {['repeat'] = -1}
  )
  ]]
end

function _G.skkeleton_disable_pre()
  vim.fn.timer_stop(pum_close_timer)
  vim.fn['ddc#custom#set_buffer'](prev_buffer_config)
end

vim.cmd[[
  augroup skkeleton_callbacks
    autocmd!
    autocmd User skkeleton-enable-pre call v:lua.skkeleton_enable_pre()
    autocmd User skkeleton-disable-pre call v:lua.skkeleton_disable_pre()
  augroup END
]]
```

ddc.vim には `patch_buffer()`, `set_buffer()` という、一時的に設定を変えることのできる関数が用意されています。これを使うわけですね。

（2021-12-27）上記のコード例でコメントアウトした部分ですが、どうやら ddc.vim の[このコミット][]で解消したようです。ここには説明文を載せていましたが、省略しておきます。

[このコミット]: https://github.com/Shougo/ddc.vim/commit/2040883914050f33e35008b49e702cf2665b254a

<details><summary>要らなくなったワークアラウンド</summary>
ここまでは納得の解決法なのですが、イケてないのはこの部分。
<pre>
  pum_close_timer = vim.fn.timer_start(
    100,
    function()
      if fn['pum#visible']() == 1 then fn['pum#close']() end
    end,
    {['repeat'] = -1}
    -- ↑「repeat」は Lua の予約語なのでこう書かないとダメなのだ！
  )
</pre>
これ何してるかというと、pum.vim の補完ウィンドウが開いたままの状態で Skkeleton をオンにしてしまうと**そのまま画面上にそれが残り続けてしまう**という悲しい現実がありましてですね。なんか上手い解決法が見付からなかったのでこうしたんです。100ms 毎にチェックして開いていたら閉じる、というかっこ悪い仕組みになっています。なんとかなんないかな～。
</details>

## 左右の⌘キーで Skkeleton を ON/OFF する方法

### Mac ユーザーにはおなじみ

「⌘」って言ってる時点で macOS 限定ではあるんですけどね。特に日本語キーボードユーザーにはおなじみと思うんですが、**IM を右⌘キーで ON、左⌘キーで OFF にする**ってのが大変快適なんです。これを何とか Skkeleton で実現したいんですが、⌘キーだけでなくシフト（⇧）、コントロール（⌃）、オプション（⌥）といった **Modifier キー**は、押しただけではターミナルでキーコードが読み取れず、その中の (Neo)Vim では反応できません。

macOS にはこういう時の心強い味方として [Karabiner-Elements][] があります。これを使って以下のような方法で解決できるかと思ったんです。

[Karabiner-Elements]: https://karabiner-elements.pqrs.org

1. Karabiner-Elements で左右の⌘キーを押した時、それぞれ <kbd>F10</kbd> / <kbd>F13</kbd> を送信する。
2. Neovim 側では <kbd>F10</kbd> / <kbd>F13</kbd> を Skkeleton の無効 / 有効にマッピングする。

<kbd>F10</kbd> / <kbd>F13</kbd> にしてるのは大した意味はなくてですね。Neovim 上で特に副作用なく使えるキーなら何でもいいんです（以外とこれを探すのが難しい）。ただこれだけだと問題ががありまして……当然ですが、Neovim が起動してなくても、⌘キーを押したときに <kbd>F10</kbd> / <kbd>F13</kbd> が入力されてしまうんです。

### 使いたい時だけ、マッピングを有効にする

これ「左右の⌘キーで IM 切り換える民」なら解ってもらえると思うんですが、この入力法を使うようになると、**文字を入力する前、無意識に左⌘キーを連打する**ようになっちゃうんです。取りあえず押しとけ、みたいな。設定にも寄るんですが、ターミナルで <kbd>F10</kbd> のようなファンクションキーを押すと、なんか変な文字列が入力されちゃうんですよね。大変ストレスです。

これを解決するには Neovim 側で Karabiner-Elements に対し、Neovim 自身が起動中であることを通知してあげる必要があります。こういうとき便利なのが Karabiner-Elements の [Command line interface][] です。Karabiner-Elements には `karabiner-cli` という CLI コマンドが付属していまして、こいつを使って任意のアプリから Karabiner-Elements を制御できるんです。

[Command line interface]: https://karabiner-elements.pqrs.org/docs/manual/misc/command-line-interface/

この為にはまず、Karabiner-Elements 側で以下のようなマッピングを指定します（GitHub の[ここ][]に完全な設定ファイルを置いています）。

[ここ]: https://github.com/delphinus/dotfiles/blob/1935343b014f3ef15ec2aac8c4bd8f027bc9f87a/.config/karabiner/assets/complex_modifications/japanese.json#L104-L145

```json
{
  "type": "basic",
  "from": { "key_code": "left_command", "modifiers": { "optional": [ "any" ] } },
  "to": [ { "key_code": "left_command" } ],
  "to_if_alone": [ { "key_code": "f10", "modifiers": [ "fn" ] } ],
  "conditions": [
    {
      "type": "variable_if",
      "name": "neovim_in_insert_mode",
      "value": 1
    }
  ]
}
```

キモはこれ、`variable_if`（[公式ドキュメント][]）です。Karabiner-Elements では「変数」という概念（上記ソースでいう `neovim_in_insert_mode`）がありまして、こいつが特定の値の時のみ有効なマッピングが定義できるんです。

[公式ドキュメント]: https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/conditions/variable/

後は想像付くと思いますが、Neovim 側では `karabiner-cli` を使ってこの値を制御します（完全なソースは[こちら][こちら2]）。

[こちら2]: https://github.com/delphinus/dotfiles/blob/1935343b014f3ef15ec2aac8c4bd8f027bc9f87a/.config/nvim/lua/packers/ddc.lua#L135-L180

```lua
local Job = require'plenary.job'
local karabiner_cli = '/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_cli'
function _G.set_karabiner(val)
  Job:new{
    command = karabiner_cli,
    args = {
      '--set-variables',
      ('{"neovim_in_insert_mode":%d}'):format(val),
    },
  }:start()
end

function _G.set_karabiner_if_in_insert_mode()
  local val = vim.fn.mode():match'[icrR]' and 1 or 0
  _G.set_karabiner(val)
end

vim.cmd[[
  inoremap <F10> <Plug>(skkeleton-disable)
  inoremap <F13> <Plug>(skkeleton-enable)

  augroup skkeleton_karabiner_elements
    autocmd!
    autocmd InsertEnter,CmdlineEnter * call v:lua.set_karabiner(1)
    autocmd InsertLeave,CmdlineLeave,FocusLost * call v:lua.set_karabiner(0)
    autocmd FocusGained * call v:lua.set_karabiner_if_in_insert_mode()
  augroup END
]]
```

Neovim の生の Job 機能はちょっと使いにくいので、ここでは [nvim-lua/plenary.nvim][] を使って楽しています。`FocusGained` の時だけ複雑なことしてますが、要するに、Skkeleton が利用可能な時のみ `1` を送信してるんですね。

[nvim-lua/plenary.nvim]: https://github.com/nvim-lua/plenary.nvim

![Kapture 2021-12-25 at 01.14.38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8880/592086a2-caf7-35cd-4a6c-3973c6d695f8.gif)

いい感じですね！

## 終わりに

なんというか、非常にニッチで誰得な情報ばかりだと思うんですが、まあ、参加することが大事ってことで……。