;;; doom-keybinds.el --- defaults for Doom's keybinds -*- lexical-binding: t; -*- ;;; Commentary: ;; ;; A centralized keybinds system, integrated with `which-key' to preview ;; available keybindings. All built into one powerful macro: `map!'. If evil is ;; never loaded, then evil bindings set with `map!' are ignored (i.e. omitted ;; entirely for performance reasons). ;; ;;; Code: (defvar doom-leader-key "SPC" "The leader prefix key for Evil users.") (defvar doom-leader-alt-key "M-SPC" "An alternative leader prefix key, used for Insert and Emacs states, and for non-evil users.") (defvar doom-localleader-key "SPC m" "The localleader prefix key, for major-mode specific commands.") (defvar doom-localleader-alt-key "M-SPC m" "The localleader prefix key, for major-mode specific commands. Used for Insert and Emacs states, and for non-evil users.") (defvar doom-leader-map (make-sparse-keymap) "An overriding keymap for keys.") ;; ;;; Global keybind settings (cond (IS-MAC ;; mac-* variables are used by the special emacs-mac build of Emacs by ;; Yamamoto Mitsuharu, while other builds use ns-*. (setq mac-command-modifier 'super ns-command-modifier 'super mac-option-modifier 'meta ns-option-modifier 'meta ;; Free up the right option for character composition mac-right-option-modifier 'none ns-right-option-modifier 'none)) (IS-WINDOWS (setq w32-lwindow-modifier 'super w32-rwindow-modifier 'super))) ;; HACK: Emacs cannot distinguish between C-i from TAB. This is largely a ;; byproduct of its history in the terminal, which can't distinguish them ;; either, however, when GUIs came about Emacs greated separate input events ;; for more contentious keys like TAB and RET. Therefore [return] != RET, ;; [tab] != TAB, and [backspace] != DEL. ;; ;; In the same vein, this keybind adds a [C-i] event, so users can bind to it. ;; Otherwise, it falls back to regular C-i keybinds. (define-key key-translation-map [?\C-i] (cmd! (if (let ((keys (this-single-command-raw-keys))) (and keys (not (cl-position 'tab keys)) (not (cl-position 'kp-tab keys)) (display-graphic-p) ;; Fall back if no keybind can be found, otherwise ;; we've broken all pre-existing C-i keybinds. (let ((key (doom-lookup-key (vconcat (cl-subseq keys 0 -1) [C-i])))) (not (or (numberp key) (null key)))))) [C-i] [?\C-i]))) ;; ;;; Universal, non-nuclear escape ;; `keyboard-quit' is too much of a nuclear option. I wanted an ESC/C-g to ;; do-what-I-mean. It serves four purposes (in order): ;; ;; 1. Quit active states; e.g. highlights, searches, snippets, iedit, ;; multiple-cursors, recording macros, etc. ;; 2. Close popup windows remotely (if it is allowed to) ;; 3. Refresh buffer indicators, like git-gutter and flycheck ;; 4. Or fall back to `keyboard-quit' ;; ;; And it should do these things incrementally, rather than all at once. And it ;; shouldn't interfere with recording macros or the minibuffer. This may require ;; you press ESC/C-g two or three times on some occasions to reach ;; `keyboard-quit', but this is much more intuitive. (defvar doom-escape-hook nil "A hook run when C-g is pressed (or ESC in normal mode, for evil users). More specifically, when `doom/escape' is pressed. If any hook returns non-nil, all hooks after it are ignored.") (defun doom/escape (&optional interactive) "Run `doom-escape-hook'." (interactive (list 'interactive)) (let ((inhibit-quit t)) (cond ((minibuffer-window-active-p (minibuffer-window)) ;; quit the minibuffer if open. (when interactive (setq this-command 'abort-recursive-edit)) (abort-recursive-edit)) ;; Run all escape hooks. If any returns non-nil, then stop there. ((run-hook-with-args-until-success 'doom-escape-hook)) ;; don't abort macros ((or defining-kbd-macro executing-kbd-macro) nil) ;; Back to the default ((unwind-protect (keyboard-quit) (when interactive (setq this-command 'keyboard-quit))))))) (global-set-key [remap keyboard-quit] #'doom/escape) (with-eval-after-load 'eldoc (eldoc-add-command 'doom/escape)) ;; ;;; General + leader/localleader keys (require 'general) ;; Convenience aliases (defalias 'define-key! #'general-def) (defalias 'undefine-key! #'general-unbind) ;; Prevent "X starts with non-prefix key Y" errors except at startup. (add-hook 'doom-after-modules-init-hook #'general-auto-unbind-keys) ;; HACK: `map!' uses this instead of `define-leader-key!' because it consumes ;; 20-30% more startup time, so we reimplement it ourselves. (defmacro doom--define-leader-key (&rest keys) (let (prefix forms wkforms) (while keys (let ((key (pop keys)) (def (pop keys))) (if (keywordp key) (when (memq key '(:prefix :infix)) (setq prefix def)) (when prefix (setq key `(general--concat t ,prefix ,key))) (let* ((udef (cdr-safe (doom-unquote def))) (bdef (if (general--extended-def-p udef) (general--extract-def (general--normalize-extended-def udef)) def))) (unless (eq bdef :ignore) (push `(define-key doom-leader-map (general--kbd ,key) ,bdef) forms)) (when-let (desc (cadr (memq :which-key udef))) (prependq! wkforms `((which-key-add-key-based-replacements (general--concat t doom-leader-alt-key ,key) ,desc) (which-key-add-key-based-replacements (general--concat t doom-leader-key ,key) ,desc)))))))) (macroexp-progn (append (and wkforms `((after! which-key ,@(nreverse wkforms)))) (nreverse forms))))) (defmacro define-leader-key! (&rest args) "Define keys. Uses `general-define-key' under the hood, but does not support :states, :wk-full-keys or :keymaps. Use `map!' for a more convenient interface. See `doom-leader-key' and `doom-leader-alt-key' to change the leader prefix." `(general-define-key :states nil :wk-full-keys nil :keymaps 'doom-leader-map ,@args)) (defmacro define-localleader-key! (&rest args) "Define key. Uses `general-define-key' under the hood, but does not support :major-modes, :states, :prefix or :non-normal-prefix. Use `map!' for a more convenient interface. See `doom-localleader-key' and `doom-localleader-alt-key' to change the localleader prefix." (if (modulep! :editor evil) ;; :non-normal-prefix doesn't apply to non-evil sessions (only evil's ;; emacs state) `(general-define-key :states '(normal visual motion emacs insert) :major-modes t :prefix doom-localleader-key :non-normal-prefix doom-localleader-alt-key ,@args) `(general-define-key :major-modes t :prefix doom-localleader-alt-key ,@args))) ;; PERF: We use a prefix commands instead of general's ;; :prefix/:non-normal-prefix properties because general is incredibly slow ;; binding keys en mass with them in conjunction with :states -- an effective ;; doubling of Doom's startup time! (define-prefix-command 'doom/leader 'doom-leader-map) (define-key doom-leader-map [override-state] 'all) ;; Bind `doom-leader-key' and `doom-leader-alt-key' as late as possible to give ;; the user a chance to modify them. (add-hook! 'doom-after-init-hook (defun doom-init-leader-keys-h () "Bind `doom-leader-key' and `doom-leader-alt-key'." (let ((map general-override-mode-map)) (if (not (featurep 'evil)) (progn (cond ((equal doom-leader-alt-key "C-c") (set-keymap-parent doom-leader-map mode-specific-map)) ((equal doom-leader-alt-key "C-x") (set-keymap-parent doom-leader-map ctl-x-map))) (define-key map (kbd doom-leader-alt-key) 'doom/leader)) (evil-define-key* '(normal visual motion) map (kbd doom-leader-key) 'doom/leader) (evil-define-key* '(emacs insert) map (kbd doom-leader-alt-key) 'doom/leader)) (general-override-mode +1)))) ;; ;;; Packages (use-package! which-key :hook (doom-first-input . which-key-mode) :init (setq which-key-sort-order #'which-key-key-order-alpha which-key-sort-uppercase-first nil which-key-add-column-padding 1 which-key-max-display-columns nil which-key-min-display-lines 6 which-key-side-window-slot -10) :config (put 'which-key-replacement-alist 'initial-value which-key-replacement-alist) (add-hook! 'doom-before-reload-hook (defun doom-reset-which-key-replacements-h () (setq which-key-replacement-alist (get 'which-key-replacement-alist 'initial-value)))) ;; general improvements to which-key readability (which-key-setup-side-window-bottom) (setq-hook! 'which-key-init-buffer-hook line-spacing 3) (which-key-add-key-based-replacements doom-leader-key "") (which-key-add-key-based-replacements doom-localleader-key "")) ;; ;;; `map!' macro (defvar doom-evil-state-alist '((?n . normal) (?v . visual) (?i . insert) (?e . emacs) (?o . operator) (?m . motion) (?r . replace) (?g . global)) "A list of cons cells that map a letter to a evil state symbol.") (defun doom--map-keyword-to-states (keyword) "Convert a KEYWORD into a list of evil state symbols. For example, :nvi will map to (list 'normal 'visual 'insert). See `doom-evil-state-alist' to customize this." (cl-loop for l across (doom-keyword-name keyword) if (assq l doom-evil-state-alist) collect (cdr it) else do (error "not a valid state: %s" l))) ;; specials (defvar doom--map-forms nil) (defvar doom--map-fn nil) (defvar doom--map-batch-forms nil) (defvar doom--map-state '(:dummy t)) (defvar doom--map-parent-state nil) (defvar doom--map-evil-p nil) (after! evil (setq doom--map-evil-p t)) (defun doom--map-process (rest) (let ((doom--map-fn doom--map-fn) doom--map-state doom--map-forms desc) (while rest (let ((key (pop rest))) (cond ((listp key) (doom--map-nested nil key)) ((keywordp key) (pcase key (:leader (doom--map-commit) (setq doom--map-fn 'doom--define-leader-key)) (:localleader (doom--map-commit) (setq doom--map-fn 'define-localleader-key!)) (:after (doom--map-nested (list 'after! (pop rest)) rest) (setq rest nil)) (:desc (setq desc (pop rest))) (:map (doom--map-set :keymaps `(quote ,(ensure-list (pop rest))))) (:mode (push (cl-loop for m in (ensure-list (pop rest)) collect (intern (concat (symbol-name m) "-map"))) rest) (push :map rest)) ((or :when :unless) (doom--map-nested (list (intern (doom-keyword-name key)) (pop rest)) rest) (setq rest nil)) (:prefix-map (cl-destructuring-bind (prefix . desc) (let ((arg (pop rest))) (if (consp arg) arg (list arg))) (let ((keymap (intern (format "doom-leader-%s-map" desc)))) (setq rest (append (list :desc desc prefix keymap :prefix prefix) rest)) (push `(defvar ,keymap (make-sparse-keymap)) doom--map-forms)))) (:prefix (cl-destructuring-bind (prefix . desc) (let ((arg (pop rest))) (if (consp arg) arg (list arg))) (doom--map-set (if doom--map-fn :infix :prefix) prefix) (when (stringp desc) (setq rest (append (list :desc desc "" nil) rest))))) (:textobj (let* ((key (pop rest)) (inner (pop rest)) (outer (pop rest))) (push `(map! (:map evil-inner-text-objects-map ,key ,inner) (:map evil-outer-text-objects-map ,key ,outer)) doom--map-forms))) (_ (condition-case _ (doom--map-def (pop rest) (pop rest) (doom--map-keyword-to-states key) desc) (error (error "Not a valid `map!' property: %s" key))) (setq desc nil)))) ((doom--map-def key (pop rest) nil desc) (setq desc nil))))) (doom--map-commit) (macroexp-progn (nreverse (delq nil doom--map-forms))))) (defun doom--map-append-keys (prop) (let ((a (plist-get doom--map-parent-state prop)) (b (plist-get doom--map-state prop))) (if (and a b) `(general--concat t ,a ,b) (or a b)))) (defun doom--map-nested (wrapper rest) (doom--map-commit) (let ((doom--map-parent-state (doom--map-state))) (push (if wrapper (append wrapper (list (doom--map-process rest))) (doom--map-process rest)) doom--map-forms))) (defun doom--map-set (prop &optional value) (unless (equal (plist-get doom--map-state prop) value) (doom--map-commit)) (setq doom--map-state (plist-put doom--map-state prop value))) (defun doom--map-def (key def &optional states desc) (when (or (memq 'global states) (null states)) (setq states (cons 'nil (delq 'global states)))) (when desc (let (unquoted) (cond ((and (listp def) (keywordp (car-safe (setq unquoted (doom-unquote def))))) (setq def (list 'quote (plist-put unquoted :which-key desc)))) ((setq def (cons 'list (if (and (equal key "") (null def)) `(:ignore t :which-key ,desc) (plist-put (general--normalize-extended-def def) :which-key desc)))))))) (dolist (state states) (push (list key def) (alist-get state doom--map-batch-forms))) t) (defun doom--map-commit () (when doom--map-batch-forms (cl-loop with attrs = (doom--map-state) for (state . defs) in doom--map-batch-forms if (or doom--map-evil-p (not state)) collect `(,(or doom--map-fn 'general-define-key) ,@(if state `(:states ',state)) ,@attrs ,@(mapcan #'identity (nreverse defs))) into forms finally do (push (macroexp-progn forms) doom--map-forms)) (setq doom--map-batch-forms nil))) (defun doom--map-state () (let ((plist (append (list :prefix (doom--map-append-keys :prefix) :infix (doom--map-append-keys :infix) :keymaps (append (plist-get doom--map-parent-state :keymaps) (plist-get doom--map-state :keymaps))) doom--map-state nil)) newplist) (while plist (let ((key (pop plist)) (val (pop plist))) (when (and val (not (plist-member newplist key))) (push val newplist) (push key newplist)))) newplist)) ;; (defmacro map! (&rest rest) "A convenience macro for defining keybinds, powered by `general'. If evil isn't loaded, evil-specific bindings are ignored. Properties :leader [...] an alias for (:prefix doom-leader-key ...) :localleader [...] bind to localleader; requires a keymap :mode [MODE(s)] [...] inner keybinds are applied to major MODE(s) :map [KEYMAP(s)] [...] inner keybinds are applied to KEYMAP(S) :prefix [PREFIX] [...] set keybind prefix for following keys. PREFIX can be a cons cell: (PREFIX . DESCRIPTION) :prefix-map [PREFIX] [...] same as :prefix, but defines a prefix keymap where the following keys will be bound. DO NOT USE THIS IN YOUR PRIVATE CONFIG. :after [FEATURE] [...] apply keybinds when [FEATURE] loads :textobj KEY INNER-FN OUTER-FN define a text object keybind pair :when [CONDITION] [...] :unless [CONDITION] [...] Any of the above properties may be nested, so that they only apply to a certain group of keybinds. States :n normal :v visual :i insert :e emacs :o operator :m motion :r replace :g global (binds the key without evil `current-global-map') These can be combined in any order, 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 (no emacs state; this is different from evil's Emacs state and will work in the absence of `evil-mode'). These must be placed right before the key string. Do (map! :leader :desc \"Description\" :n \"C-c\" #'dosomething) Don't (map! :n :leader :desc \"Description\" \"C-c\" #'dosomething) (map! :leader :n :desc \"Description\" \"C-c\" #'dosomething)" (when (or (bound-and-true-p byte-compile-current-file) (not noninteractive)) (doom--map-process rest))) (provide 'doom-keybinds) ;;; doom-keybinds.el ends here