2024-12-12

which-key.elとメニューの定義

Emacs 30からwhich-key-modeが追加されます。which-key-mode自体は何年も前からある(github.com/justbur/emacs-which-key)ようですが、それがEmacsに組み込まれたということのようです。私はこれまで使ったことはありませんでしたが、手元のEmacs 30.0.92に入っていたので使ってみました。

そもそもwhich-key-modeは何かというと、次に押すべきキーを教えてくれる、というと抽象的で分かりづらいでしょうか。一言で言うと、自動的にキーメニューを表示してくれる(グローバルマイナー)モードです。例えばC-xと入力して少し待つ(次に打つべきキーを迷っている)とC-xで始まるキー割り当ての一覧が表示されます。もちろんC-xだけでなく複数のキーストロークが必要な場面では自動的にメニューが出ます。

which-key-modeを有効にしてC-xを押したところ
which-key-modeを有効にしてC-xを押したところ

キーメニューというとHydraTransientを思い出しますが、これらは基本的には明示的にメニューを定義した上で使うものだと思います。自分で使いやすいようにメニューを設計できますが、逆に言えばそのような手間をかけなければなりません。一方which-key-modeは現在のキーマップから自動的にメニューを作成してくれます。

which-key-modeは事前の定義が不要な分手軽で広範囲で使用できますが、表示されるのは無味乾燥なコマンド名の羅列……と言いたいところですが、そこにはちゃんと対策が用意されています。

次の関数を使うと特定のキー割り当てに対して表示される説明を好きなように変更(置き換え)できます。

後者二つはwhich-key-replacement-alist等のwhich-key専用の変数に置き換えルールを記録しますが、興味深いのは一つ目の特定のKEYMAPに対する説明の置き換えです。which-key-add-keymap-based-replacementsの実装を見れば分かりますが、その情報は指定されたKEYMAPそれ自体に保存されます。説明(replacement)に文字列が指定された場合を追ってみると、最終的には (define-key keymap (kbd key) (cons replacement 元の割り当て)) が実行されていることが分かります。これはいったいどういうことでしょうか?

元々Emacsのキーマップというのはメニューを記述する役割を兼ねています。そのためキーマップにはメニュー項目用の文字列を埋め込めるようになっています。キーマップの書式(Format of Keymaps (GNU Emacs Lisp Reference Manual))にある item-name というがそれです。つまりwhich-key-add-keymap-based-replacementsがやっていることは実質的にはメニューを構築しているようなものです。そしてwhich-key-modeはそのメニューの項目用の文字列をコマンド名の代わりに表示してくれるというわけです。

ということはつまり、わざわざwhich-key-add-keymap-based-replacementsを使わずともキーマップに項目文字列を最初から設定しておけばwhich-key-modeのキーメニューをよりわかりやすく出来るということです。

キーマップを作成するには例えば次のようなコードがよく使われてきました(<Emacs29)。

(let ((km (make-sparse-keymap)))
  (define-key km (kbd "C-c h c") 'hello-cat)
  (define-key km (kbd "C-c h d") 'hello-dog)
  (define-key km (kbd "C-c h f") 'hello-flog)
  km)

これを次のようにするだけでwhich-key-modeのメニューをよりわかりやすくすることが出来ます。

(let ((km (make-sparse-keymap)))
  (define-key km (kbd "C-c h") (cons "Hello Animals" (make-sparse-keymap)))
  (define-key km (kbd "C-c h c") '("Cat" . hello-cat))
  (define-key km (kbd "C-c h d") '("Dog" . hello-dog))
  (define-key km (kbd "C-c h f") '("Flog" . hello-flog))
  km)

(注: 最近(>=Emacs29)ではdefine-keyはレガシー扱いとなりkeymap-setやdefine-keymapが追加されていますが、いずれにせよ項目文字列を指定する方法は用意されています)

マイナーモードならこんな感じでしょうか。

(define-minor-mode hello-animals-mode
  "Hello Animals"
  :keymap
  `((,(kbd "C-c h") . ("Hello Animals" . ,(make-sparse-keymap)))
    (,(kbd "C-c h c") . ("Cat" . hello-cat))
    (,(kbd "C-c h d") . ("Dog" . hello-dog))
    (,(kbd "C-c h f") . ("Flog" . hello-flog))))

実際に有効にして C-c h を押してみたところ、次のように表示されました。

マイナーモードのキーマップに項目名を入れて使ってみた所
マイナーモードのキーマップに項目名を入れて使ってみた所

複雑なキーマップを分かりやすくしたい、でもHydraやTransientを使うのは面倒という場合はこのような工夫をしてみてはどうでしょうか。

余談:

ちなみにキーマップをキーボードで操作できるメニューにしたいのであれば tmm-prompt を使うという方法もあります。

(defun hello-cat () (interactive) (message "Nya"))
(defun hello-dog () (interactive) (message "Wan"))
(defun hello-flog () (interactive) (message "Geko"))

(let ((km (make-sparse-keymap)))
  (define-key km (kbd "C-c") (cons "C-c" (make-sparse-keymap)))
  (define-key km (kbd "C-c h") (cons "Hello Animals" (make-sparse-keymap)))
  (define-key km (kbd "C-c h c") '("Cat" . hello-cat))
  (define-key km (kbd "C-c h d") '("Dog" . hello-dog))
  (define-key km (kbd "C-c h f") '("Flog" . hello-flog))
  (tmm-prompt km)) ;; (x-popup-menu t km))だとマウスで操作するメニューになる

HydraTransientは良くも悪くも独特の世界観を作ってしまっているところが欠点ではありますよね。で、作り込んでみてもキー操作を覚えてしまえば見なくなるわけですし。それに一つのメニューの中に沢山のコマンドを表示してしまうと探すのが大変でむしろM-xでミニバッファから補完した方が探しやすいということにもなりかねません。Magitでたまにしか使わないコマンドのキーがメニューから見つけられないことが私は良くあります。いや、MagitはそもそもGitのコマンド体系自体が(以下略