BREAKING CHANGE: This deprecates the IS-(MAC|WINDOWS|LINUX|BSD) family of global constants in favor of a native `featurep` check: IS-MAC -> (featurep :system 'macos) IS-WINDOWS -> (featurep :system 'windows) IS-LINUX -> (featurep :system 'linux) IS-BSD -> (featurep :system 'bsd) The constants will stick around until the v3 release so folks can still use it -- and there are still some modules that use it, but I'll phase those uses out gradually. Fix: #7479
473 lines
18 KiB
EmacsLisp
473 lines
18 KiB
EmacsLisp
;;; 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 <leader> keys.")
|
|
|
|
|
|
;;
|
|
;;; Global keybind settings
|
|
|
|
(cond
|
|
(doom--system-macos-p
|
|
;; 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))
|
|
(doom--system-windows-p
|
|
(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 <C-i> 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 <leader> 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 <localleader> 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 "<leader>")
|
|
(which-key-add-key-based-replacements doom-localleader-key "<localleader>"))
|
|
|
|
|
|
;;
|
|
;;; `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
|