最初に謝罪
この記事は闇の魔術に対する防衛術のカレンダー | Advent Calendar 2021 - Qiita 25日目の記事です。
ふと気付いたらこの一年、全く記事を書いていなかったのでこの記事を書いています。12月24日の真夜中になってアドベントカレンダーの空きを探すという無謀なことに挑戦したのですが、案の定、関連がありそうなところは全部埋まってまして、なんとかこじつければ合っていそうなこちらにエントリーした次第。
ガチな Vimmer なら「闇の魔術」って何か分かりますよね? そう。↓これです。
Hello vim people, what do you mean by "dark powered" ? : vim
では改めて……
2021 年を振り返りますと、僕の脳内 Vim 界においては Lua! Lua! Neovim! の一年でしたのでそっち方面のこと色々書こうかと思ったのですが、それだと適当な分量に纏められる気がしません(さらに、闇の魔術とも関係ありません)。ここは手頃なネタとして、ここ数ヶ月ハマっている ddc.vim + Skkeleton 周りの小ネタについて書きます。
Skkeleton とは何か? については先日作者様から公開されたこちらを読んでいただくのが適当なので置いといて、今日のお題は以下のようなものです。
- 拙作プラグイン skkeleton_indicaator について。
- ddc.vim(これが闇の魔術です)で pum.vim を使っているときのワークアラウンド。
- 左右のコマンド(⌘)キーで Skkeleton を切り換える方法。
なお、Skkeleton 自体は Neovim / Vim 両対応のプラグインなのですが、僕自身は Neovim しか使っていません。この記事では Vim のことを考慮していませんので悪しからず。
skkeleton_indicator
delphinus/skkeleton_indicator.nvim のトップ画像がもう全てを表しています。Skkeleton は非常に大人しい IM(Input Method)でして、入力モードが切り替わっても特に表示は変わりません。Skkeleton には skkeleton-mode-changed
という自動コマンドイベントが提供されていますので、これに応じてモードを表示するのがこのプラグインの役目です。
なんか普通の IM と遜色ないリッチな UI でテンション上がりますよね? そうでもない?
ddc.vim + pum.vim 使ってるあなたに
ddc.vim には pum.vim という相棒がいまして、こいつを使うと補完ウィンドウを細かくカスタマイズしてリッチな見た目に出来るんです。これに関してもやはり作者様の記事がありますのでそちらを読んでいただくとして、こいつと Skkeleton を組み合わせる際には大きな問題があります。
(現状私の怠慢のせいで pum.vim を使用している場合 での補完ハンドリングを除く skkeleton が上書きするマッピングと被っている場合機能しなくなります、すみません)
上掲の記事で書かれてるこれですね。これは Skkeleton の(現在の)仕組みを考えると容易に解決できる問題ではないようで、pum.vim ユーザーの僕としては**「普段は pum.vim を有効にしつつ、Skkeleton のときだけネイティブな補完ウィンドウに戻す」**という対策を採ることにしました。
僕の設定内容はこの辺なのですが、俺々ライブラリとか使ってるので読みにくいと思います。以下では同様のコードを書いて説明します。
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 のこのコミットで解消したようです。ここには説明文を載せていましたが、省略しておきます。
要らなくなったワークアラウンド
ここまでは納得の解決法なのですが、イケてないのはこの部分。
pum_close_timer = vim.fn.timer_start(
100,
function()
if fn['pum#visible']() == 1 then fn['pum#close']() end
end,
{['repeat'] = -1}
-- ↑「repeat」は Lua の予約語なのでこう書かないとダメなのだ!
)
左右の⌘キーで Skkeleton を ON/OFF する方法
Mac ユーザーにはおなじみ
「⌘」って言ってる時点で macOS 限定ではあるんですけどね。特に日本語キーボードユーザーにはおなじみと思うんですが、IM を右⌘キーで ON、左⌘キーで OFF にするってのが大変快適なんです。これを何とか Skkeleton で実現したいんですが、⌘キーだけでなくシフト(⇧)、コントロール(⌃)、オプション(⌥)といった Modifier キーは、押しただけではターミナルでキーコードが読み取れず、その中の (Neo)Vim では反応できません。
macOS にはこういう時の心強い味方として Karabiner-Elements があります。これを使って以下のような方法で解決できるかと思ったんです。
- Karabiner-Elements で左右の⌘キーを押した時、それぞれ F10 / F13 を送信する。
- Neovim 側では F10 / F13 を Skkeleton の無効 / 有効にマッピングする。
F10 / F13 にしてるのは大した意味はなくてですね。Neovim 上で特に副作用なく使えるキーなら何でもいいんです(以外とこれを探すのが難しい)。ただこれだけだと問題ががありまして……当然ですが、Neovim が起動してなくても、⌘キーを押したときに F10 / F13 が入力されてしまうんです。
使いたい時だけ、マッピングを有効にする
これ「左右の⌘キーで IM 切り換える民」なら解ってもらえると思うんですが、この入力法を使うようになると、文字を入力する前、無意識に左⌘キーを連打するようになっちゃうんです。取りあえず押しとけ、みたいな。設定にも寄るんですが、ターミナルで F10 のようなファンクションキーを押すと、なんか変な文字列が入力されちゃうんですよね。大変ストレスです。
これを解決するには Neovim 側で Karabiner-Elements に対し、Neovim 自身が起動中であることを通知してあげる必要があります。こういうとき便利なのが Karabiner-Elements の Command line interface です。Karabiner-Elements には karabiner-cli
という CLI コマンドが付属していまして、こいつを使って任意のアプリから Karabiner-Elements を制御できるんです。
この為にはまず、Karabiner-Elements 側で以下のようなマッピングを指定します(GitHub のここに完全な設定ファイルを置いています)。
{
"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
)がありまして、こいつが特定の値の時のみ有効なマッピングが定義できるんです。
後は想像付くと思いますが、Neovim 側では karabiner-cli
を使ってこの値を制御します(完全なソースはこちら)。
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
を送信してるんですね。
いい感じですね!
終わりに
なんというか、非常にニッチで誰得な情報ばかりだと思うんですが、まあ、参加することが大事ってことで……。