🎬

Neovimでqキーをprefixとして使う

に公開

この記事はVim駅伝の2025-07-21の記事です。
前回の記事はatusyさんのgit statusをfuzzy findする時にプロンプトタイトルにブランチ情報を出すと便利 with telescope.nvimです。

Vim駅伝は常に参加者を募集しています。詳しくはこちらのページをご覧ください。


課題

Neovimでプラグインをたくさん入れていると、だんだん使用できるキーマップが足りなくなってきます。
Neovimは複数キーのキーシーケンスに対してマッピングを設定できるので、prefixキーを使うことでキーマップを増やすというアイデアがあります。

https://zenn.dev/vim_jp/articles/2023-05-19-vim-keybind-philosophy

で、既存機能を潰しづらく、prefixにしたくなるキーの一つがqです。ただし、qキーはマクロの開始・終了に使用されるため、単純にprefixとすることはできず、ちょっと工夫した設定が必要になります。
以下の記事で解説されています。

https://thinca.hatenablog.com/entry/q-as-prefix-key-in-vim

マクロを何種類も使って切り替えることはほとんどないと仮定し(ちなみに筆者はまったくない)、特定のレジスタ(筆者はqレジスタ)だけを記録用にする仕組みです。

ここまでの問題意識や設定例は以下の記事でも述べました。

https://zenn.dev/vim_jp/articles/43d021f461f3a4

さいきん、この「qキーのprefix化」をNeovim向けに別の方法で実装して、しばらく使ってみて良い感じだったため本記事で解説します。

やりたいこと

実現したいことは以下です。

  1. qqでマクロ記録開始
    • qレジスタは通常のマクロ記録先として使う
  2. マクロ記録中はqで記録停止
    • 次のキーを待機せずに 即座に記録を停止
  3. マクロ記録中 以外qをprefixキーとして活用
    • 次のキーを待機
    • 2キー目以降が入力されたら、それに対応する機能が発火

素朴に実装すると以下のようになります。

-- マクロ記録開始
vim.keymap.set('n', 'qq', 'qq', { desc = 'start recording' })

-- 現在のウィンドウ以外を閉じる
vim.keymap.set('n', 'qo', '<cmd>only<cr>', { desc = 'only' })

-- 直前のバッファに切り替え
vim.keymap.set('n', 'qt', '<c-^>', { desc = 'toggle buffer' })

これは目的の2で挙げた「即座に記録を停止」を実現できません。マクロ記録中にqを押したときに、「qなのかqoなのか」を判断できないため、次のキーの待機が発生します。これは避けたい。

アイデア

上記の待機が発生するのは、qキーがprefixと停止の両方に使われているためです。
そこで、マクロに入っている間はprefixとしてのマッピングを一時的に潰せば良いのではないかと考えました。

ここで「Neovim向け」のポイントが出てきます。マクロの開始と停止で発火するRecordingEnterRecordingLeaveイベントを利用します。
これらは記事執筆時点でNeovimにしかありません。

https://neovim.io/doc/user/autocmd.html#RecordingEnter

設定例

行う設定は以下のとおりです。

  1. RecordingEnterでマクロに入ったときにqキーをprefixとしたマッピングが動かないようにする
  2. RecordingLeaveでマクロから出たときに設定を復旧
-- マクロ記録開始時に発火
vim.api.nvim_create_autocmd('RecordingEnter', {
  pattern = '*',
  group = augroup_wrapper,
  callback = function()
    -- q以外のマクロは使わないという強い意志で即終了
    if vim.fn.reg_recording() ~= 'q' then
      vim.cmd('normal! q')
      return
    end

    -- マクロ記録中のみ有効なaugroup
    local augroup_inner = vim.api.nvim_create_augroup('prefix-q-inner', {})

    -- カレントバッファを取得
    local buffer = vim.api.nvim_get_current_buf()

    -- buffer-localでqを即終了キーに設定することでprefixを無視
    vim.keymap.set('n', 'q', 'q', { nowait = true, buffer = buffer })

    -- BufferやWindowをまたぐマクロはないだろうということで強制終了
    vim.api.nvim_create_autocmd({ 'BufLeave', 'WinLeave' }, {
      pattern = '*',
      once = true,
      group = augroup_inner,
      callback = function()
        vim.cmd('normal! q')
        vim.notify('stop recording', vim.log.levels.INFO)
      end,
      desc = 'stop recording when leaving buffer',
    })

    -- マクロ記録終了時にqキーのマッピングとautocmdを削除
    vim.api.nvim_create_autocmd('RecordingLeave', {
      pattern = '*',
      once = true,
      callback = function()
        vim.keymap.del('n', 'q', { buffer = buffer })
        vim.api.nvim_del_augroup_by_id(augroup_inner)
      end,
      desc = 'delete q mapping when recording leave',
    })
  end,
})

ここでは、BufferやWindowをまたぐ動作をマクロにしないだろうという前提で、qnowaitなローカルマッピングとして定義することで、ほかのマッピングを上書きする戦略をとりました。
BufferやWindowを移動する動きもマクロにしたい場合は、RecordingEnterでmaplist()とかを使って定義を取り出して保存するみたいな処理を挟めばできると思います。試していませんが。

おまけ mini.clueと組み合わせる場合

筆者は以下のbookで書いた通りmini.clueを使ってキー入力アシストを表示させています。

https://zenn.dev/kawarimidoll/books/6064bf6f193b51/viewer/ec6109

今回の設定も、mini.clueと組み合わせることができます。

local clue = require('mini.clue')
clue.setup({
  triggers = {
    -- `q` key
    { mode = 'n', keys = 'q' },
  },
})

ただし、mini.clueがmappingを上書きしてくようなので、マクロ記録停止のqのマッピングを一瞬遅延させる必要があります。手段は何でも良いと思うのですが、筆者はCursorMovedにフックしました。

-- HACK: mini.clueがbuffer mappingを上書きしてくるのでautocmd CursorMovedで設定する
vim.api.nvim_create_autocmd('CursorMoved', {
  pattern = '*',
  once = true,
  group = augroup_inner,
  callback = function()
    vim.keymap.set('n', 'q', 'q', { nowait = true, buffer = buffer })
  end,
  desc = 'set stop-recording key',
})

これで、qを押すと以下の画像のように入力アシストが表示されます。

既存手法と比較すると、今回の手法ではautocmdを別に定義する必要がある代わりに、qをprefixとしたマッピングをそのまま書くことができます。<sid>(q)のような書き方が不要になり、以下をそのまま書けます。

-- 前述のautocmd定義をしておく(省略)

-- マクロ記録開始
vim.keymap.set('n', 'qq', 'qq', { desc = 'start recording' })

-- 現在のウィンドウ以外を閉じる
vim.keymap.set('n', 'qo', '<cmd>only<cr>', { desc = 'only' })

-- 直前のバッファに切り替え
vim.keymap.set('n', 'qt', '<c-^>', { desc = 'toggle buffer' })

まとめ

qキーをprefixに使う手法を紹介しました。
これで、「自分でマッピングを定義していたらキーが足りなくなってきた」という問題を少し和らげることができると思います。

なお、RecordingEnter/RecordingLeaveはNeovimにしかないEventですが、マクロ記録開始のqqに今回示したような関数自体をマッピングすることで、Vimでも同様のことができるかもなーと思っています。

Discussion