はじめに
近年、Emacsのミニバッファー補完インフラにおいては、selectrumやverticoのような外部ライブラリーが活用されてきています。これらは、いずれも
- 補完ウインドウを開かずに、ミニバッファー自体を広げて補完候補を表示すること
- 補完コマンドの実行が不要で、インクリメンタルに補完候補を更新すること
- 補完候補には、各種属性を付与して縦に並べること
といった特徴を持っています。今後は、同様の特徴を持ち、Emacs28以降で標準のLISPとして提供されるようになったfido-vertical-modeがじわじわとシェアを伸ばしてくるだろうと予想されます。
これらの比較、解説については、既にいくつか記事が出回っていますのでそちらを参照していただくとして、そういえばEmacs31が見えてきた2026年6月時点においても、fido-vertical-modeを使い倒している記事はそんなに多くないな、ということに気が付きました。そこで、現状のEmacs30.2や(β版の)Emacs-31.0.90において、どこまでこれが使えるようになっているのかを試してみたところ、少し設定(adviceによる挙動のカスタマイズを含む)を追加するだけで普通に実用に足りることがわかりました。というわけで、せっかくなので記事化しておこうと思います。
Emacs脱初心者を目指す方がおそらく興味を持っているであろう、各種メジャー・マイナーモードの設定をどのように組んでいくのか、という観点でも参考にしやすいように、ポイント毎にステップを踏んだ形で紹介します。
fido-vertical-modeとは
fido-vertical-modeは、大雑把に言って、以下のモードを組み合わせて実装されたものと考えて差し支えないでしょう。fido-vertical-modeの下請けとして、これらのモードも同時に有効化されます。設定を行うにあたっては、これらの関係性をある程度把握しておいたほうが理解しやすいかもしれません1。
- icomplete-mode
標準の補完インフラでインクリメンタル補完を実現するモード - fido-mode2
補完機能にいくつかの拡張を加えるido-modeのicomplete版 - icomplete-vertical-mode
補完結果を縦方向に表示するモード
標準外のライブラリーとも連携するとはいえ、主要部分については標準機能だけで網羅されているのがポイントです。このためなのかはわかりませんが、実際に使ってみた限りでは、他より動作が軽い印象を受けました。また、詳細は後述しますが、特にtrampでリモートのファイルを開くことが多い人にとっては使いやすい仕様となっているのではないかと思われます。
対象環境
- Emacs30.2
- Emacs31.0.90
Emacs29でもおそらく同じように動くのではないかと予想していますが、未確認です。
設定概要
ほぼ標準ライブラリーの設定となるため、init.el(もしくはsite-start.el)に直接記述する前提です。orderlessやmarginaliaといった外部ライブラリーを各種パッケージャー経由で設定する場合は、当該部分を適宜読み替えていただければと思います。
基本設定
単にfido-vertical-modeをグローバルで有効化するだけです。
(fido-vertical-mode 1)
有効化の際には、verticoやspectrumほどではないものの、芋蔓式に複数のライブラリーがロードされるため、起動時間を「詰めて」いる場合は、初期化処理をタイマーに任せたくなるかもしれません。その場合は、上記の代わりに以下のようにすればよいでしょう。
(defvar fvm:init-timer nil)
(add-hook 'after-init-hook
;; fido-vertical-modeの初期化は依存ライブラリーのロードのために遅いことから、
;; タイマーに仕掛けておく
(lambda ()
(setq fvm:init-timer
(run-with-timer
0.2 0.1
(lambda ()
(fido-vertical-mode 1)
(cancel-timer fvm:init-timer)
)))
))
補完モードのカスタマイズ
インクリメンタル補完と相性のよい補完スタイルや補完候補の並び順を提供するorderlessや、補完候補に各種属性を付与するmarginaliaも併せて利用したくなるかと思います。fido-vertical-modeの有効化時に呼び出されるfido-vertical-mode-hookで、これらの設定を行います。
marginaliaについては、marginalia-modeを呼ぶだけですが、orderlessについてはモード実装ではないため、ライブラリーをロードした後、ロード時フックでcompletion-stylesにorderlessを含むように補完モードのリストを設定するのが本来のやり方です(詳細後述)。
実際、vertico等の他の補完インフラではこれで問題ないのですが、fido-vertical-modeでは、下請けであるfido-modeが、minibuffer-setup-hookでcompletion-stylesを上書きするようなフックを追加している3ため、こちらにもフックを追加して再度上書きする必要があります。
add-hookの第3引数に正の値を渡すことで、fido-mode側のフックの後に実行されることが保証されます4。
第3引数がないと、このフックの実行後にfido-mode側のフックが実行されてしまうため、うまく設定できません。
(add-hook 'fido-vertical-mode-hook
(lambda ()
(require 'orderless)
(marginalia-mode)
))
(add-hook 'fido-mode-hook
(lambda ()
(add-hook 'minibuffer-setup-hook
(lambda ()
;; お好みで他の補完スタイルを追加してもよい
(setq-local completion-styles '(orderless))
)
;; フックの実行順を制御
1)
))
パッケージャーを使わない場合、orderless.el[c]とmarginalia.el[c]をload-pathの切れたとこに放り込んでおきましょう。おそらく、(marginalia-mode)したタイミングでmarginalia.el[c]がautoloadされるはずですが、念のためmarginaliaのautoloadも仕込んでおきます。
(autoload 'marginalia-mode "marginalia"
"Annotate completion candidates with richer information." t)
パッケージャーを用いる場合は、ファイルの格納やautoloadの設定の代わりにパッケージャーで有効化することとなります。
marginariaの設定は、marginalia-mode-hookでもいいのですが、単純な変数設定とadviceの追加だけなので、ベタ書きでも構いません。以下の設定例では、タイムスタンプを常に絶対時刻表示にする設定と、標準の時刻フォーマットが長すぎるのでadviceで上書きする設定が含まれます。お好みでどうぞ。
;; 全て絶対時間表示にする
(setq marginalia-max-relative-age 0)
;; 絶対時間のフォーマットをカスタマイズ
(defun marginalia:fixed-time-format (time)
(format-time-string "%Y/%m/%d %H:%M" time))
(advice-add 'marginalia--time-absolute :override 'marginalia:fixed-time-format)
orderlessの設定は、適切なフックが見当たらないため、eval-after-loadでロード時フックとして仕込みます。ただし、fido-vertical-modeからのみ使う場合は、おそらくこのあたりの設定は不要のはずです。companyやcapeのようなキーワード補完機能向けの設定例と考えてください。
前述の通り、ここで設定しているcompletion-stylesは、fido-vertical-modeでは上書きされてしまうため効きません。逆に言うと、ミニバッファー補完とキーワード補完とで補完スタイルを変えたい場合は、fido-vertical-modeを使っている限りはここで書き分けることができるということです。
(with-eval-after-load 'orderless
;; fido-vertical-mode以外の補完インフラ用
(setq completion-styles '(orderless))
(setq completion-category-defaults nil)
(setq completion-category-overrides nil)
(setq tab-always-indent 'complete)
(setq tab-first-completion 'eol)
)
ミニバッファー補完の表示を整える
補完時にミニバッファーをどのくらい広げるのかについては、設定の追加が必須と思われます。ここでは、max-mini-window-heightに0.3を設定することで、フレームの高さの3割程度を補完候補表示用に割り当てていますが、実数値を変更することで割合を調整したり、整数値に変更することで割り当てる絶対的な行数として指定したりすることも可能です。詳細は参考文献1を参照ください。
また、marginaliaを有効化していると起こりやすいのですが、補完結果が長すぎてウインドウ幅からはみ出してしまうと、ミニバッファー内で表示が折り返されることで表示が破綻してしまいます。このため、truncate-linesをtに設定して折り返しを抑止しています。
なお、Emacs30には(おそらく)バグがあり5、補完候補が「ミニバッファーの画面外」に1行ぶんはみ出してしまうため、最後の1件がスクロールしても画面内に入ることがなくなり、見ることができない、という問題があります。こちらについては、算出した補完領域の表示可能行数をadviceで補正することによって対処します。
(add-hook 'icomplete-minibuffer-setup-hook
(lambda ()
(setq-local max-mini-window-height 0.3)
(setq-local truncate-lines t)
))
;; xxx ミニバッファーのサイズを調整
(defun fvm:adjust-mini-window-height (orig-func &rest args)
(let ((icomplete-prospects-height
(- (min
icomplete-prospects-height
(truncate (max-mini-window-lines) 1))
;; xxx total-spaceの値を2減らさないと表示がはみ出してしまう
2)))
(apply orig-func args)))
(advice-add 'icomplete--render-vertical :around 'fvm:adjust-mini-window-height)
キーバインドを設定する
fido-vertical-mode、というかicomplete-mode系全般での話なのですが、せっかくミニバッファー上でインクリメンタルに補完候補を絞り込めるのに、スペースキーを叩くと従来通りの補完ウィンドウが出現してしまいます。当初、これはfido-vertical-modeが進化途上であるがゆえの不具合なのだろうと思い込んでいたのですが、よくよく考えると、これは単にキーバインドの不備でしかないのではないか、ということに気が付いてしまいました。icomplete-minibuffer-mapで、スペースはself-insert-commandにバインドして普通に入力できるようにします。これは、複数キーワードでの絞り込みを行うorderlessとの相性がよく、操作性の向上に寄与します。
Tabについてはミニバッファー補完処理を強制実行するキーに割り当てます。これは、trampを用いたリモートファイルのアクセスに効果的です。というのも、verticoと違って、fido-vertical-modeおよび下請けとなるicomplete-vertical-modeでは、ファイル補完時に先頭で/を叩いた場合ローカルファイルしか見に行かないのです。この設定を行うことで、/ホスト名:まで打った後Tabを叩くことでtrampでのリモートファイルへのアクセスが有効になるようにできます。
これは一見面倒に見えますが、リモートホストの入力中に、trampのホスト名キャッシュに一致するリモートファイルシステムを含めた補完処理をリアルタイムに実行するverticoの仕様は、特にtramp-default-user-alist等を設定していない場合やSSHエージェントを動かしていない場合において、キーを叩く度にユーザー名やパスワード(またはパスフレーズ)を聞かれてしまうため、非常に煩わしく感じます。このため、ホスト名を全部打った後、明示的にSSHアクセスをトリガーできるこちらの仕様のほうが、使い勝手の面では優れているのではないかと考えます。もちろん、trampのホスト名キャッシュは有効なので、ホスト名を候補から選択した後Tabを叩く、という操作も可能です。
verticoと違って、fido-vertical-mode(が下請けにしているicomplete-mode)は、カレントディレクトリーである.を補完対象とするため、/の直後でTabを打つと、延々と.が補完されていきます。少し気持ち悪い気もしますが、これは一旦こういう仕様ということで許容することにします。
(add-hook 'icomplete-mode-hook
(lambda ()
(let ((map icomplete-minibuffer-map))
(define-key map " " 'self-insert-command)
(define-key map "\C-i" 'icomplete-force-complete)
(define-key map [tab] 'icomplete-force-complete)
)
))
verticoでC-hにバインドすることを想定しているvertico-directory-*系のコマンドと同等のものが、fido-modeで提供されています。icomplete-fido-mode-mapに設定するのがよさそうです。
(add-hook 'fido-mode-hook
(lambda ()
(let ((map icomplete-fido-mode-map))
(define-key map "\C-h" 'icomplete-fido-backward-updir)
(define-key map "\M-h" 'backward-delete-char)
(define-key map [backspace] 'icomplete-fido-backward-updir)
)
))
機能追加
verticoでは、vertico-cycleをtに設定するだけで、先頭と末尾をループで移動できますが、fido-vertical-modeや、下請けとなるicomplete-vertical-modeにはこのような機能は今のところありません。これではちょっと不便なので、adviceで機能を追加します。fvm:cycle-completionsで制御できるようにしています。
;; vertico-cycleと同様の、next/prevでの補完候補のループ
(defvar fvm:cycle-completions t)
(defun fvm:next-with-cycle (orig-func &rest args)
(if (and (interactive-p) icomplete-scroll fvm:cycle-completions)
(or (apply orig-func args)
(icomplete-vertical-goto-first))
(apply orig-func args)))
(advice-add 'icomplete-forward-completions :around 'fvm:next-with-cycle)
(defun fvm:prev-with-cycle (orig-func &rest args)
(if (and (interactive-p) icomplete-scroll fvm:cycle-completions)
(or (apply orig-func args)
(icomplete-vertical-goto-last))
(apply orig-func args)))
(advice-add 'icomplete-backward-completions :around 'fvm:prev-with-cycle)
また、C-vやM-v等で呼び出されるページ移動系コマンドも存在しないため、関数とキーバインドを追加します。併せて、先頭や末尾に移動する機能を編集キーにも割り当てています67。
;; 補完候補の次ページ移動
(defun fvm:next-page ()
(interactive)
(let ((n (1- (/ (1- (min icomplete-prospects-height
(truncate (max-mini-window-lines) 1)))
2))))
(while (and (> n 0) (icomplete-forward-completions))
(setq n (1- n)))))
;; 補完候補の前ページ移動
(defun fvm:prev-page ()
(interactive)
(let ((n (1- (/ (1- (min icomplete-prospects-height
(truncate (max-mini-window-lines) 1)))
2))))
(while (and (> n 0) (icomplete-backward-completions))
(setq n (1- n)))))
(add-hook 'icomplete-vertical-mode-hook
(lambda ()
(let ((map icomplete-vertical-mode-minibuffer-map))
(define-key map "\C-v" 'fvm:next-page)
(define-key map "\M-v" 'fvm:prev-page)
(define-key map [next] 'fvm:next-page)
(define-key map [prior] 'fvm:prev-page)
(define-key map [S-down] 'fvm:next-page)
(define-key map [S-up] 'fvm:prev-page)
(define-key map [find] 'icomplete-vertical-goto-first)
(define-key map [select] 'icomplete-vertical-goto-last)
(define-key map [home] 'icomplete-vertical-goto-first)
(define-key map [end] 'icomplete-vertical-goto-last)
)
;; xxx pixel-scroll-precision-mode有効時はpixel-scroll-precision-mode-mapを
;; 優先して参照するため、PgUp(prior)/PgDn(next)がバインドされていると
;; icomplete-vertical-mode-minibuffer-mapのキーバインドが無視される
(when (and pixel-scroll-precision-mode pixel-scroll-precision-mode-map)
(let ((map pixel-scroll-precision-mode-map))
(define-key map [next] nil)
(define-key map [prior] nil)
))
))
icomplete-vertical-mode-minibuffer-mapでPageDownとPageUpをバインドしています7が、Emacs29で導入されたpixel-scroll-precision-modeを有効にしていると、pixel-scroll-precision-mode-mapが最優先で参照される仕様となっており、このマップにバインドされたPageDown/PageUpのデフォルトキーバインド(pixel-scroll-interpolate-down/up)が呼び出されてしまいます。
pixel-scroll-interpolate-down/upは、global-mapでバインドされているscroll-up/down-commandとほぼ挙動は変わらない気がします8ので、pixel-scroll-precision-mode側のキーバインドを解除してしまうのが簡単かと思われます。後半部分のwhenの中は、この処理を行うものです。
上記の設定は、pixel-scroll-precision-modeがfido-vertical-modeより先に設定されている前提のものです。初期化順が逆になる場合は、pixel-scroll-precision-mode-hookで設定するか、あるいは以下のように、有効化後に明示的にキーバインドを解除することとなります。このくらいの規模の処理であれば、これがシンプルでいいのかもしれません。
(when window-system
(pixel-scroll-precision-mode 1)
;; xxx 各キーマップでの置き換えが効かなくなるので解除しておく
(define-key pixel-scroll-precision-mode-map [next] nil)
(define-key pixel-scroll-precision-mode-map [prior] nil)
)
補完時の例外対策(Emacs30以前のみ)
Emacs30までの話ですが、fido-vertical-modeおよびverticoのどちらにおいても、ファイル名補完の際にミニバッファーを空にすると、内部で例外が発生してしまい、以降補完が効かなくなってしまいます。
これは、/で補完を実行すると、ルートディレクトリー自体が空文字列として補完されるため、これにtext-propertyを付与しようとした際に範囲外エラーが出てしまうことによります。
問題の処理は、completion--twq-allの中にあり、ここでput-text-propertyを呼ぶかどうかをチェックすれば直るのですが、この方法だとadviceでの対応ができないため、関数をまるごと再定義することになります。そこで、当面はput-text-propertyに例外を無視するようなadviceを追加することで回避することとします。
Emacs31.0.90で、この問題は解消しており、本節のadviceは不要となりますが、バージョンを確認して除外しているので、入れていても害はありません。
;;; xxx 例外が出るのでパッチ (fido-vertical-mode, vertico共通)
(when (< emacs-major-version 31) ; Emacs31以降では不要
(defun adv:ignore-error (orig-func &rest args)
(ignore-errors
(apply orig-func args)))
(advice-add 'put-text-property :around 'adv:ignore-error))
全設定一覧
設定内容を全てまとめるとこのようになります。
;;;
;;; fido-vertical-modeの設定
;;;
(add-hook 'fido-vertical-mode-hook
(lambda ()
(require 'orderless)
(marginalia-mode)
))
(add-hook 'fido-mode-hook
(lambda ()
(add-hook 'minibuffer-setup-hook
(lambda ()
(setq-local completion-styles '(orderless))
)
1)
(let ((map icomplete-fido-mode-map))
(define-key map "\C-h" 'icomplete-fido-backward-updir)
(define-key map "\M-h" 'backward-delete-char)
(define-key map [backspace] 'icomplete-fido-backward-updir)
)
))
(add-hook 'icomplete-mode-hook
(lambda ()
(let ((map icomplete-minibuffer-map))
(define-key map " " 'self-insert-command)
(define-key map "\C-i" 'icomplete-force-complete)
(define-key map [tab] 'icomplete-force-complete)
)
))
(add-hook 'icomplete-minibuffer-setup-hook
(lambda ()
(setq-local max-mini-window-height 0.3)
(setq-local truncate-lines t)
))
(add-hook 'icomplete-vertical-mode-hook
(lambda ()
(let ((map icomplete-vertical-mode-minibuffer-map))
(define-key map "\C-v" 'fvm:next-page)
(define-key map "\M-v" 'fvm:prev-page)
(define-key map [next] 'fvm:next-page)
(define-key map [prior] 'fvm:prev-page)
(define-key map [S-down] 'fvm:next-page)
(define-key map [S-up] 'fvm:prev-page)
(define-key map [find] 'icomplete-vertical-goto-first)
(define-key map [select] 'icomplete-vertical-goto-last)
(define-key map [home] 'icomplete-vertical-goto-first)
(define-key map [end] 'icomplete-vertical-goto-last)
)
;; xxx pixel-scroll-precision-mode有効時はpixel-scroll-precision-mode-mapを
;; 優先して参照するため、PgUp(prior)/PgDn(next)がバインドされていると
;; icomplete-vertical-mode-minibuffer-mapのキーバインドが無視される
(when (and pixel-scroll-precision-mode pixel-scroll-precision-mode-map)
(let ((map pixel-scroll-precision-mode-map))
(define-key map [next] nil)
(define-key map [prior] nil)
))
))
(defvar fvm:init-timer nil)
(add-hook 'after-init-hook
;; fido-vertical-modeの初期化は依存ライブラリーのロードのために遅いことから、
;; タイマーに仕掛けておく
(lambda ()
(setq fvm:init-timer
(run-with-timer
0.2 0.1
(lambda ()
(fido-vertical-mode 1)
(cancel-timer fvm:init-timer)
)))
))
;; vertico-cycleと同様の、next/prevでの補完候補のループ
(defvar fvm:cycle-completions t)
(defun fvm:next-with-cycle (orig-func &rest args)
(if (and (interactive-p) icomplete-scroll fvm:cycle-completions)
(or (apply orig-func args)
(icomplete-vertical-goto-first))
(apply orig-func args)))
(advice-add 'icomplete-forward-completions :around 'fvm:next-with-cycle)
(defun fvm:prev-with-cycle (orig-func &rest args)
(if (and (interactive-p) icomplete-scroll fvm:cycle-completions)
(or (apply orig-func args)
(icomplete-vertical-goto-last))
(apply orig-func args)))
(advice-add 'icomplete-backward-completions :around 'fvm:prev-with-cycle)
;; 補完候補の次ページ移動
(defun fvm:next-page ()
(interactive)
(let ((n (1- (/ (1- (min icomplete-prospects-height
(truncate (max-mini-window-lines) 1)))
2))))
(while (and (> n 0) (icomplete-forward-completions))
(setq n (1- n)))))
;; 補完候補の前ページ移動
(defun fvm:prev-page ()
(interactive)
(let ((n (1- (/ (1- (min icomplete-prospects-height
(truncate (max-mini-window-lines) 1)))
2))))
(while (and (> n 0) (icomplete-backward-completions))
(setq n (1- n)))))
;; xxx ミニバッファーのサイズを調整
(defun fvm:adjust-mini-window-height (orig-func &rest args)
(let ((icomplete-prospects-height
(- (min
icomplete-prospects-height
(truncate (max-mini-window-lines) 1))
;; xxx total-spaceの値を2減らさないと表示がはみ出してしまう
2)))
(apply orig-func args)))
(advice-add 'icomplete--render-vertical :around 'fvm:adjust-mini-window-height)
;;; xxx 例外が出るのでパッチ (fido-vertical-mode, vertico共通)
(when (< emacs-major-version 31) ; Emacs31以降では不要
(defun adv:ignore-error (orig-func &rest args)
(ignore-errors
(apply orig-func args)))
(advice-add 'put-text-property :around 'adv:ignore-error))
;;;
;;; marginalia-modeの設定
;;;
(autoload 'marginalia-mode "marginalia"
"Annotate completion candidates with richer information." t)
;; 全て絶対時間表示にする
(setq marginalia-max-relative-age 0)
;; 絶対時間のフォーマットをカスタマイズ
(defun marginalia:fixed-time-format (time)
(format-time-string "%Y/%m/%d %H:%M" time))
(advice-add 'marginalia--time-absolute :override 'marginalia:fixed-time-format)
;;;
;;; orderlessの設定
;;;
(with-eval-after-load 'orderless
;; fido-vertical-mode以外の補完インフラ用
(setq completion-styles '(orderless))
(setq completion-category-defaults nil)
(setq completion-category-overrides nil)
(setq tab-always-indent 'complete)
(setq tab-first-completion 'eol)
)
おわりに
実際にfido-vertical-modeを使ってみましたが、実はEmacs29時点で既に実用レベルに到達していたのではないかという気がしました。従来のselectrumやverticoと比べても、それぞれ完成度や細かいところの挙動といった差異はありますが、それぞれカスタマイズを加えることである程度は挙動を揃えることができます。その上で、お好みのモードを取捨選択することとなりますが、選択肢が増えるのはいいことだと思いますので、参考になれば幸いです。
参考文献
-
設定変数やフックが、icomplete-mode由来のもの、fido-mode由来のもの、icomplete-vertical-mode由来のものと、fido-vertical-mode由来のものに分散しており、例えばキーバインドを追加しようとした場合に、どのモードのキーマップに追加するのが最適なのか、というのは少しばかり知識が必要です。もっとも、fido-vertical-modeしか使わない、というのであれば、ここについては気にせずとも実用上は問題ないのですが、設定変数を探る場合はやはり面倒です。 ↩
-
Emacs infoによると、fidoとは「Fake IDO」の略で、idoは「Incremental DO」の略とのことです。 ↩
-
Emacs infoのicompleteの項によると、これはfido-modeの仕様であるとのことです。 ↩
-
詳細は参考文献1を参照。当該記事ではadd-hookの第3引数に
tを渡していますが、add-hookのDocStringによると、これは過去互換性のために許容されているやり方で、現在の仕様では数値指定が推奨のようです。 ↩ -
fido-vertical-modeではなくicomplete-vertical-mode側の問題です。また、Emacs31.0.90でも修正されていません。 ↩
-
PCでのHome/Endキーに相当するキーは、環境によってはFind/Selectとして認識されることがあるため、両方設定しています。 ↩
-
PageDown/PageUpキーは、Next/Priorキーとして認識されます。Home/Endと異なり、こちらはそもそもPageDown/PageUpのキーシンボルが存在しません。 ↩ ↩2
-
実際、C-v/M-vをpixel-scroll-precision-down/upにバインドしている方は、ほとんどいないのではないでしょうか? ↩