Emacs 읽기 전용 버퍼에서 모드 키 조합 없이 이동하기
Emacs를 쓰게 되면서 한 가지 안 좋아진 점은, 왼쪽 새끼 손가락 쪽 인대의 피로도가 예전에 비해 꽤나 많이 높아 졌다는 것이다. 보통 이런 증상을 Repetitive strain injury(RSI), 혹은 좀 더 specific하게는 Emacs pinky라고 하던데, Emacs 쓰기 이전 vi나 source insight를 사용하던 때는 싸구려 멤브레인 키보드로도 그닥 장시간 작업 해도 피로감이 없었으나, Emacs를 쓰고 난 이후부터 증상이 슬슬 나타나기 시작했다. 나름대로는 이 증상을 완화시켜보겠다고 Ctrl 키를 Caps Lock 위치로 바꾼다거나 비싼 키보드를 지른다거나 했지만 (물론 이런 노력을 하지 않던 예전에 비해 확실히 덜하긴 하지만) 여전히 퇴근할 때 쯤에는 왼팔꿈치 근처나 새끼 손가락쪽 인대가 묵직하게 저려 오는 느낌이 있다.
그래서 최대한 왼쪽 새끼손가락으로 모드 키, 즉 Ctrl Alt Shift를 누르는 회수를 줄이려는 노력을 계속 하고 있는데, 이것도 그 중의 한 가지 일환이다. 아래의 소스에서 보듯 my:read-only-mode 마이너 모드는, buffer-read-only가 non-nil일 때, 즉 버퍼가 읽기 전용 모드일 때 모드 키 없이 일반 문자로 커서 이동을 할 수 있게 해 준다. 마치 read-only 상태를 vi에서의 modal 상태처럼 다룬다고 보면 될 것 같다.
그러나 특정 메이저/마이너 모드에서는 특정 버퍼를 읽기 전용으로 띄우고 모드 키 없는 바인딩을 자체적으로 추가하기도 하는데, 보통 이 경우는 단순히 커서 이동 뿐만 아니라 이동 된 위치의 아이템에 대한 상세 정보를 다른 윈도우에 띄우기도 한다. 가령 magit에서 n이나 p 키는 라인이 아니라 섹션 단위로 이동하는 역할을 하고 때에 따라선 옮겨진 위치의 정보를 다른 윈도우에 표시하는 등 부가적인 기능을 수행하기도 하는데, 이와 같이 해당 키에 기존에 정의된 바인딩이 있다면 해당 내용을 단순한 커서 이동으로 덮어 쓰지 않도록 해 줄 필요가 있다.
(defconst my:read-only-keybind-alist '( ("p" . previous-line) ("n" . next-line) ("f" . forward-char) ("b" . backward-char) ("a" . beginning-of-line) ("e" . end-of-line) ;; vi-like ("h" . (lambda () (interactive) (if (bolp) (error "Beginning of line") (backward-char)))) ("l" . (lambda () (interactive) (if (eolp) (error "End of line") (forward-char)))) ("j" . next-line) ("k" . previous-line) ;; page scroll ("SPC" . my:scroll-up-command) ("u" . my:scroll-down-command) ) "key binding for read-only buffer") (defvar my:read-only-overridden-keys nil) (make-variable-buffer-local 'my:read-only-overridden-keys) (defface my:read-only-face '((t :foreground "yellow" :background "red")) "Used for readonly lighter" :group 'basic-faces) (defvar my:read-only-mode-lighter (list " " (propertize "Readonly" 'face `my:read-only-face))) (put 'my:read-only-mode-lighter 'risky-local-variable t) (define-minor-mode my:read-only-mode "Control-key-less keybind for read-only buffer." :lighter my:read-only-mode-lighter :variable buffer-read-only (let ((overwritten-keys my:read-only-overridden-keys)) (if buffer-read-only (progn (dolist (keybind my:read-only-keybind-alist) (unless (and (current-local-map) (lookup-key (current-local-map) (kbd (car keybind)))) (add-to-list 'overwritten-keys (car keybind)) (buffer-local-set-key (kbd (car keybind)) (cdr keybind)))) (setq my:read-only-overridden-keys overwritten-keys)) (dolist (key overwritten-keys) (buffer-local-set-key (kbd key) nil)) (setq my:read-only-overridden-keys nil))) (read-only-mode (if buffer-read-only 1 -1)))
buffer-local-set-key 함수는 EmacsWiki에서 참고한 것으로, Emacs 자체(혹은 최소한 GNU Emacs)에서 버퍼별로 keymap을 할당할 수 있는 방법이 없기 때문에 해당 버퍼 전용 minor mode를 만들어서 거기에다 키를 바인딩한다.
;; https://www.emacswiki.org/emacs/BufferLocalKeys (defun buffer-local-set-key (key func) (interactive "KSet key on this buffer: \naCommand: ") (let ((name (format "%s-magic" (buffer-name)))) (eval `(define-minor-mode ,(intern name) "Automagically built minor mode to define buffer-local keys.")) (let* ((mapname (format "%s-map" name)) (map (intern mapname))) (unless (boundp (intern mapname)) (set map (make-sparse-keymap))) (eval `(define-key ,map ,key func))) (funcall (intern name) t)))
위와 같이 정의했다면 my:read-only-mode을 원래 read-only-mode의 키인 C-x C-q에 지정하자. 내 경우는 왼쪽 모드키를 덜 누르는 게 지상과제이니 만큼 <f12>에도 바인딩해 주었다.
(global-set-key (kbd "C-x C-q") 'my:read-only-mode) ; override default key (global-set-key (kbd "<f12>") 'my:read-only-mode)
이제 C-x C-q이나 <f12>을 눌러 버퍼를 읽기 전용으로 세팅하면 모드 키를 누르지 않고 이동을 할 수 있게 되지만, 기본적으로 읽기 전용 모드인 버퍼, 특히 *Help* 버퍼처럼 모드 키 없이 커서 이동이 가능한 바인딩이 되어 있지 않은 버퍼들에 대해서도 읽다보면 라인이나 페이지 이동을 자주 쓸 일이 있으므로 위 키 바인딩을 적용할 필요가 있다.
;; add my:read-only-mode when the buffer-read-only is set (add-hook 'buffer-list-update-hook (lambda () (with-current-buffer (current-buffer) (when (and buffer-read-only (not (member 'my:read-only-mode minor-mode-list))) (my:read-only-mode 1)))))
buffer-list-update-hook에 hook을 추가하여 버퍼가 읽기 전용일 때, 즉 buffer-read-only 버퍼 로컬 변수가 non-nil일 때, my:read-only-mode가 적용이 되어 있지 않다면 적용하도록 해 주었다. buffer-list-update-hook은 GNU Emacs Lisp Manual에서 아래와 같이 설명하듯,
This is a normal hook run whenever the buffer list changes. Functions (implicitly) running this hook are버퍼 리스트의 순서나 element가 추가/삭제 될 경우 호출 되므로 이 시점에 my:read-only-mode를 적용하도록 하는 것이 가장 이상적으로 보였다.get-buffer-create
(see Creating Buffers),rename-buffer
(see Buffer Names),kill-buffer
(see Killing Buffers),bury-buffer
(see above) andselect-window
(see Selecting Windows).
그러나 이렇게 적용하고 난 후 magit-popup-mode에서는 키가 바인딩 되는 시점의 문제인지 오히려 my:read-only-mode 의 키 바인딩 때문에 기존 magit popup의 키가 제대로 적용되지 않는 문제가 생겼다. 대충 봐선 타이밍 이슈로 보이긴 하는 데 더 이상 깊게 들여다보고 싶지도 않고, 사실 magit popup에서는 커서 이동이 거의 필요가 없으므로, (차후에 또 어떤 메이저 모드에서 말썽일지 모르므로 준비 차원에서) my:read-only-mode를 적용하지 않을 blacklist를 작성하는 식으로 처리했다. 아래는 위의 hook을 수정한 결과이다.
(defconst my:read-only-mode-blacklist '( magit-popup-mode ) "The blacklist which can not run well with my:read-only-mode.") ;; add my:read-only-mode when the buffer-read-only is set (add-hook 'buffer-list-update-hook (lambda () (with-current-buffer (current-buffer) (when (and buffer-read-only (not (member major-mode my:read-only-mode-blacklist)) (not (member 'my:read-only-mode minor-mode-list))) (my:read-only-mode 1)))))
코드를 작성할 때야 어쩔 수 없어도, 읽을 때는 읽기 전용 모드 켜 놓고 모드 키 없이 이동하니 한결 손이 편해지는 느낌이다. 사실 일하면서 코드 작성하는 것 보다 읽는 시간이 훨씬 많은 것 같은 데 그 때마다 라인이나 페이지 이동할 때마다 모드 키 입력이 필요하던 걸 이런 식으로 줄여 놓으니 확실히 차이가 보이는 듯. 사실 누가 보면, 이럴거면 viper-mode나 evil-mode 쓰지 왜 그러냐 하겠지만 기본적인 이동 키 외의 vi 키는 고작 1년 안썼다고 익숙치가 않기도 하고 다시 vi 바인딩으로 돌아가는 건 초가 삼간 태우러 가는 일을 하게 될 것 같은 기분이 들기도 해서 그다지 끌리지 않는다.
'Emacs' 카테고리의 다른 글
ido에서 helm으로 넘어오다 (0) | 2016.05.17 |
---|---|
Emacs에서 End of Line 타입 변경을 간편하게! (0) | 2016.05.11 |
Emacs의 자동 저장 및 복구 기능을 좀 더 잘 활용하자 (0) | 2016.04.04 |
Emacs의 버퍼 변경 알림 프롬프트에서 변경 내용 표시하기 (0) | 2016.04.01 |
Emacs spell checker (0) | 2016.03.11 |