From 704099a7b90dc28575da831745da8836ef0d00f9 Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Sun, 19 Feb 2017 07:03:05 -0500 Subject: [PATCH] Add core-keybinds.el --- core/core-keybinds.el | 200 ++++++++++++++++++++++++++++++++++++++++++ core/core-lib.el | 152 -------------------------------- core/core.el | 3 +- core/packages.el | 3 + 4 files changed, 205 insertions(+), 153 deletions(-) create mode 100644 core/core-keybinds.el diff --git a/core/core-keybinds.el b/core/core-keybinds.el new file mode 100644 index 000000000..70172acf9 --- /dev/null +++ b/core/core-keybinds.el @@ -0,0 +1,200 @@ +;;; core-keybinds.el + +;; A centralized keybinds system. Uses `which-key' to preview the available keys +;; when using `doom-leader-key' or `doom-localleader-key'. + +(defvar doom-leader-key "," + "The leader prefix key, for global commands.") + +(defvar doom-localleader-key "\\" + "The leader prefix key, for global commands.") + +(defvar doom--which-key-defs '(nil)) + + +(@def-package which-key + :defer 1 + :config + (setq which-key-sort-order 'which-key-prefix-then-key-order + ;; only pop-up for leader/localleader keys + which-key-allow-regexps (list (format "^%s" (regexp-quote doom-leader-key)) + (format "^%s" (regexp-quote doom-localleader-key))) + ;; perdy unicode icons + which-key-replacement-alist + (append '((("TAB" . nil) . ("↹" . nil)) + (("RET" . nil) . ("⏎" . nil)) + (("DEL" . nil) . ("⇤" . nil)) + (("SPC" . nil) . ("␣" . nil))) + which-key-replacement-alist)) + ;; embolden local bindings + (set-face-attribute 'which-key-local-map-description-face nil :weight 'bold) + (which-key-mode +1) + (when (cdr doom--which-key-defs) + (apply 'which-key-add-key-based-replacements (cdr doom--which-key-defs)))) + + +(defun doom-keyword-to-states (keyword &optional ignore) + (let ((states '(("n" . normal) + ("v" . visual) + ("i" . insert) + ("e" . emacs) + ("o" . operator) + ("m" . motion) + ("r" . replace)))) + (delq nil + (mapcar (lambda (l) + (let ((state (cdr (assoc l states)))) + (unless state + (unless (or (eq ignore t) (member l ignore)) + (error "not a valid state: %s" l))) + state)) + (split-string (substring (symbol-name keyword) 1) "" t))))) + +;; Register keywords for proper indentation (see `@map') +(put ':prefix 'lisp-indent-function 'defun) +(put ':map 'lisp-indent-function 'defun) +(put ':map* 'lisp-indent-function 'defun) +(put ':after 'lisp-indent-function 'defun) +(put ':when 'lisp-indent-function 'defun) +(put ':unless 'lisp-indent-function 'defun) +(put ':desc 'lisp-indent-function 'defun) + +(defmacro @map (&rest rest) + "A nightmare of a key-binding macro that will use `evil-define-key*', +`define-key', `local-set-key' and `global-set-key' depending on context and +plist key flags (and whether evil is loaded or not). It was designed to make +binding multiple keys more concise, like in vim. + +If evil isn't loaded, it will ignore evil-specific bindings. + +States + :n normal + :v visual + :i insert + :e emacs + :o operator + :m motion + :r replace + :L local + + These can be combined (order doesn't matter), e.g. :nvi will apply to + normal, visual and insert mode. The state resets after the following + key=>def pair. + + If states are omitted the keybind will be global. + + :L cannot be in a :map. + +Flags + (:map [KEYMAP] [...]) ; inner keybinds are applied to KEYMAP(S) + (:map* [KEYMAP] [...]) ; same as :map, but deferred + (:prefix [PREFIX] [...]) ; assign prefix to all inner keybindings + (:after [FEATURE] [...]) ; apply keybinds when [FEATURE] loads + +Conditional keybinds + (:when [CONDITION] [...]) + (:unless [CONDITION] [...]) + +Example + (@map :map magit-mode-map + :m \"C-r\" 'do-something ; assign C-r in motion state + :nv \"q\" 'magit-mode-quit-window ; assign to 'q' in normal and visual states + \"C-x C-r\" 'a-global-keybind + + (:when IS-MAC + :n \"M-s\" 'some-fn + :i \"M-o\" (lambda (interactive) (message \"Hi\"))))" + (let ((keymaps (if (boundp 'keymaps) keymaps)) + (prefix (if (boundp 'prefix) prefix)) + (defer (if (boundp 'defer) defer)) + local key def states forms desc) + (while rest + (setq key (pop rest)) + (cond + ;; it's a sub expr + ((listp key) + (push (macroexpand `(@map ,@key)) forms)) + + ;; it's a flag + ((keywordp key) + (cond ((eq key :leader) + (push 'doom-leader-key rest) + (setq key :prefix + desc "")) + ((eq key :localleader) + (push 'doom-localleader-key rest) + (setq key :prefix + desc ""))) + (pcase key + (:when (prog1 `((if ,(pop rest) ,(macroexpand `(@map ,@rest)))) (setq rest '()))) + (:unless (prog1 `((if (not ,(pop rest)) ,(macroexpand `(@map ,@rest)))) (setq rest '()))) + (:after (prog1 `((@after ,(pop rest) ,(macroexpand `(@map ,@rest)))) (setq rest '()))) + (:desc (setq desc (pop rest))) + (:map* (setq defer t) (push :map rest)) + (:map + (setq keymaps + (let ((car (pop rest))) + (if (listp car) car (list car))))) + (:prefix + (let ((def (pop rest))) + (setq prefix + (if (or (symbolp def) (listp def)) + `(vconcat ,prefix (if (stringp ,def) (kbd ,def) ,def)) + `(vconcat ,prefix ,(if (stringp def) (kbd def) def)))) + (when desc + (push `(nconc doom--which-key-defs (list ,(key-description (eval prefix)) ,desc)) + forms) + (setq desc nil)))) + (otherwise ; might be a state prefix + (setq states (doom-keyword-to-states key '("L"))) + (when (let (case-fold-search) + (string-match-p "L" (symbol-name key))) + (setq local t) + (cond ((= (length states) 0) + (user-error "local keybinding for %s must accompany another state" key)) + ((> (length keymaps) 0) + (user-error "local keybinding for %s cannot accompany a keymap" key))))))) + + ;; It's a key-def pair + ((or (stringp key) + (characterp key) + (vectorp key)) + (unwind-protect + (catch 'skip + (when (stringp key) + (setq key (kbd key))) + (when prefix + (setq key (append prefix (list key)))) + (unless (> (length rest) 0) + (user-error "@map has no definition for %s key" key)) + (setq def (pop rest)) + (when desc + (push `(nconc doom--which-key-defs (list ,(key-description (eval key)) ,desc)) + forms)) + (cond ((and keymaps states) + (unless (featurep 'evil) (throw 'skip 'evil)) + (dolist (keymap keymaps) + (push `(,(if defer 'evil-define-key 'evil-define-key*) + ',states ,keymap ,key ,def) + forms))) + (states + (unless (featurep 'evil) (throw 'skip 'evil)) + (dolist (state states) + (push `(define-key + ,(intern (format "evil-%s-state-%smap" state (if local "local-" ""))) + ,key ,def) + forms))) + (keymaps + (dolist (keymap keymaps) + (push `(define-key ,keymap ,key ,def) forms))) + (t (push `(,(if local 'local-set-key 'global-set-key) ,key ,def) + forms)))) + (setq states '() + local nil + desc nil))) + + (t (user-error "Invalid key %s" key)))) + `(progn ,@(reverse forms)))) + +(provide 'core-keybinds) +;;; core-keybinds.el ends here diff --git a/core/core-lib.el b/core/core-lib.el index 95679d76f..6d7584fb9 100644 --- a/core/core-lib.el +++ b/core/core-lib.el @@ -135,157 +135,5 @@ Examples: (t (user-error "@associate invalid rules for mode [%s] (in %s) (match %s) (files %s)" mode in match files)))))) -;; Register keywords for proper indentation (see `@map') -(put ':prefix 'lisp-indent-function 'defun) -(put ':map 'lisp-indent-function 'defun) -(put ':after 'lisp-indent-function 'defun) -(put ':when 'lisp-indent-function 'defun) -(put ':unless 'lisp-indent-function 'defun) -(put ':leader 'lisp-indent-function 'defun) -(put ':localleader 'lisp-indent-function 'defun) - -(defmacro @map (&rest rest) - "A nightmare of a key-binding macro that will use `evil-define-key*', -`define-key', `local-set-key' and `global-set-key' depending on context and -plist key flags. It was designed to make binding multiple keys more concise, -like in vim. - -If evil isn't loaded, it will ignore evil-specific bindings. - -Yes, it tries to do too much. Yes, I only did it to make the \"frontend\" config -that little bit more concise. Yes, I could simply have used the above functions. -But it takes a little insanity to custom write your own emacs.d, so what else -were you expecting? - -States - :n normal - :v visual - :i insert - :e emacs - :o operator - :m motion - :r replace - :L local - - These can be combined (order doesn't matter), e.g. :nvi will apply to - normal, visual and insert mode. The state resets after the following - key=>def pair. - - Capitalize the state flag to make it a local binding. - - If omitted, the keybind will be defined globally. - -Flags - (:map [KEYMAP] [...]) ; inner keybinds are applied to KEYMAP - (:prefix [PREFIX] [...]) ; assign prefix to all inner keybindings - (:after [FEATURE] [...]) ; apply keybinds when [FEATURE] loads - -Conditional keybinds - (:when [CONDITION] [...]) - (:unless [CONDITION] [...]) - -Example - (@map :map magit-mode-map - :m \"C-r\" 'do-something ; assign C-r in motion state - :nv \"q\" 'magit-mode-quit-window ; assign to 'q' in normal and visual states - \"C-x C-r\" 'a-global-keybind - - (:when IS-MAC - :n \"M-s\" 'some-fn - :i \"M-o\" (lambda (interactive) (message \"Hi\"))))" - (let ((keymaps (if (boundp 'keymaps) keymaps)) - (prefix (if (boundp 'prefix) prefix)) - (state-map '(("n" . normal) - ("v" . visual) - ("i" . insert) - ("e" . emacs) - ("o" . operator) - ("m" . motion) - ("r" . replace))) - local key def states forms) - (while rest - (setq key (pop rest)) - (cond - ;; it's a sub expr - ((listp key) - (push (macroexpand `(@map ,@key)) forms)) - - ;; it's a flag - ((keywordp key) - (when (memq key '(:leader :localleader)) - (cond ((eq key :leader) - (push '+evil-leader rest)) - ((eq key :localleader) - (push '+evil-localleader rest))) - (setq key :prefix)) - (pcase key - (:ignore) - (:prefix - (let ((def (pop rest))) - (setq prefix - (if (or (symbolp def) (listp def)) - `(vconcat ,prefix (if (stringp ,def) (kbd ,def) ,def)) - `(vconcat ,prefix ,(if (stringp def) (kbd def) def)))))) - (:map (setq keymaps (-list (pop rest)))) - (:after (prog1 `((@after ,(pop rest) ,(macroexpand `(@map ,@rest)))) (setq rest '()))) - (:when (prog1 `((if ,(pop rest) ,(macroexpand `(@map ,@rest)))) (setq rest '()))) - (:unless (prog1 `((if (not ,(pop rest)) ,(macroexpand `(@map ,@rest)))) (setq rest '()))) - (otherwise ; might be a state prefix - (mapc (lambda (letter) - (cond ((assoc letter state-map) - (push (cdr (assoc letter state-map)) states)) - ((string= letter "L") - (setq local t)) - (t (user-error "Invalid mode prefix %s in key %s" letter key)))) - (split-string (substring (symbol-name key) 1) "" t)) - (unless states - (user-error "Unrecognized keyword %s" key)) - (when local - (cond ((= (length states) 0) - (user-error "local keybinding for %s must accompany another state" key)) - ((> (length keymaps) 0) - (user-error "local keybinding for %s cannot accompany a keymap" key))))))) - - ;; It's a key-def pair - ((or (stringp key) - (characterp key) - (vectorp key)) - (unwind-protect - (catch 'skip - (when (stringp key) - (setq key (kbd key))) - (when prefix - (setq key (append prefix (list key)))) - (unless (> (length rest) 0) - (user-error "Map has no definition for %s" key)) - (setq def (pop rest)) - (push (cond ((and keymaps states) - (unless (featurep 'evil) - (throw 'skip 'evil)) - `(progn - ,@(mapcar (lambda (keymap) `(evil-define-key* ',states ,keymap ,key ,def)) - keymaps))) - (keymaps - `(progn - ,@(mapcar (lambda (keymap) `(define-key ,keymap ,key ,def)) - keymaps))) - (states - (unless (featurep 'evil) - (throw 'skip 'evil)) - `(progn - ,@(mapcar (lambda (state) - `(define-key - ,(intern (format "evil-%s-state-%smap" state (if local "local-" ""))) - ,key ,def)) - states))) - (t `(,(if local 'local-set-key 'global-set-key) - ,key ,def))) - forms)) - (setq states '() - local nil))) - - (t (user-error "Invalid key %s" key)))) - `(progn ,@(reverse forms)))) - (provide 'core-lib) ;;; core-lib.el ends here diff --git a/core/core.el b/core/core.el index 1031f054d..936d29403 100644 --- a/core/core.el +++ b/core/core.el @@ -173,7 +173,8 @@ enable multiple minor modes for the same regexp.") (require 'core-ui) ; draw me like one of your French editors (require 'core-popups) ; taming sudden yet inevitable windows (require 'core-editor) ; baseline configuration for text editing - (require 'core-projects))) ; making Emacs project-aware + (require 'core-projects) ; making Emacs project-aware + (require 'core-keybinds))) ; centralized keybind system + which-key (provide 'core) ;;; core.el ends here diff --git a/core/packages.el b/core/packages.el index c797b0e4d..1636e51f5 100644 --- a/core/packages.el +++ b/core/packages.el @@ -43,3 +43,6 @@ ;; core-projects.el (@package projectile) + +;; core-keybinds.el +(@package which-key)