Dired with Evil
これは何?
Evil Advent Calendar 2014 の9日目の記事です.
8日目は mikio_kun さんによる 第3のエディタEvilのすすめ でした.
やっぱり Lisp は最高だぜ.
これを受けて(大嘘), Evil が Lisp を内蔵した Vim であることの利点を, Emacs に内蔵されているファイラ Dired
を拡張することを通して見てみたいと思います.
(素の Emacs Lisp は貧弱だけどナー.)
どこでも hjkl : magit
参考: http://d.hatena.ne.jp/tarao/20130304/evil_config
Evil を使い初めて慣れてくると, いくつかのバッファは emacs ステートになっていてhjkl移動が使えない
ことに気付くでしょう. これを避ける方法が提供されていることは知っていたのだけれど, どうにも乗り気が
しなくて放ったらかしにしていました. やっぱり小指が辛くなってきたので重い腰を上げてちゃんと設定する
ことにしたらQoLが前日比で100倍くらい上がったので, みなさんちゃんと設定しましょう, と言ってみる.
括弧はトモダチ, コワクナイヨ.
例として, 今の magit (Git の Emacs フロントエンド) の設定はこんなん:
;;; magit
(require 'magit)
(when (featurep 'evil)
;; magit-status-mode でも normal state
(setq evil-emacs-state-modes (delq 'magit-status-mode evil-emacs-state-modes))
(evil-make-overriding-map magit-mode-map 'normal) ; magit-mode-map の優先度を上げる
(evil-make-overriding-map magit-status-mode-map 'normal) ; 同上
(evil-define-key 'normal magit-mode-map
";" (lookup-key evil-motion-state-map ";")
"h" 'magit-goto-previous-section ; 右手人差し指
"t" 'magit-goto-next-section ; 右手中指
"H" 'magit-goto-previous-sibling-section ; + 親指
"T" 'magit-goto-next-sibling-section ; + 親指
"m" (lookup-key evil-normal-state-map "m") ; 右手人差し指下段, (lambda () (interactive) (scroll-down 1))
"w" (lookup-key evil-normal-state-map "w") ; 右手中指下段, (lambda () (interactive) (scroll-up 1))
"M" (lookup-key magit-mode-map "m") ; マージコマンドを退避
"-" (lookup-key magit-mode-map (kbd "TAB"))
(kbd "SPC") (lookup-key magit-mode-map (kbd "TAB"))
)
)
ね, 簡単でしょう?
hjkl が hjkl じゃないって? 僕は dvorak 配列を使っていて右手ホームポジションには dhtn があって,
なおかつ h (上) と t (下) の意味を jk と反対にしていて, かつ右手親指の位置に shift + sticky shift
を置いてる変態リマッパーなんだ. これについてはまた後日書くことにしよう.
話が逸れたね. 上の記事の中ではまとめて移動系のキーを定義するのに evil-define-key
ではなく
evil-add-hjkl-bindings
を使っているけど, これは実は evil-define-key
のラッパーでこれ自身で
複数のキーを一度に定義できる. hjkl が hjkl じゃない人や, Qwerty の場合でも jk なんかの特定のキーだけ
割り当てたい場合には上の様にするといいんじゃないかな.
こういう設定を書いたことのない人に: 函数やキーバインドの調べ方.
- 当該モードで
C-h k
(describe-key
) やhelm-descbinds
してみる. そうすると函数名がわかる. -
C-h f
(describe-function
) したりして函数が定義されているファイルに飛ぶ.
find-library
も地味にべんり. - grep やインクリメンタルサーチを使って
define-key
に使われている場所を探す.
そうするとキーマップ名がわかる. (上だとmagit-mode-map
やmagit-status-mode-map
.)
その周辺を読むと大体どういうバインディングになっているかがわかる. - あとは remap! remap!
(慣れるとソース読む方が早いし, バインド調べる系の拡張ではわからないことが結構あるので.)
本題: Dired
お次は dired. Emacs Lisp で実装されてるファイラだから設定も拡張も簡単に書けるね.
今迄これをちゃんと設定してなかったことを後悔したよ.
(eval-when-compile
(require 'erfi-macros)
(erfi:use-short-macro-name))
(require 'f)
(evil-make-overriding-map dired-mode-map 'normal)
(evil-define-key 'normal dired-mode-map
";" (lookup-key evil-motion-state-map ";")
"h" 'dired-previous-line ; 人差し指
"t" 'dired-next-line ; 中指
"d" 'dired-up-directory ; 人差し指の左
"n" 'keu-dired-down-directory ; 薬指
"m" (lookup-key evil-normal-state-map "m")
"w" (lookup-key evil-normal-state-map "w")
(kbd "SPC") (lookup-key dired-mode-map "m")
(kbd "S-SPC") (lookup-key dired-mode-map "d")
)
(defun keu-dired-down-directory ()
"[Dired command] Go down to the directory."
(interactive)
(condition-case err
(let1 path (dired-get-file-for-visit)
(if (f-directory? path)
(dired-find-file)
(message "This is not directory!")))
(error (message "%s" (cadr err)))))
上下でファイルを選ぶ, 左でディレクトリを上がる, 右で下がる. 素晴しいね!!
デフォルトだと上に行くには ^
を押さなきゃいけないからすごく遅かったんだ.
(f.el
の依存は f-directory?
の代わりに file-directory-p
を使えば外せる.
ERFI は let1
に使ってるだけだから let
を使えばいい.)
f.el
: https://github.com/rejeep/f.el
ERFI: https://github.com/kenoss/erfi
でもこのインターフェース, 右を押したときにディレクトリじゃなかったら小言を言うだけだから
少し勿体無いね. o
(dired-find-file-other-window
) を使ってもいいけど, どうせなら
ファイルのときは内容を上下スクロールで覗けるようにして, 左を押したら元の状態に戻す, 右を押したら
覗いたファイルを dired-find-file-other-window
で開いたのと同じ様な状態になるようにしよう.
貼るにはちと長いけど, 全体像はこんな感じだ:
;; -*- lexical-binding: t -*- ファイルの先頭に書くこと.
(eval-when-compile
(require 'cl)
(require 'erfi-macros)
(erfi:use-short-macro-name))
(require 'f)
;;; Turn on highlight line.
(add-hook 'dired-mode-hook 'my-turn-on-hl-line-mode)
;;;
;;; Evil integration
;;;
(evil-make-overriding-map dired-mode-map 'normal)
(evil-define-key 'normal dired-mode-map
";" (lookup-key evil-motion-state-map ";")
"h" 'dired-previous-line
"t" 'dired-next-line
"d" 'dired-up-directory
"n" 'keu-dired-down-directory-or-peep-file
"m" (lookup-key evil-normal-state-map "m")
"w" (lookup-key evil-normal-state-map "w")
(kbd "SPC") (lookup-key dired-mode-map "m")
(kbd "S-SPC") (lookup-key dired-mode-map "d")
)
(defun keu-dired-down-directory-or-peep-file ()
"[Dired command] Go down to the directory or peep file.
If current line is a file, call `keu-dired:peep-file'.
Key binding is described by message."
(interactive)
(condition-case err
(let1 path (dired-get-file-for-visit)
(if (f-directory? path)
(dired-find-file)
(keu-dired:peep-file path)))
(error (message "%s" (cadr err)))))
(let ((key-bindings '(("h" scroll-down)
("t" scroll-up)))
(quit-key "d")
(open-key "n")
(msg "h/t: scroll-down/up, d: quit peeping, n: open file")
(before-peep (lambda () (hl-line-mode -1)))
(after-quit 'hl-line-mode)
(before-open 'hl-line-mode)
(after-open 'evil-normal-state))
(defun keu-dired:peep-file (path)
(let1 win-config (current-window-configuration)
(delete-other-windows)
(funcall before-peep)
(let* ((path (file-truename path))
(newly-opened? (not (loop for buf in (buffer-list)
thereis (string= path (buffer-file-name buf)))))
(window (split-window-right))
(continue-symbols nil)
(map (rlet1 map (make-sparse-keymap)
(dolist (key+command key-bindings)
(destructuring-bind (key command) key+command
(let1 sym (make-symbol "temp-scroll-command")
;; Define commands to distinguish them and to determine whether continue.
(push sym continue-symbols)
(fset sym (lambda () (interactive)
(with-selected-window window (call-interactively command))))
(define-key map key sym))))
(define-key map quit-key (lambda () (interactive)
(progn
(if newly-opened?
(progn
(kill-buffer (window-buffer window))
(message "Quit peep. Buffer killed."))
(message "Quit peep. Buffer still alive."))
(funcall after-quit)
(set-window-configuration win-config))))
(define-key map open-key (lambda () (interactive)
(progn
(funcall before-open)
(select-window window)
(funcall after-open)))))))
(with-selected-window window (find-file path))
(message msg)
(set-temporary-overlay-map map (lambda ()
(rlet1 cont (memq this-command continue-symbols)
(when cont (message msg))))))))
)
いくつか注意を. これは lexical-binding
が t
じゃないと動かない. コピペしてセーブして
revert-buffer
してから eval すれば大丈夫なはずだ.
(let
を全部 lexical-let
にするという手もあるけど, 最早時代遅れだ.)
ERFI への依存はさっきと同様に let1
と rlet1
だけだから let
を使って書き直していけば外せる.
Qwerty な人は keu-dired:peep-file
の直前の let
で束縛してる文字を適宜変えれば幸せになれる.
ああ, 大事なことを忘れていた. これは Emacs 24.3 あたりで登場した set-temporary-overlay-map
を必要
とする. 最近の Helm も使ってるから入れてる人も多いと思うけど, 古い version の Emacs を使ってる人は
この下の最後の方にあるコードをコピペして使えばいい.
keu-dired:peep-file
がなんで上手く動くかは...わかる人はわかるし知らない人にはちょっと難しいから
詳しい説明はいいかな? 核心は set-temporary-overlay-map
を使ってることで, 細かいコントロール
のために set-temporary-overlay-map
と似たようなことをやっている. set-temporary-overlay-map
めっちょべんり. 最近はこれを使って連続コマンドを定義するのがマイブーム.
あとはまぁ, クロージャ便利だよねクロージャ.
;; Compatibility for emacs < 24.3
(when (< (+ emacs-major-version (* 0.1 emacs-minor-version)) 24.3)
(defalias 'kbd 'read-kbd-macro)
;; Copied from Emacs 24.3.1
(defun set-temporary-overlay-map (map &optional keep-pred)
"Set MAP as a temporary keymap taking precedence over most other keymaps.
Note that this does NOT take precedence over the \"overriding\" maps
`overriding-terminal-local-map' and `overriding-local-map' (or the
`keymap' text property). Unlike those maps, if no match for a key is
found in MAP, the normal key lookup sequence then continues.
Normally, MAP is used only once. If the optional argument
KEEP-PRED is t, MAP stays active if a key from MAP is used.
KEEP-PRED can also be a function of no arguments: if it returns
non-nil then MAP stays active."
(let* ((clearfunsym (make-symbol "clear-temporary-overlay-map"))
(overlaysym (make-symbol "t"))
(alist (list (cons overlaysym map)))
(clearfun
;; FIXME: Use lexical-binding.
`(lambda ()
(unless ,(cond ((null keep-pred) nil)
((eq t keep-pred)
`(eq this-command
(lookup-key ',map
(this-command-keys-vector))))
(t `(funcall ',keep-pred)))
(set ',overlaysym nil) ;Just in case.
(remove-hook 'pre-command-hook ',clearfunsym)
(setq emulation-mode-map-alists
(delq ',alist emulation-mode-map-alists))))))
(set overlaysym overlaysym)
(fset clearfunsym clearfun)
(add-hook 'pre-command-hook clearfunsym)
;; FIXME: That's the keymaps with highest precedence, except for
;; the `keymap' text-property ;-(
(push alist emulation-mode-map-alists)))
)
終わりに: Everywhere vim-keybinding i love
久々に拡張性とかあんまり考えずにテキトーに書いた短いコードでQoLが上ったのでハッピーです.
ここで書いたような volatile application を誰でも簡単に書けるように command-chain.el
(GitHub) を改良する予定です.
皆さんもどんどん独自コマンドを定義して小指を労ってあげてください. ついでに Lisp 人口増えてください.
Keno, Emacs utilizer.