mini.nvimの新モジュールmini.keymapで決めろマルチステップコンボ
この記事はVim駅伝の2025-05-14の記事です。
前回の記事はわたすけさんのNeovim v0.11のネイティブ補完APIをlazy.nvim+Mason 2.0で使うです。
Vim駅伝は常に参加者を募集しています。詳しくはこちらのページをご覧ください。
mini.nvimというNeovimプラグインがあります。モジュールという単位で多様な機能をまとめたプラグインです。
先日、mainブランチにmini.keymapという機能がマージされました。
本記事執筆時点ではまだstableにはなっておらず、ベータ版の状態です。以下のissueでフィードバックを募っています。
近い内に本リリースになると思われるので、本記事で機能の概要を解説します。
mini.keymapはmultistep
とcombo
の2つの主要な機能を持っています。
map_multistep
map_multistepはfallback mapを簡単に書くことができる機能です。
複数の機能を条件とともに一つのキーにマップし、条件を満たしている機能を実行します。
例として、以下のように設定します。stepsに指定しているのはビルトインステップの名前です。ここでは同じmini.nvimファミリーのmini.snippetsの機能を用いるものを指定していますが、Neovim本体のvim.snippet、プラグインのLuaSnip、nvim-cmp、blink.cmp、nvim-autopairsの機能にも対応しているようです。
local map_multistep = require('mini.keymap').map_multistep
local tab_steps = {
'minisnippets_next', -- スニペットが展開されていれば次の位置へ
'minisnippets_expand', -- スニペットが展開できるなら展開
'pmenu_next', -- 補完メニューが展開されていれば次の候補へ
'jump_after_tsnode', -- 「次のノード」が見つかればジャンプ
'jump_after_close', -- 閉じ括弧が見つかればその後にジャンプ
}
map_multistep('i', '<tab>', tab_steps)
local shifttab_steps = {
'minisnippets_prev', -- スニペットが展開されていれば前の位置へ
'pmenu_prev', -- 補完メニューが展開されていれば前の候補へ
'jump_before_tsnode', -- 「前のノード」が見つかればジャンプ
'jump_before_open', -- 開き括弧が見つかればその前にジャンプ
}
map_multistep('i', '<s-tab>', shifttab_steps)
jump_before_tsnode
やjump_after_close
はtabout.nvimを参考に作られたものらしく、挿入モード内で括弧(など)をまたいだ移動をできる機能です(現状は想定以上にジャンプしてしまうなど、ちょっとバギーな感じはします)。
また、ステップは自分で定義することもできます。実行条件を判定するcondition
と、実際の機能またはエミュレートするキーを返すaction
の2つの無引数関数を持つテーブルを定義し、map_multistep
に渡します。
筆者は<cr>
は以下のように定義しています。
local function has_trailing_word_after_completion()
if vim.fn.complete_info({ 'selected' }).selected == -1 then
-- complete item is NOT selected
return false
end
local current_line = vim.api.nvim_get_current_line()
if current_line == '' then
-- current line is empty
return false
end
local col = vim.fn.getcursorcharpos()[3]
local char_at_cursor = vim.fn.strcharpart(current_line, col - 1, 1)
local iskeyword = vim.regex('\\k'):match_str(char_at_cursor)
return iskeyword ~= nil
end
local function pmenu_accept_and_remove_trailing_word()
return '<c-y><c-o>"_de'
end
map_multistep('i', '<cr>', {
{
-- 条件:補完候補を選択していて、かつ直後にwordが続いていたら
condition = has_trailing_word_after_completion,
-- 機能:補完候補を確定しつつ、直後のwordを削除
action = pmenu_accept_and_remove_trailing_word,
},
'pmenu_accept', -- 補完候補が表示されていたら確定
'minipairs_cr', -- mini.pairsのcrを実行
})
has_trailing_word_after_completion
は補完候補選択中にカーソル直後に文字列が続いているときにtrue
となる関数です。pmenu_accept_and_remove_trailing_word
は補完候補を選択した後に直後のwordを削除します。これにより、補完時にバッファ文字列を上書きするような動きができるようになります。<cr>
を自分から押さなければ上書きは実行されないので、普通に入力していて邪魔になることもなく、たまに便利です。
pmenu_accept_and_remove_trailing_wordの動作例
map_combo
map_comboは短時間に一定のキーが押された場合のみ有効になるキーマップが書けます。複数キーの(ほぼ)同時押しや、同キーの連打によって発動するキーマップを作成できます。長くVim界隈にいる方には、vim-arpeggioのようなものというとわかりやすいかもしれません(とはいえmap_comboは内部的にvim.on_key
を使用していて、実装は大きく異なりますが)。
この動作自体は、better-escape.nvimを参考にしたもののようです。
「単語移動のw
やb
を連打すると段落移動になる」という例を考えてみましょう。素朴に実装すると以下のようになりますが、これでは1回目のw
で2キー目の待機が入ってしまいます。
vim.keymap.set({ 'n', 'x' }, 'ww', '}', {})
vim.keymap.set({ 'n', 'x' }, 'bb', '{', {})
map_combo()
を使うと待機が入らず、キー単体の機能を保ったまま、連打機能を追加することができます。ただし、いったん単体の機能も実行される(1回目に押したときには単語単位での移動も行われる)ことには注意です。
local map_combo = require('mini.keymap').map_combo
map_combo({ 'n', 'x' }, 'ww', '}')
map_combo({ 'n', 'x' }, 'bb', '{')
以下は<esc>
の2連打で<c-l>
を用いた画面のクリーンアップを行う例です。Neovimのデフォルトの拡張(:h CTRL-L-default
)と、vim._extui
のウィンドウの消去も行います。
map_combo({ 'n', 'i', 'x', 'c' }, '<esc><esc>', function()
local ok, extui_shared = pcall(require, 'vim._extui.shared')
if ok then
local extuiwins = extui_shared.wins[vim.api.nvim_get_current_tabpage()]
vim.api.nvim_win_set_config(extuiwins.box, { hide = true })
end
vim.cmd.nohlsearch()
vim.cmd.diffupdate()
return '<c-l>'
end)
extuiの消去は以下の記事を参考にしました。
そもそもextuiがなんなのか知りたい方は以下の記事へどうぞ。
以下はj
とk
の同時押しで挿入モードまたはコマンドラインモードから抜ける例です。実際はjk
またはkj
の短時間の連打なのですが、これら両方の順序を定義しておくことで、同時押しにマッピングしたかのように使えます。j
とk
がいったん入力されてしまうため、<bs>
2回で削除しています。
map_combo({ 'i', 'c' }, 'jk', '<bs><bs><esc>')
map_combo({ 'i', 'c' }, 'kj', '<bs><bs><esc>')
ヘルプには以下のように「同じキーを押し続けたら警告する」という例も載っています。アイデア次第でいろいろと機能を作れそうですね。
local notify_many_keys = function(key)
local lhs = string.rep(key, 5)
local action = function() vim.notify('Too many ' .. key) end
map_combo({ 'n', 'x' }, lhs, action)
end
notify_many_keys('h')
notify_many_keys('j')
notify_many_keys('k')
notify_many_keys('l')
まとめ
multistep
もcombo
も自作しようとすると面倒だったのですが、このモジュールで簡単に設定できるようになりました。「コンテキストをもとに特定の条件下でのみ機能が発動する」「キーを同時に押したときに機能が発動する」など、これまでとは違うマッピングの可能性を秘めた機能だと思います。
筆者もどんなマッピングが有効なのか試行錯誤中です。もし面白い設定を作れたら教えていただけると嬉しいです。
mini.nvimのほかのモジュールは以下の本にまとめているので、併せてご覧ください。
Discussion