понедельник, 13 апреля 2015 г.

Умное назначение горячих клавиш в Emacs

Рано или поздно пользователям Emacs приходится переназначать горячие клавиши. Даже те, кто по идеологическим причинам используют дефолтные сочетания, приходят к тому, что нужно назначить на часто-употребляемую команду более простое сочетание клавиш или повесить hotkey на свой собственный скрипт. В интернетах советуют делать это как-то так:
(global-set-key (kdb "C-a") 'mark-whole-buffer)
Это какое-то время работает, пока не находится плагин, который переопределяет выбранное сочетание на что-то свое...

Происходит это по той причине, что Emacs назначает глобальным сочетаниям клавиш минимальный приоритет, выше приоритет будет у сочетаний определенных в "основном режиме" (major-mode) и самый высокий у тех, что определены во "вспомогательном режиме" (minor-mode). Так же стоит отметить, что поскольку minor-mode может быть загружен не один, то среди них приоритет будет определятся порядком загрузки.

Отсюда следует, что выходом из создавшегося положения, является создание своего собственного minor-mode и навешивание горячих клавиш уже в нем. Делается это не сложно:
(defvar cfg-mode-map (make-sparse-keymap))

(define-minor-mode cfg-mode
  "cfg-mode"
  :lighter " cfg"
  cfg-mode-map)
Тут мы при помощи макроса "define-minor-mode" определяем наш режим "cfg-mode" (по соглашению название режима должно иметь постфикс "-mode"), ключ "lighter" определяет, то, как наш режим будет отображаться в строке состояния (mode-line), а к переменной "cfg-mode-map" мы будем привязывать интересующие нас горячие клавиши, которые будут ассоциированы с нашим режимом. Так же предусмотрим две полезные функции, при помощи которых мы сможем включать и выключать наш режим для каждого буфера:
(defun turn-on-cfg-mode ()
  (interactive)
  (cfg-mode t))

(defun turn-off-cfg-mode ()
  (interactive)
  (cfg-mode -1))
И что-бы не загружать режим каждый раз отдельно, сделаем его глобальным:
(define-globalized-minor-mode global-cfg-mode cfg-mode turn-on-cfg-mode)
Тут "global-cfg-mode" это имя глобального режима, который будет автоматически включать наш режим "cfg-mode" во всех буферах.

Тут есть один нюанс, в мини буфере все же стоит оставить те горячие клавиши, которые были определены другими плагинами, иначе мы просто не сможем воспользоваться функциональностью этих плагинов. Поэтому добавляем хук, который выключит режим "cfg-mode" в минибуфере:
(add-hook 'minibuffer-setup-hook 'turn-off-cfg-mode)
Я уже выше писал, что при загрузке другого minor-mode он может переопределить наши горячие клавиши, что бы этого не произошло, нужно после загрузки каждой новой библиотеки "пододвигать" наши сочетания на самое приоритетное место. Для этого расширим системную функцию, которая отвечает за загрузку emacs-lisp библиотек и после того как она отработает, удалим и снова добавим горячие клавиши привязанные к "cfg-mode", тем самым сделав их самыми приоритетными:
(defadvice load (after cfg-keybindings-priority)
  (if (not (eq (car (car minor-mode-map-alist)) 'cfg-mode))
      (let ((mykeys (assq 'cfg-mode minor-mode-map-alist)))
        (assq-delete-all 'cfg-mode minor-mode-map-alist)
        (add-to-list 'minor-mode-map-alist mykeys))))
(ad-activate 'load)
Еще один подводный камень, это fundamental-mode. Дело в том, что некоторые плагины, в частности htmlize, который генерирует html файл с подсветкой синтаксиса, умудряются открывать буфер в fundamental-mode, без загрузки каких-либо дополнительных режимов, что естественно приводит к тому, что все определенные нами горячие клавиши - перестают работать. Для того, что бы побороть это, будем привязывать наши хоткеи одновременно к режиму cfg-mode и устанавливать их как глобальное сочетание клавиш, т.к. для fundamental-mode глобальные хоткеи останутся нетронутыми. Что бы вручную по два раза не определять одни и те же команды, можно сделать функцию, которая получая список из (hotkey/command) одновременно применяла его для минорного режима и добавляла в global key map:
(defun lcl:get-hotkeys ()
  (list
   (list "C-x u" 'undo-tree-visualize)
   ...
   (list "M-Z" 'undo-tree-redo)
   (list "M-z" 'undo-tree-undo)
   ))

(defun cfg:cfg-hotheys (map)
  (dolist (k (lcl:get-hotkeys))
    (when k
      (let ((key (kbd (car k)))
            (func (car (cdr k))))
        (define-key map key func)
        (global-set-key key func)))))

(cfg:cfg-hotheys cfg-mode-map)
Все вместе это будет выглядеть вот так:
(defvar cfg-mode-map (make-sparse-keymap))

(define-minor-mode cfg-mode
  "cfg-mode"
  :lighter " cfg"
  cfg-mode-map)

(defadvice load (after cfg-keybindings-priority)
  (if (not (eq (car (car minor-mode-map-alist)) 'cfg-mode))
      (let ((mykeys (assq 'cfg-mode minor-mode-map-alist)))
        (assq-delete-all 'cfg-mode minor-mode-map-alist)
        (add-to-list 'minor-mode-map-alist mykeys))))
(ad-activate 'load)

(defun turn-on-cfg-mode ()
  (interactive)
  (cfg-mode t))

(defun turn-off-cfg-mode ()
  (interactive)
  (cfg-mode -1))

(define-globalized-minor-mode global-cfg-mode cfg-mode turn-on-cfg-mode)

(defun lcl:get-hotkeys ()
  (list
   (list "C-x u" 'undo-tree-visualize)
   ...
   (list "M-Z" 'undo-tree-redo)
   (list "M-z" 'undo-tree-undo)))

(defun cfg:cfg-hotheys (map)
  (dolist (k (lcl:get-hotkeys))
    (when k
      (let ((key (kbd (car k)))
            (func (car (cdr k))))
        (define-key map key func)
        (global-set-key key func)))))

(defun cfg:cfg ()
  (add-hook 'minibuffer-setup-hook 'turn-off-cfg-mode)
  (cfg:cfg-hotheys cfg-mode-map)
  (global-cfg-mode))
Теперь для инициализации всего этого хозяйства достаточно вызывать функцию "cfg:cfg"

Комментариев нет:

Отправить комментарий