コマンド履歴ウィンドウに曖昧検索機能を追加する方法
概要
この記事は 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 を使うことの楽しさだと思います。