They need to be changed as soon as possible, otherwise leader keybinds done before the change will use the old leader key.
349 lines
12 KiB
EmacsLisp
349 lines
12 KiB
EmacsLisp
;;; core-keybinds.el -*- lexical-binding: t; -*-
|
|
|
|
;; 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!' will be ignored.
|
|
|
|
(defvar doom-leader-key "SPC"
|
|
"The leader prefix key for Evil users.
|
|
|
|
This needs to be changed from $DOOMDIR/init.el.")
|
|
|
|
(defvar doom-leader-alt-key "M-SPC"
|
|
"An alternative leader prefix key, used for Insert and Emacs states, and for
|
|
non-evil users.
|
|
|
|
This needs to be changed from $DOOMDIR/init.el.")
|
|
|
|
(defvar doom-localleader-key "SPC m"
|
|
"The localleader prefix key, for major-mode specific commands.
|
|
|
|
This needs to be changed from $DOOMDIR/init.el.")
|
|
|
|
(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.
|
|
|
|
This needs to be changed from $DOOMDIR/init.el.")
|
|
|
|
(defvar doom-leader-map (make-sparse-keymap)
|
|
"An overriding keymap for <leader> keys.")
|
|
|
|
|
|
;;
|
|
(defvar doom-escape-hook nil
|
|
"A hook run after C-g is pressed (or ESC in normal mode, for evil users). Both
|
|
trigger `doom/escape'.
|
|
|
|
If any hook returns non-nil, all hooks after it are ignored.")
|
|
|
|
(defun doom/escape ()
|
|
"Run the `doom-escape-hook'."
|
|
(interactive)
|
|
(cond ((minibuffer-window-active-p (minibuffer-window))
|
|
;; quit the minibuffer if open.
|
|
(abort-recursive-edit))
|
|
;; Run all escape hooks. If any returns non-nil, then stop there.
|
|
((cl-find-if #'funcall doom-escape-hook))
|
|
;; don't abort macros
|
|
((or defining-kbd-macro executing-kbd-macro) nil)
|
|
;; Back to the default
|
|
((keyboard-quit))))
|
|
|
|
(global-set-key [remap keyboard-quit] #'doom/escape)
|
|
|
|
|
|
;;
|
|
;; General
|
|
|
|
(require 'general)
|
|
|
|
;; Convenience aliases
|
|
(defalias 'define-key! #'general-def)
|
|
(defalias 'unmap! #'general-unbind)
|
|
|
|
;; leader/localleader keys
|
|
(defvar doom-leader-alist `((t . ,doom-leader-map)))
|
|
(add-to-list 'emulation-mode-map-alists 'doom-leader-alist)
|
|
|
|
;; We avoid `general-create-definer' to ensure that :states, :prefix and
|
|
;; :keymaps cannot be overwritten.
|
|
(defmacro define-leader-key! (&rest args)
|
|
`(general-define-key
|
|
:states nil
|
|
:keymaps 'doom-leader-map
|
|
:prefix doom-leader-alt-key
|
|
,@args))
|
|
|
|
(general-create-definer define-localleader-key!
|
|
:major-modes t
|
|
:wk-full-keys nil
|
|
:prefix doom-localleader-alt-key)
|
|
|
|
;; Because :non-normal-prefix doesn't work for non-evil sessions (only evil's
|
|
;; emacs state), we must redefine `define-localleader-key!' once evil is loaded
|
|
(after! evil
|
|
(defmacro define-leader-key! (&rest args)
|
|
`(general-define-key
|
|
:states '(normal visual motion emacs)
|
|
:keymaps 'doom-leader-map
|
|
:prefix doom-leader-key
|
|
:non-normal-prefix doom-leader-alt-key
|
|
,@args))
|
|
|
|
(general-create-definer define-localleader-key!
|
|
:states '(normal visual motion emacs)
|
|
:major-modes t
|
|
:wk-full-keys nil
|
|
:prefix doom-localleader-key
|
|
:non-normal-prefix doom-localleader-alt-key))
|
|
|
|
|
|
;;
|
|
;; Packages
|
|
|
|
(def-package! which-key
|
|
:defer 1
|
|
:after-call pre-command-hook
|
|
:init
|
|
(setq which-key-sort-order #'which-key-prefix-then-key-order
|
|
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
|
|
;; general improvements to which-key readability
|
|
(set-face-attribute 'which-key-local-map-description-face nil :weight 'bold)
|
|
(which-key-setup-side-window-bottom)
|
|
(setq-hook! 'which-key-init-buffer-hook line-spacing 3)
|
|
(which-key-mode +1))
|
|
|
|
|
|
;; `hydra'
|
|
(setq lv-use-seperator t)
|
|
|
|
|
|
;;
|
|
(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--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 (substring (symbol-name keyword) 1)
|
|
if (cdr (assq l doom-evil-state-alist)) collect it
|
|
else do (error "not a valid state: %s" l)))
|
|
|
|
|
|
;; Register keywords for proper indentation (see `map!')
|
|
(put :after 'lisp-indent-function 'defun)
|
|
(put :desc 'lisp-indent-function 'defun)
|
|
(put :leader 'lisp-indent-function 'defun)
|
|
(put :localleader 'lisp-indent-function 'defun)
|
|
(put :map 'lisp-indent-function 'defun)
|
|
(put :keymap 'lisp-indent-function 'defun)
|
|
(put :mode 'lisp-indent-function 'defun)
|
|
(put :prefix 'lisp-indent-function 'defun)
|
|
(put :unless 'lisp-indent-function 'defun)
|
|
(put :when 'lisp-indent-function 'defun)
|
|
|
|
;; 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 '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)))
|
|
((or :map :map* :keymap)
|
|
(doom--map-set :keymaps `(quote ,(doom-enlist (pop rest)))))
|
|
(:mode
|
|
(push (cl-loop for m in (doom-enlist (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
|
|
(cl-destructuring-bind (prefix . desc) (doom-enlist (pop rest))
|
|
(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--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 nil ,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.
|
|
|
|
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').
|
|
|
|
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)
|
|
:keymap [KEYMAP(s)] [...] same as :map
|
|
:prefix [PREFIX] [...] set keybind prefix for following keys
|
|
:after [FEATURE] [...] apply keybinds when [FEATURE] loads
|
|
:textobj KEY INNER-FN OUTER-FN define a text object keybind pair
|
|
:if [CONDITION] [...]
|
|
:when [CONDITION] [...]
|
|
:unless [CONDITION] [...]
|
|
|
|
Any of the above properties may be nested, so that they only apply to a
|
|
certain group of keybinds.
|
|
|
|
Example
|
|
(map! :map magit-mode-map
|
|
:m \"C-r\" 'do-something ; C-r in motion state
|
|
:nv \"q\" 'magit-mode-quit-window ; q in normal+visual states
|
|
\"C-x C-r\" 'a-global-keybind
|
|
:g \"C-x C-r\" 'another-global-keybind ; same as above
|
|
|
|
(:when IS-MAC
|
|
:n \"M-s\" 'some-fn
|
|
:i \"M-o\" (lambda (interactive) (message \"Hi\"))))"
|
|
(doom--map-process rest))
|
|
|
|
(provide 'core-keybinds)
|
|
;;; core-keybinds.el ends here
|