Introduce general.el & rewrite map!

+ 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
This commit is contained in:
Henrik Lissner 2018-12-22 03:30:04 -05:00
parent ce38a80cf8
commit 4daa9271a0
No known key found for this signature in database
GPG key ID: 5F6C0EA160557395
26 changed files with 1616 additions and 1501 deletions

View file

@ -5,21 +5,21 @@
;; never loaded, then evil bindings set with `map!' will be ignored.
(defvar doom-leader-key "SPC"
"The leader prefix key, for global commands.")
"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-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.")
(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
@ -45,6 +45,50 @@ If any hook returns non-nil, all hooks after it are ignored.")
;;
;; 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
@ -68,18 +112,16 @@ If any hook returns non-nil, all hooks after it are ignored.")
;;
(defun doom--keybind-register (key desc &optional modes)
"Register a description for KEY with `which-key' in MODES.
KEYS should be a string in kbd format.
DESC should be a string describing what KEY does.
MODES should be a list of major mode symbols."
(after! which-key
(if modes
(dolist (mode modes)
(which-key-add-major-mode-key-based-replacements mode key desc))
(which-key-add-key-based-replacements key desc))))
(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.
@ -95,202 +137,197 @@ For example, :nvi will map to (list 'normal 'visual 'insert). See
(put :after 'lisp-indent-function 'defun)
(put :desc 'lisp-indent-function 'defun)
(put :leader 'lisp-indent-function 'defun)
(put :local 'lisp-indent-function 'defun)
(put :localleader 'lisp-indent-function 'defun)
(put :map '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 :textobj '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--keymaps nil)
(defvar doom--prefix nil)
(defvar doom--defer nil)
(defvar doom--local nil)
(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 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.
"A convenience macro for defining keybinds, powered by `general'.
If evil isn't loaded, it will ignore evil-specific bindings.
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
:n normal
:v visual
:i insert
:e emacs
:o operator
:m motion
:r replace
:g global (will work without evil)
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.
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').
If states are omitted the keybind will be global.
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] [...]
This can be customized with `doom-evil-state-alist'.
:textobj is a special state that takes a key and two commands, one for the
inner binding, another for the outer.
Flags
(:leader [...]) an alias for (:prefix doom-leader-key ...)
(:localleader [...]) an alias for (:prefix doom-localleader-key ...)
(:mode [MODE(s)] [...]) inner keybinds are applied to major MODE(s)
(:map [KEYMAP(s)] [...]) inner keybinds are applied to KEYMAP(S)
(:map* [KEYMAP(s)] [...]) same as :map, but deferred
(:prefix [PREFIX] [...]) assign prefix to all inner keybindings
(:after [FEATURE] [...]) apply keybinds when [FEATURE] loads
(:local [...]) make bindings buffer local; incompatible with keymaps!
Conditional keybinds
(: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 ; 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
(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\"))))"
(let ((doom--keymaps doom--keymaps)
(doom--prefix doom--prefix)
(doom--defer doom--defer)
(doom--local doom--local)
key def states forms desc modes)
(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 "<leader>"))
((eq key :localleader)
(push 'doom-localleader-key rest)
(setq key :prefix
desc "<localleader>")))
(pcase key
(:when (push `(if ,(pop rest) ,(macroexpand `(map! ,@rest))) forms) (setq rest '()))
(:unless (push `(if (not ,(pop rest)) ,(macroexpand `(map! ,@rest))) forms) (setq rest '()))
(:after (push `(after! ,(pop rest) ,(macroexpand `(map! ,@rest))) forms) (setq rest '()))
(:desc (setq desc (pop rest)))
((or :map :map*)
(setq doom--keymaps (doom-enlist (pop rest))
doom--defer (eq key :map*)))
(:mode
(setq modes (doom-enlist (pop rest)))
(unless doom--keymaps
(setq doom--keymaps
(cl-loop for m in modes
collect (intern (format "%s-map" (symbol-name m)))))))
(:textobj
(let* ((key (pop rest))
(inner (pop rest))
(outer (pop rest)))
(push (macroexpand `(map! (:map evil-inner-text-objects-map ,key ,inner)
(:map evil-outer-text-objects-map ,key ,outer)))
forms)))
(:prefix
(let ((def (pop rest)))
(setq doom--prefix
`(vconcat ,doom--prefix
,(if (or (stringp def)
(and (symbolp def)
(stringp (symbol-value def))))
`(kbd ,def)
def)))
(when desc
(push `(doom--keybind-register ,(key-description (eval doom--prefix))
,desc ',modes)
forms)
(setq desc nil))))
(:local
(setq doom--local t))
(_ ; might be a state doom--prefix
(setq states (doom--keyword-to-states key)))))
;; It's a key-def pair
((or (stringp key)
(characterp key)
(vectorp key)
(symbolp key))
(unwind-protect
(catch 'skip
(when (symbolp key)
(setq key `(kbd ,key)))
(when (stringp key)
(setq key (kbd key)))
(when doom--prefix
(setq key (append doom--prefix (list key))))
(unless (> (length rest) 0)
(user-error "map! has no definition for %s key" key))
(setq def (pop rest))
(when (or (vectorp def)
(stringp def))
(setq def
`(lambda () (interactive)
(setq unread-command-events
(nconc (mapcar (lambda (ev) (cons t ev))
(listify-key-sequence
,(cond ((vectorp def) def)
((stringp def) (kbd def)))))
unread-command-events)))))
(when desc
(push `(doom--keybind-register ,(key-description (eval key))
,desc ',modes)
forms))
(cond ((and doom--local doom--keymaps)
(push `(lwarn 'doom-map :warning
"Can't local bind '%s' key to a keymap; skipped"
,key)
forms)
(throw 'skip 'local))
((and doom--keymaps states)
(dolist (keymap doom--keymaps)
(when (memq 'global states)
(push `(define-key ,keymap ,key ,def) forms))
(when (featurep 'evil)
(when-let* ((states (delq 'global states)))
(push `(,(if doom--defer #'evil-define-key #'evil-define-key*)
',states ,keymap ,key ,def)
forms)))))
(states
(dolist (state states)
(if (eq state 'global)
(push `(global-set-key ,key ,def) forms)
(when (featurep 'evil)
(push (if doom--local
`(evil-local-set-key ',state ,key ,def)
`(evil-define-key* ',state 'global ,key ,def))
forms)))))
(doom--keymaps
(dolist (keymap doom--keymaps)
(push `(define-key ,keymap ,key ,def) forms)))
(t
(push `(,(if doom--local #'local-set-key #'global-set-key)
,key ,def)
forms))))
(setq states '()
doom--local nil
desc nil)))
(t (user-error "Invalid key %s" key))))
`(progn ,@(nreverse forms))))
(: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