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:
parent
ce38a80cf8
commit
4daa9271a0
26 changed files with 1616 additions and 1501 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue