Hydra のススメ

みなさん、 Emacs 使ってますか?先日 Emacs 26.1 がリリースされて盛り上がってますね。なので今回は Emacs、 Emacs といっても Hydra の話を少し書いてみたいと思います。
Hydra はとても便利なのですが、あまり Hydra 自体紹介されてないようです。
Hydra の提供する機能は Emacs 関係なく他のエディタでも見習うべきだと思います。


Emacs のキーバインド問題

Emacs をそれなりに使い始めてると様々なソースを Emacs で編集したくなることでしょう。そうすると mode 毎にショートカットを覚えて使い込んで行くことになるのですが、いろいろなソースを編集しているとキーバインドが覚えられなくなってきます。
C-x, C-c などの prefix は?、その後のキーは?結構あやふやになってきます。もちろん mode 毎にキーバインドは設定できるのでそこでなるべく同じような機能を割り当てる設定をすることで大分覚えやすくはなります。ですが、prefix の割当も含めどうするかなど設定を考えること大変だし、そもそも機能が多すぎるとキーバインドがぶつかりがちです。

* 共通で使うキーバインド
* mode 毎のキーバインド

多くの場合は上のような設定になりますが、キーバインドの数が多く覚えることは困難です。また Emacs は C-c などの同時押しコンビネーションで様々な機能のショートカットを割り当てる文化です。なので、押すキー自体の位置関係も含め考えなければいけません、同時押ししにくい場合には逆に効率がさがってしまう場合があります。そしてこれは変則キーボード使い、変則キーマップ使いにはとても重要なことです。私のように 40%、あるいはそれ以下のキー数のキーボードを使ってるミニマリストにはメタキーとの同時は非常に困難です。(メタキーが別レイヤに押し込まれてしまうキーマップなど)

そこで Hydra の出番です。

Hydra を使うとよく使う機能をまとめてシンプルなキーバインドを割り当てることができます。私は MiniDox(変則 40%キーボード)を使うことが有りますが、Hydra があるおかげで苦にならずに Emacs が使えています。


Hydra を使う

Hydra の使い方を具体的に見てましょう。Hydra は defhydra マクロで呼び出すキーバインドや関数に割り当てるキーなどを設定するだけで使えます。

(defhydra hydra-zoom (global-map “<f2>”)
“zoom”
(“g” text-scale-increase “in”)
(“l” text-scale-decrease “out”))

上は Hydra を使った zoom in out の設定です。
<f2> g でズームインします。l でズームアウトします。Hydra は繰り返しも解釈するので 5 回ズームする場合には <f2> で zoom を呼び出して 5g と入力するだけでいけます。これで特定の関数を複数回呼ぶ際に同じ Prefix を何度も押すという無駄な作業から開放されますね。この辺の機能は Smartrep という拡張に近いかも知れません。

また Hydra の素晴らしさはこれだけではありません。UI、メニューというべき見た目の部分をカスタマイズできる点です。複雑なメニューでも簡単に見た目のよいメニューが構築できるのが Hydra の良さです。

UI でどのキーがどの関数に割当られているのがわかればとても安心ですね。


Hydra の実例

私が使っている Hydra のメイン部分は以下のようになっています。
Hydra で割り当ててるのは主に以下の機能です。

* elisp で評価用の eval-buffer
* keep binding を確認するための describe-key
* package のアップデートを行うための paradox
* メモ用の temp buffer の作成
* helm-ag 系コマンド

C-z を押すとメニューが立ち上がりそこからワンキーで必要なキーにアクセスします。なので任意の関数を C-z + X で実行でき、C-c C-c x のような複雑なキーバインドを覚えなくても済非常に楽です。

(defun generate-buffer ()
(interactive)
(switch-to-buffer (make-temp-name “temp-”)))
(defun _exit ()
(interactive)
(if (display-graphic-p)
(delete-frame)
(exit)))
(use-package hydra
:ensure t
:defer t
:commands
(hydra-main/body)
:bind
((“C-z” . hydra-main/body)
(“C-c c” . hydra-main/body)
(“C-c h” . hydra-highlight-symbol/body))
:config
(defhydra hydra-main (:hint nil :exit t)

^Main^ ^Helm^ ^Other^
^^^^^^ — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
_e_: eval-buffer _a_: helm-do-ag _r_: anzu-query-replace
_d_: describe-key _A_: helm-do-ag-project-root _f_: make-frame-command
_g_: magit-status _l_: helm-ls-git-ls
_p_: paradox-list-packages _i_: helm-imenu
_b_: generate-buffer _m_: helm-mini
_q_: exit

(“e” eval-buffer)
(“d” describe-key)
(“g” magit-status)
(“P” prodigy)
(“p” paradox-list-packages)
(“b” generate-buffer)

(“a” helm-do-ag)
(“A” helm-do-ag-project-root)
(“l” helm-ls-git-ls)
(“i” helm-imenu)
(“m” helm-mini)

(“r” anzu-query-replace)
(“f” make-frame-command)

(“q” _exit)
(“z” nil “leave”))
C-zを押すとこのようなメニューが出る

その他にも highlight-mode で移動するためのものも定義しています。

(defhydra hydra-highlight-symbol
(:hint nil
:post (progn
(highlight-symbol-remove-all)))
“highlight-symbol”
(“.” highlight-symbol-at-point “highlight”)
(“n” highlight-symbol-next “next”)
(“p” highlight-symbol-prev “prev”)
(“N” highlight-symbol-next-in-defun “next in defun”)
(“P” highlight-symbol-prev-in-defun “prev in defun”)
(“z” nil “leave”)))

java の場合は C-z で以下のような割当をします。

(use-package meghanada
:defer t
:init
(add-hook ‘java-mode-hook
(lambda ()
(google-set-c-style)
(google-make-newline-indent)
(meghanada-mode t)
(dumb-jump-mode t)
(smartparens-mode t)
(rainbow-delimiters-mode t)
(highlight-symbol-mode t)
(add-hook ‘before-save-hook
(lambda ()
(meghanada-code-beautify-before-save)))))
:config
(use-package realgud
:ensure t)
(setq indent-tabs-mode nil)
(setq tab-width 2)
(setq c-basic-offset 2)
(setq meghanada-server-remote-debug t)
(setq meghanada-javac-xlint “-Xlint:all,-processing”)
(setq meghanada-server-jvm-option “-Xms128m -Xmx1024m -XX:+UseConcMarkSweepGC -XX:ReservedCodeCacheSize=240m -XX:SoftRefLRUPolicyMSPerMB=50 -Dsun.io.useCanonCaches=false”)
(setq meghanada-import-static-enable “java.util.Objects,org.junit.Assert”)
(setq meghanada-cache-in-project nil)
:bind
(:map meghanada-mode-map
(“C-S-t” . meghanada-switch-testcase)
(“C-M-.” . helm-imenu)
(“M-r” . meghanada-reference)
(“M-t” . meghanada-typeinfo)
(“C-z” . hydra-meghanada/body))
:commands
(meghanada-mode))

(defun _exit ()
(interactive)
(if (display-graphic-p)
(delete-frame)
(exit)))
(defhydra hydra-meghanada (:hint nil :exit t)

^Main^
^^^^^^ — — — — — — — — — — — — — — — — — — — — — — — — — — — -
_c_: meghanada-compile-file _m_: meghanada-restart
_C_: meghanada-compile-project _t_: meghanada-run-task
_o_: meghanada-optimize-import _j_: meghanada-run-junit-test-case
_s_: meghanada-switch-test-case _J_: meghanada-run-junit-class
_v_: meghanada-local-variable _R_: meghanada-run-junit-recent
_i_: meghanada-import-at-point _r_: meghanada-reference
_I_: meghanada-import-all _T_: meghanada-typeinfo
_p_: meghanada-show-project
_g_: magit-status
_a_: helm-do-ag
_A_: helm-do-ag-project-root
_l_: helm-ls-git-ls
_q_: exit

(“c” meghanada-compile-file)
(“C” meghanada-compile-project)
(“m” meghanada-restart)
(“o” meghanada-optimize-import)
(“s” meghanada-switch-test-case)
(“v” meghanada-local-variable)
(“i” meghanada-import-at-point)
(“I” meghanada-import-all)
(“p” meghanada-show-project)

(“g” magit-status)
(“a” helm-do-ag)
(“A” helm-do-ag-project-root)
(“l” helm-ls-git-ls)
(“I” helm-imenu)
(“u” helm-mini)
(“U” helm-resume)

(“t” meghanada-run-task)
(“T” meghanada-typeinfo)
(“j” meghanada-run-junit-test-case)
(“J” meghanada-run-junit-class)
(“r” meghanada-reference)
(“R” meghanada-run-junit-recent)

(“q” _exit)
(“z” nil “leave”))
Javaの場合はこうなる

カーソル移動モードを定義するのもいいかも知れません。

(defhydra hydra-move (:color pink :hint nil)

Navigation Scroll
— — — — — — — — — — — — — — — — -
^^^^_p_ ^_v_
^^^^↑ ^^^^↑
_B_ _b_ ← → _f_ _F_
^^^^↓ ^^^^↓
^^^^_n_ ^_V_

(“n” new-next-line “next-line”)
(“p” new-previous-line “previous-line”)
(“f” forward-char”forward-char”)
(“b” backward-char “backward-char”)
(“F” forward-word “forward-word”)
(“B” backward-word “backward-word”)
(“a” beginning-of-visual-indented-line “beginning-of-line”)
(“e” end-of-visual-line “move-end-of-line”)
(“v” scroll-up-command “scroll-up-command”)
(“V” scroll-down-command “scroll-down-command”)
(“l” recenter-top-bottom “recenter-top-bottom”)
(“q” nil “leave”)
(“z” nil “leave”))
(global-set-key (kbd “C-n”) ‘hydra-move/body)

Hydra はアイディア次第でいろいろ応用できると思います。最初のうちはなかなか難しいかも知れません。最初はうちは有志たちが公開している設定を参考にすると良いと思います。

というわけで今回は Hydra を簡単に紹介してみました。

あと最後にお知らせですが弊社では社員を募集しています。興味のある方は連絡お待ちしております。