+ Now uses an overriding keymap for leader keys, so that it is always available, even outside of normal/visual states. In insert/emacs states, or in sessions where evil is absent, an alternative prefix is used for leader/localleader keys. See these variables: + doom-leader-prefix + doom-leader-alt-prefix + doom-localleader-prefix + doom-localleader-alt-prefix + Keybinds now support alternative prefixes through the new :alt-prefix property. This is useful for non-evil users and non-normal evil states. By default, this is M-SPC (leader) and M-SPC m (localleader). + Removed +evil-commands flag from config/default (moved to feature/evil/+commands.el). + config/default/+bindings.el has been split into config/default/+{evil,emacs}-bindings.el, which one is loaded depends on whether evil is present or not. The latter is blank, but will soon be populated with a keybinding scheme for non-evil users (perhaps inspired by #641). + The define-key! macro has been replaced; it is now an alias for general-def. + Added unmap! as an alias for general-unbind. + The following modifier key conventions are now enforced for consistency, across all OSes: alt/option = meta windows/command = super It used to be alt/option = alt windows/command = meta Many of the default keybinds have been updated to reflect this switch, but it is likely to affect personal meta/super keybinds! The map! macro has also been rewritten to use general-define-key. Here is what has been changed: + map! no longer works with characters, e.g. (map! ?x #'do-something) is no longer supported. Keys must be kbd-able strings like "C-c x" or vectors like [?C-c ?x]. + The :map and :map* properties are now the same thing. If specified keymaps aren't defined when binding keys, it is automatically deferred. + The way you bind local keybinds has changed: ;; Don't do this (map! :l "a" #'func-a :l "b" #'func-b) ;; Do this (map! :map 'local "a" #'func-a "b" #'func-b) + map! now supports the following new blocks: + (:if COND THEN-FORM ELSE-FORM...) + (:alt-prefix PREFIX KEYS...) -- this prefix will be used for non-normal evil states. Equivalent to :non-normal-prefix in general. + The way you declare a which-key label for a prefix key has changed: ;; before (map! :desc "label" :prefix "a" ...) ;; now (map! :prefix ("a" . "label") ...) + It used to be that map! supported binding a key to a key sequence, like so: (map! "a" [?x]) ; pressing a is like pressing x This functionality was removed *temporarily* while I figure out the implementation. Addresses: #448, #814, #860 Mentioned in: #940
333 lines
12 KiB
EmacsLisp
333 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.")
|
|
|
|
(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.")
|
|
|
|
(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>
|
|
(define-prefix-command 'doom-leader 'doom-leader-map)
|
|
(define-key doom-leader-map [override-state] 'all)
|
|
(general-define-key :states '(normal visual motion replace) doom-leader-key 'doom-leader)
|
|
(general-define-key :states '(emacs insert) doom-leader-alt-key 'doom-leader)
|
|
|
|
(defun general-leader-define-key (_state _keymap key def orig-def _kargs)
|
|
(let (general-implicit-kbd)
|
|
(general-define-key
|
|
:keymaps 'doom-leader-map
|
|
:wk-full-keys nil
|
|
key orig-def)))
|
|
|
|
;; <localleader>
|
|
(defun general-localleader-define-key (state keymap key _def orig-def kargs)
|
|
(unless keymap
|
|
(signal 'wrong-type-argument (list 'keymapp keymap)))
|
|
(let (general-implicit-kbd)
|
|
(apply #'general-define-key
|
|
:major-modes t
|
|
:keymaps keymap
|
|
(append
|
|
;; :non-normal-prefix isn't respected when evil is absent, so this
|
|
;; is necessary:
|
|
(if (featurep 'evil)
|
|
(list :states '(normal visual motion)
|
|
:prefix doom-localleader-key
|
|
:non-normal-prefix doom-localleader-alt-key)
|
|
(list :prefix doom-localleader-alt-key))
|
|
(list key orig-def)
|
|
nil))))
|
|
|
|
|
|
;;
|
|
;; 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 :alt-prefix 'lisp-indent-function 'defun)
|
|
(put :unless 'lisp-indent-function 'defun)
|
|
(put :if 'lisp-indent-function 'defun)
|
|
(put :when 'lisp-indent-function 'defun)
|
|
|
|
;; specials
|
|
(defvar doom--map-forms 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-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-set :definer '(quote leader)))
|
|
(:localleader
|
|
(doom--map-set :definer '(quote localleader)))
|
|
(: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 :if :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 :prefix prefix)
|
|
(when (stringp desc)
|
|
(setq rest (append (list :desc desc "" nil) rest)))))
|
|
(:alt-prefix
|
|
(cl-destructuring-bind (prefix . desc) (doom-enlist (pop rest))
|
|
(doom--map-set :non-normal-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 e
|
|
(doom--map-def (pop rest) (pop rest) (doom--keyword-to-states key) desc)
|
|
(error
|
|
(error "Not a valid `map!' property: %s" key))))))
|
|
|
|
((doom--map-def key (pop rest) nil desc)))))
|
|
|
|
(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 (copy-seq (append doom--map-state doom--map-parent-state nil))))
|
|
(push (if wrapper
|
|
(append wrapper (list (doom--map-process rest)))
|
|
(doom--map-process rest))
|
|
doom--map-forms)))
|
|
|
|
(defun doom--map-set (prop &optional value inhibit-commit)
|
|
(unless (or inhibit-commit
|
|
(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 (delq 'global states))
|
|
(push 'nil states))
|
|
(dolist (state states)
|
|
(when desc
|
|
(setq def
|
|
(if (and (equal key "")
|
|
(null def))
|
|
`(quote (nil :which-key ,desc))
|
|
`(list ,@(plist-put (general--normalize-extended-def def)
|
|
:which-key desc)))))
|
|
(push key (alist-get state doom--map-batch-forms))
|
|
(push def (alist-get state doom--map-batch-forms))))
|
|
|
|
(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 `(general-define-key ,@(if state `(:states ',state)) ,@attrs ,@(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)
|
|
:non-normal-prefix (doom--map-append-keys :non-normal-prefix)
|
|
:definer (plist-get doom--map-parent-state :definer)
|
|
:keymaps
|
|
(append (plist-get doom--map-parent-state :keymaps)
|
|
(plist-get doom--map-state :keymaps)
|
|
nil))
|
|
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 (will work without evil)
|
|
|
|
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
|
|
:alt-prefix [PREFIX] [...] use non-normal-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
|