コマンド履歴ウィンドウに曖昧検索機能を追加する方法

概要

この記事は Vim 駅伝 の 2025/10/10 の記事です。 前回の記事は私の Quickfix を Fuzzy Finder やバッファ管理などの UI として使う方法 | 閑古鳥ブログ でした。

前置き

コマンドラインウィンドウは Vim/Neovim の標準機能ですが、これまで全然使っていませんでした。というよりも、:q がタイプミスで q: となって意図せずにコマンドラインウィンドウが開くことがあり、イライラの原因になっていました。そのため、コマンドの再実行は snacks.nvim で実行していました。

しかし、前回の記事の方法で snacks.nvim を使わずに曖昧検索などをサクサク実行できるようになりましたので、コマンドの再実行も snacks.nvim を使わずにサクサクできるようになりたいと思うようになりました。

そこで、コマンドラインウィンドウを活用する方法を模索していたところ、vim-fuzzyhistory というプラグインを発見しました。このプラグインはコマンドラインウィンドウに曖昧検索を追加するもので、コマンドラインウィンドウでも絞り込み検索ができることを知りました。

ここまで来れば後は実装するだけであり、私の場合は絞り込み機能があれば用は足りそうなので、AI の力を借りて実装しました。

実際の動作

コマンドラインウィンドウを開いてから / をタイプするとコマンドラインに移動しますので、検索クエリを入力して絞り込みしていきます。絞り込み後に <Esc> をタイプすれば元に戻ります。

実装

実装は以下のとおりです。

  1local fuzzy_rank = require('util.fuzzy_rank')
  2
  3-- コマンドラインを使ってリアルタイムな曖昧検索
  4local function cmdline_fuzzy_search()
  5  local buf = vim.api.nvim_get_current_buf()
  6  local original_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
  7  -- 検索前の履歴を一時退避
  8  vim.b.cmdwin_original_lines = original_lines
  9
 10  -- 検索状態を保持するフラグ
 11  local search_active = true
 12  local group = vim.api.nvim_create_augroup('CmdwinFuzzySearch', { clear = true })
 13
 14  -- 入力のためにコマンドラインに移動
 15  vim.api.nvim_feedkeys(':', 'n', false)
 16
 17  -- CmdlineChangedイベントでリアルタイムフィルタリング
 18  vim.api.nvim_create_autocmd('CmdlineChanged', {
 19    group = group,
 20    callback = function()
 21      if not search_active then return end
 22      local query = vim.fn.getcmdline()
 23      -- 検索文字を全て削除した場合の処理
 24      if query == '' then
 25        vim.api.nvim_buf_set_lines(buf, 0, -1, false, original_lines)
 26      else
 27        local filtered = fuzzy_rank.rank(query, original_lines)
 28        if #filtered > 0 then
 29          vim.api.nvim_buf_set_lines(buf, 0, -1, false, filtered)
 30        else
 31          vim.api.nvim_buf_set_lines(buf, 0, -1, false, { '-- No matches found --' })
 32        end
 33      end
 34      -- コマンドラインを再描画
 35      vim.cmd('redraw')
 36      vim.cmd('normal! gg')
 37    end
 38  })
 39
 40  -- Enterキーで検索を確定するキーマッピングを追加
 41  vim.api.nvim_create_autocmd('CmdlineEnter', {
 42    group = group,
 43    once = true,
 44    callback = function()
 45      vim.keymap.set('c', '<CR>', function()
 46        search_active = false
 47        vim.api.nvim_clear_autocmds({ group = group })
 48        -- キーマッピングを削除
 49        pcall(vim.keymap.del, 'c', '<CR>')
 50        -- 空のコマンドを実行してコマンドラインを閉じる
 51        return '<C-u><Esc>'
 52      end, { expr = true })
 53    end
 54  })
 55
 56  -- ESCキーでキャンセル
 57  vim.api.nvim_create_autocmd('CmdlineLeave', {
 58    group = group,
 59    once = true,
 60    callback = function()
 61      vim.schedule(function()
 62        if search_active then
 63          -- キャンセルされた場合は元に戻す
 64          vim.api.nvim_buf_set_lines(buf, 0, -1, false, original_lines)
 65        end
 66        search_active = false
 67        vim.api.nvim_clear_autocmds({ group = group })
 68        -- キーマッピングを削除
 69        pcall(vim.keymap.del, 'c', '<CR>')
 70      end)
 71    end
 72  })
 73end
 74
 75vim.api.nvim_create_autocmd(
 76  { 'CmdwinEnter' },
 77  {
 78    callback = function()
 79      vim.keymap.set('n', 'q', '<Cmd>close<CR>', { buffer = true })
 80      -- コマンドライン曖昧検索
 81      vim.keymap.set('n', '/', cmdline_fuzzy_search, {
 82        buffer = true,
 83        desc = 'Fuzzy search with command line'
 84      })
 85
 86      -- このキーマップがあると `<Esc>` による復元処理がワンテンポ遅くなるので一時的に削除する
 87      pcall(vim.keymap.del, 'n', '<Esc><Esc>')
 88      -- <Esc> で検索結果を破棄してコマンド履歴を復元する
 89      vim.keymap.set('n', '<Esc>', function()
 90        local buf = vim.api.nvim_get_current_buf()
 91        local original = vim.b.cmdwin_original_lines
 92        if original then
 93          vim.api.nvim_buf_set_lines(buf, 0, -1, false, original)
 94          vim.b.cmdwin_original_lines = nil
 95          vim.cmd('normal! G')
 96        end
 97      end, { buffer = true, desc = 'restore command history.' })
 98      vim.fn["ddc#custom#patch_global"]({
 99        ui = 'none'
100      })
101    end
102  }
103)
104
105vim.api.nvim_create_autocmd(
106  { 'CmdwinLeave' },
107  {
108    callback = function()
109      pcall(vim.keymap.del, 'n', 'q')
110      pcall(vim.keymap.del, 'n', '/')
111      pcall(vim.keymap.del, 'n', '<Esc>')
112      vim.keymap.set('n', '<ESC><ESC>', '<Cmd>nohlsearch<CR>', { silent = true })
113      vim.fn["ddc#custom#patch_global"]({
114        ui = 'pum'
115      })
116    end
117  }
118)

大まかな処理の流れは次のとおりです。

まず、コマンドラインウィンドウに入った時点で、CmdwinEnter イベントを使って / に絞り込み処理を担当する cmdline_fuzzy_search() 関数を割り当てます。

cmdline_fuzzy_search() 関数では、vim.api.nvim_feedkeys(':', 'n', false): キーを送信してコマンドラインに移動し、検索クエリを入力できるようにします。

それから、CmdlineChanged イベントを使ってコマンドラインに入力された文字を vim.fn.getcmdline() を使ってリアルタイムに取得し、その文字列をクエリにして絞り込みしていきます。絞り込み処理は実際には曖昧検索という形で実行していますが、核となる検索機能と順位付けの処理は、前回の記事で登場したファイルの曖昧検索と同じ関数を使っています。

絞り込み処理が進んで目当てのコマンドが表示されたら、Enter キーを押して検索結果を確定します。このキーの割り当ては、CmdlineEnter イベントを使って実施しています。

また、間違って検索結果を確定したときに備えて、CmdwinEnter イベントでノーマルモードの Escape キーに履歴復元機能を割り当てています。そのために、cmdline_fuzzy_search() 関数ではオリジナルの履歴を退避してから検索を実施しています。なお、CmdlineLeave イベントは検索状態のクリーンアップと、キャンセル時の自動復元を担当しています。

実装した感想

これまで使いこなせていなかったコマンドラインウィンドウですが、絞り込み機能を付け足すことで便利に使えるようになり、コマンド再実行でも snacks.nvim を使うことが無くなりました。

今回のカスタマイズも前回の記事同様に Neovim のカスタマイズの奥深さが分かるもので、こういうカスタマイズができるのも Neovim を使うことの楽しさだと思います。