;;; general.el --- Convenience wrappers for keybindings. -*- lexical-binding: t -*- ;; Author: Fox Kiester ;; URL: https://github.com/noctuid/general.el ;; Package-Version: 20180628.1112 ;; Created: February 17, 2016 ;; Keywords: vim, evil, leader, keybindings, keys ;; Package-Requires: ((emacs "24.4") (cl-lib "0.5")) ;; Version: 0.1 ;; This file is not part of GNU Emacs. ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; This package provides convenient wrappers for more succinctly defining ;; keybindings. It allows defining multiple keys at once, specifying an ;; arbitrary number of named prefix keys to be used in key definitions, ;; implicitly wrapping key strings with (kbd ...), and more. It provides a ;; single function for standard emacs key definitions as well as evil key ;; definitions for any evil state and any keymap. It also provides a setup ;; function to generate "nmap", "vmap", etc. keybinding functions for evil. ;; For more information see the README in the online repository. ;;; Code: (require 'cl-lib) ;; * Settings (defgroup general nil "Gives convenient wrappers for key definitions." :group 'convenience :prefix 'general-) (defcustom general-implicit-kbd t "Whether to implicitly wrap a (kbd) around `general-define-key' keys. This applies to the prefix key as well. This option is provided to make it easy to transition from other key definers to `general-define-key'. It does not apply to other helpers such as `general-key', `general-key-dispatch', and `general-translate-key'. These will always use `kbd' on keys that are strings." :group 'general :type 'boolean) (defcustom general-default-prefix nil "The default prefix key sequence to use." :group 'general :type 'string) (make-obsolete-variable 'general-default-prefix "This functionality will be removed in the future." "2018-01-21") (defcustom general-default-non-normal-prefix nil "The default prefix key sequence to use for the 'emacs and 'insert states. Note that this setting is only useful for evil-users and will only have an effect when binding keys in the 'emacs and/or 'insert states or in the 'evil-insert-state-map and/or 'evil-emacs-state-map keymaps. When this is not specified, `general-default-prefix' will be the default prefix for any states and keymaps. If this is specified `general-default-prefix' or the arg to :prefix will not be used when binding keys in the insert and emacs states." :group 'general :type 'string) (make-obsolete-variable 'general-default-non-normal-prefix "This functionality will be removed in the future." "2018-01-21") (defcustom general-default-global-prefix nil "The default prefix key sequence to use for all evil states. This setting is only useful for evil users. Note that like with `general-default-non-normal-prefix', if this or :global-prefix is specified, `general-default-prefix' or the arg to :prefix will not be used for binding keys in the insert and emacs states. If you don't need a different or extra prefix for one or both state types (insert and emacs vs. the other states), just use `general-default-prefix'/:prefix by itself." :group 'general :type 'string) (make-obsolete-variable 'general-default-global-prefix "This functionality will be removed in the future." "2018-01-21") (define-widget 'general-state 'lazy "General's evil state type." :type '(choice (const :tag "Insert state" insert) (const :tag "Emacs state" emacs) (const :tag "Normal state" normal) (const :tag "Visual state" visual) (const :tag "Motion state" motion) (const :tag "Operator state" operator) (const :tag "Replace state" replace) (const :tag "Use define-key not evil-define-key" nil) ;; other packages define states symbol)) (defcustom general-default-states nil "The default evil state(s) to make mappings in. Non-evil users should keep this nil." :group 'general :type '(choice general-state (set general-state))) (make-obsolete-variable 'general-default-states "This functionality will be removed in the future." "2018-01-21") (defcustom general-non-normal-states '(insert emacs hybrid iedit-insert) "List of \"non-normal\" evil states (used with :non-normal-prefix). When :states is not specified (only :keymaps), these will automatically be expanded to their full global evil keymap equivalents." :group 'general :type '(repeat general-state)) (define-widget 'general-keymap 'lazy "General's keymap type." :type '(choice (const :tag "Global keymap" global) (const :tag "Buffer local keymap" local) symbol)) (defcustom general-default-keymaps 'global "The default keymap(s) to bind keys in." :group 'general :type '(choice general-keymap (repeat general-keymap))) (make-obsolete-variable 'general-default-keymaps "This functionality will be removed in the future." "2018-01-21") (defcustom general-vim-definer-default nil "Whether set the states or keymaps in a `general-create-vim-definer' function. If nil, use the default from when the function was created. If 'keymaps, set the default keymaps. If 'states, set the default states." :group 'general :type '(choice (const :tag "Default to setting :keymaps" keymaps) (const :tag "Default to setting :states" states) (const :tag "Use the initial default" nil))) (make-obsolete-variable 'general-vim-definer-default "This functionality is no longer necessary." "2018-01-20") (defvar general-keybindings nil "Holds all the keybindings created with `general-define-key' (and wrappers). This is an alist of a keymap to an alist of a state to keybindings.") (defvar general-local-keybindings nil "Holds all the local keybindings created with `general-define-key'. This is an alist of a state to keybindings.") (make-variable-buffer-local 'general-local-keybindings) (define-widget 'general-alist 'lazy "General's alist type." :type '(alist :key-type (or symbol (repeat symbol)) :value-type symbol)) (defcustom general-keymap-aliases '((override . general-override-mode-map) ((i insert) . evil-insert-state-map) ((e emacs) . evil-emacs-state-map) ((h hybrid) . evil-hybrid-state-map) ((n normal) . evil-normal-state-map) ((v visual) . evil-visual-state-map) ((m motion) . evil-motion-state-map) ((o operator) . evil-operator-state-map) ((r replace) . evil-replace-state-map) ((in inner) . evil-inner-text-objects-map) ((out outer) . evil-outer-text-objects-map)) "An alist for mapping short keymap names to their full names. Earlier entries have higher precedence." :group 'general :type 'general-alist) (defcustom general-state-aliases '((i . insert) (e . emacs) (h . hybrid) (n . normal) (v . visual) (m . motion) (o . operator) (r . replace)) "An alist for mapping short state names to their full names. Earlier entries have higher precedence." :group 'general :type 'general-alist) ;; ** `general-describe-keybindings' Settings (defcustom general-describe-keybinding-sort-function nil "Function used to sort keybindings for `general-describe-keybindings'." :group 'general :type '(choice function (const nil))) (defcustom general-describe-state-sort-function #'general--sort-evil-state-conses "Function used to sort the states conses for `general-describe-keybindings'." :group 'general :type '(choice function (const nil))) (defcustom general-describe-keymap-sort-function nil "Function used to sort the keymap conses`general-keybindings' for `general-describe-keybindings'." :group 'general :type '(choice function (const nil))) (defcustom general-describe-priority-keymaps '(local global evil-insert-state-map evil-emacs-state-map evil-hybrid-state-map evil-normal-state-map evil-visual-state-map evil-motion-state-map evil-operator-state-map evil-replace-state-map evil-inner-text-objects-map evil-outer-text-objects-map evil-ex-search-keymap evil-ex-completion-map evil-command-window-mode-map evil-window-map) "Keymaps to print first for `general-describe-keybindings'." :group 'general :type '(repeat sybmol)) (defcustom general-describe-update-previous-definition 'on-change "Whether to update the previous definition when a key is bound. When set to 'on-change, the previous definition will only be updated when the definition changes (e.g. re-evaluating a file with keybindings will not affect the stored previous definition). When set to nil, it will only be updated when the key was previously unbound." :group 'general ;; can't think of a use case, but add 'always if requested ;; t is equivalent of on-change :type '(choice (const :tag "When definition has changed" on-change) (const :tag "When the key was previously unbound" nil))) ;; * Override Minor Modes (defcustom general-override-auto-enable t "Whether to automatically enable `general-override-mode'. If non-nil, enable `general-override-mode' when binding a key in `general-override-mode-map'." :group 'general :type 'boolean) (defvar general-override-mode-map (make-sparse-keymap) "A keymap that will take priority over other minor mode keymaps. This is only for non-evil keybindings (it won't override keys bound with `evil-define-key'.") (define-minor-mode general-override-mode "A global minor mode used for key definitions that should override others." :lighter "" :global t :keymap general-override-mode-map) (defvar-local general-override-local-mode-map (make-sparse-keymap) "A keymap that will take priority over other minor mode keymaps. This keymap is buffer-local and will take precedence over `general-override-mode-map'. General uses this keymap when creating non-evil local keybindings.") (define-minor-mode general-override-local-mode "A local minor mode used for key definitions that should override others." :lighter "" :keymap general-override-local-mode-map) (defvar-local general-maps-alist `((general-override-mode . ,general-override-mode-map)) "Holds the (mode . keymap) pairs for general's override modes.") ;; not affected by changing major modes (put 'general-maps-alist 'permanent-local t) (defvar-local general--maps-alist-updated nil "Whether `general-maps-alist' has been set correctly for the current buffer.") (put 'general-maps-alist 'permanent-local t) (declare-function evil-make-intercept-map "evil-core") (defun general-override-make-intercept-maps (_sym states) "Make intercept keymaps for STATES in `general-override-mode-map'. This means that keys bound in STATES for `general-override-mode-map' will take precedence over keys bound in other evil auxiliary maps." ;; can't use `general-with-eval-after-load' here; not available (with-eval-after-load 'evil ;; TODO eventually use new evil-make-intercept-map arg (dolist (state states) (evil-make-intercept-map (evil-get-auxiliary-keymap general-override-mode-map state t t) state)))) (defcustom general-override-states '(insert emacs hybrid normal visual motion operator replace) "States to make intercept maps for in `general-override-mode-map'. Note that this uses :set, meaning that if you want to change the value, you should either set it using customize (e.g. `general-setq' or `customize-set-variable') or set it before loading general if using `setq'." :group 'general :type '(repeat general-state) :set #'general-override-make-intercept-maps) (defun general--update-maps-alist () "Update `general-maps-alist' for override modes. This is necessary to ensure `general-override-local-mode-map' is the buffer's local version." (setq general-maps-alist `((general-override-local-mode . ,general-override-local-mode-map) (general-override-mode . ,general-override-mode-map)) general--maps-alist-updated t)) (cl-pushnew 'general-maps-alist emulation-mode-map-alists) (defun general-local-map () "Return `general-override-local-mode-map'. Also turn on `general-override-local-mode' and update `general-maps-alist'." (or general-override-local-mode (general-override-local-mode)) (unless general--maps-alist-updated (general--update-maps-alist)) general-override-local-mode-map) ;; * General Helpers (defmacro general-with-eval-after-load (file &rest body) "Like `with-eval-after-load' but don't always add to `after-load-alist'. When FILE has already been loaded, execute BODY immediately without adding it to `after-load-alist'." (declare (indent 1)) `(if (if (stringp ,file) (load-history-filename-element (purecopy (load-history-regexp ,file))) (featurep ,file)) (progn ,@body) (eval-after-load ,file (lambda () ,@body)))) (defun general--unalias (symbol &optional statep) "Return the full keymap or state name associated with SYMBOL. If STATEP is non-nil, check `general-state-aliases' instead of `general-keymap-aliases'." (let ((match (cdr (cl-assoc symbol (if statep general-state-aliases general-keymap-aliases) ;; test-fn is new to assoc in 26.1 :test (lambda (symbol key) (or (eq symbol key) (ignore-errors (memq symbol key)))))))) (or match symbol))) ;; don't want to reuse `general--unalias' since the user can alter ;; `general-keymap-aliases' (defun general--evil-keymap-for-state (state) "Return a symbol corresponding to the global evil keymap for STATE." (intern (concat "evil-" (symbol-name state) "-state-map"))) (defun general--kbd (key) "Use `kbd' on KEY when it is a string." (if (stringp key) (kbd key) key)) ;; TODO refactor to be more straightforward (defun general--concat (nokbd &rest keys) "Concatenate the strings in KEYS. If `general-implicit-kbd' is non-nil, interleave the strings in KEYS with spaces; unless NOKBD is non-nil, apply (kbd ...) to the result. If `general-implicit-kbd' is nil, just concatenate the keys." (setq keys (remove nil keys)) (if general-implicit-kbd (let ((keys (mapconcat (lambda (x) (if (vectorp x) (key-description x) x)) keys " "))) (if nokbd keys (kbd keys))) (apply #'concat keys))) (defun general--apply-prefix-and-kbd (prefix maps) "Prepend the PREFIX sequence to all the keys that are strings in MAPS. Also apply (kbd ...) to key and definition strings if `general-implicit-kbd' is non-nil." (setq prefix (or prefix "")) (cl-loop for (key def) on maps by 'cddr collect (general--concat nil prefix key) and collect def)) (defun general--lookup-key (state keymap key &optional minor-mode-p) "Return the current definition for STATE, KEYMAP, and KEY." (when key (let ((keymap (general--get-keymap state keymap minor-mode-p))) (when keymap (let ((def (lookup-key keymap key))) (if (and (numberp def) (= def 1)) nil def)))))) (defun general--record-keybindings (keymap state maps &optional minor-mode-p) "For KEYMAP and STATE, add MAPS to `general-keybindings'. If KEYMAP is \"local\", add MAPS to `general-local-keybindings.' For non-evil keybindings, STATE will be nil. Duplicate keys will be replaced with the new ones. MINOR-MODE-P should be non-nil when keymap corresponds to a minor-mode name (as used with `evil-define-minor-mode-key') as opposed to a keymap name." (if (and state (not (featurep 'evil))) (general-with-eval-after-load 'evil (general--record-keybindings keymap state maps minor-mode-p)) (let* (keys (maps (cl-loop for (key new-def _orig-def) on maps by 'cl-cdddr collect (list key new-def (let* ((current-def (general--lookup-key state keymap key minor-mode-p)) ;; none of these will fail if nil (keymap-cons (assq keymap general-keybindings)) (state-cons (assq state (cdr keymap-cons))) (mapping (cl-find key (cdr state-cons) :test #'equal :key #'car)) (previous-def (cl-caddr mapping))) (if (or (and current-def (not previous-def)) (and general-describe-update-previous-definition (not (equal new-def current-def)))) current-def previous-def))) do (push key keys)))) (cond ((eq keymap 'local) (unless (assq state general-local-keybindings) (add-to-list 'general-local-keybindings (list state))) (let ((state-cons (assq state general-local-keybindings))) (setcdr state-cons ;; remove old duplicate keys (cl-remove-duplicates (append (cdr state-cons) maps) :key #'car :test #'equal)))) (t (unless (assq keymap general-keybindings) (add-to-list 'general-keybindings (list keymap))) (unless (assq state (assq keymap general-keybindings)) (setcdr (assq keymap general-keybindings) (append (cdr (assq keymap general-keybindings)) (list (list state))))) (let ((state-cons (assq state (assq keymap general-keybindings)))) (setcdr state-cons (cl-remove-duplicates (append (cdr state-cons) maps) :key #'car :test #'equal)))))))) ;; don't force non-evil user to require evil for one function (defun general--delay (condition form hook &optional append local name) "Execute FORM when CONDITION becomes true, checking with HOOK. NAME specifies the name of the entry added to HOOK. If APPEND is non-nil, the entry is appended to the hook. If LOCAL is non-nil, the buffer-local value of HOOK is modified. This is `evil-delay'." (declare (indent 2)) (if (and (not (booleanp condition)) (eval condition)) (eval form) (let* ((name (or name (format "general-delay-form-in-%s" hook))) (fun (make-symbol name)) (condition (or condition t))) (fset fun `(lambda (&rest args) (when ,condition (remove-hook ',hook #',fun ',local) ,form))) (put fun 'permanent-local-hook t) (add-hook hook fun append local)))) (defun general--getf (def fallback-plist keyword) "From DEF or FALLBACK-PLIST get the corresponding value for KEYWORD. FALLBACK-PLIST will be checked when KEYWORD does not exist in DEF (not in cases where it is explicitly specified as nil). If DEF isn't a general extended definition, only check in FALLBACK-PLIST." (if (general--extended-def-p def) (cl-getf def keyword (cl-getf fallback-plist keyword)) (cl-getf fallback-plist keyword))) (defun general--getf2 (plist keyword1 keyword2) "Check in PLIST for either KEYWORD1 or KEYWORD2." (or (cl-getf plist keyword1) (cl-getf plist keyword2))) (declare-function evil-get-minor-mode-keymap "evil-core") (declare-function evil-state-property "evil-common") (declare-function evil-get-auxiliary-keymap "evil-core") (cl-defun general--get-keymap (state &optional keymap minor-mode ignore-special) "Transform STATE and the symbol or keymap KEYMAP into the appropriate keymap. If MINOR-MODE and STATE are non-nil, use `evil-get-minor-mode-keymap'. If IGNORE-SPECIAL is non-nil, do not try to resolve the \"special\" keymaps 'global and 'local. In this case, the only thing this function will do is return the actually keymap if KEYMAP is a symbol besides 'global or 'local. Otherwise the keymap returned depends on whether STATE is specified. Note that if STATE is specified, evil needs to be installed and will be required. STATE nil: 'local - Run/return `general-local-map' 'global - Run/return `current-global-map' else - Return keymap or (symbol-value keymap) STATE non-nil: 'local - Return the corresponding evil local map 'global - Return the corresponding evil global map else - Return the corresponding evil auxiliary or minor mode map" (when (and (symbolp keymap) (not (memq keymap '(global local)))) (setq keymap (symbol-value keymap))) (when ignore-special (cl-return-from general--get-keymap keymap)) (if state (if (require 'evil nil t) (cond ((or (null keymap) (eq keymap 'global)) (evil-state-property state :keymap t)) (minor-mode (evil-get-minor-mode-keymap state keymap)) ((eq keymap 'local) (evil-state-property state :local-keymap t)) (t (evil-get-auxiliary-keymap keymap state t t))) (error "Evil is required if state is specified")) (cl-case keymap (global (current-global-map)) (local (general-local-map)) (t keymap)))) (define-obsolete-function-alias 'general--parse-keymap 'general--get-keymap "2018-01-14") (defun general--remove-keyword-args (rest) "Remove all keyword arguments from the list REST. Return a list of the altered REST list and a list of the removed keyword arguments. The order of arguments will be preserved. Note that the length of REST does not need to be even (i.e. there can be an odd number of positional arguments)." (let (args kargs) (while rest (cond ((keywordp (car rest)) (push (pop rest) kargs) (push (pop rest) kargs)) (t (push (pop rest) args)))) (list (nreverse args) (nreverse kargs)))) (defmacro general--ensure-lists (&rest vars) "Ensure that all variables in VARS are lists if they are not already. If any variable is a lambda, it will not be considered to be a list. If a var is nil, it will be set to (list nil)." `(progn ,@(mapcar (lambda (var) `(unless (and ,var (listp ,var) ;; lambdas are lists (not (functionp ,var))) (setq ,var (list ,var)))) vars))) ;; * Extended Key Definition Language ;; ** Variables (defvar general-extended-def-keywords '(:which-key :wk :properties :repeat :jump) "Extra keywords that are valid for extended definitions. These can work both locally (in extended definitions) and globally (in which case they apply to all definitions including normal ones). Note that not all keywords need to make sense/work globally. If the keyword should be ignored when used globally, add it to `general-extended-def-global-ignore-keywords' as well. For each keyword there should be a corresponding function named general-extended-def-: which will be passed state, keymap (the symbol not actual keymap), key (the internal representation, i.e. `kbd' already called if necessary), edef (always a plist; normal definitions will automatically be converted), and kargs (the original `general-define-key' keyword argument plist; useful when the keyword can be used globally or has helper keywords that can be used globally). This function is only called for side effects; if you actually need to alter the definition, you should add the keyword to `general-rewrite-def-keywords' or `general-rewrite-def-after-keywords' instead. The order of those lists matters, but the order of this list does not. `general--get-keymap' may be useful for getting the actual keymap from the keymap symbol. `general--getf' may be useful for keywords (helper or main) that can be specified globally (in kargs) and overridden locally (in def).") (defvar general-rewrite-def-keywords '(:keymap :prefix-command :prefix-keymap) "Extended definition keywords that alter the definition. Each keyword should have a corresponding function named general-extended-def-: and should return a new extended definition plist (with an altered :def entry). See `general-extended-def-keywords' for information on the arguments this function should take. These functions will be run in the order they appear in this list, and each will be passed the most recent version of the extended definition plist. In contrast to the functions for `general-rewrite-def-after-keywords', these functions will alter the definition before any `general-extended-def-keyword' functions run. For example, if your function creates a newly named wrapper command around the user-specified command, you'd want to add the keyword to this list, so that `general-extended-def-keywords' functions would have access to new command name (e.g. for :which-key to work properly). On the other hand, if the keyword, for example, involves putting the definition in an extended menu item like with :predicate, you should add to `general-rewrite-def-after-keywords' instead.") (defvar general-rewrite-def-after-keywords '(:predicate) "Extended definition keywords that alter the definition. See `general-rewrite-def-keywords' for more information.") (defvar general-extended-def-global-ignore-keywords '(:keymap :prefix-command :prefix-map) "Extended definitions that should be ignored when used globally. For example, :prefix-command and :prefix-map are handled differently when used globally (they have special interaction with other global keywords). :keymap, on the other hand, doesn't make sense at all globally.") ;; ** Normal Extended Definition Functions (defvar which-key-replacement-alist) (defun general--add-which-key-replacement (mode replacement) (let* ((mode-match (assq mode which-key-replacement-alist)) (mode-alist (cdr mode-match))) (cond (mode (push replacement mode-alist) (if mode-match (setcdr mode-match mode-alist) (push (cons mode mode-alist) which-key-replacement-alist))) (t (push replacement which-key-replacement-alist))))) (defvar which-key--prefix-title-alist) (defun general--add-which-key-title-prefix (mode keys title-prefix) (let* ((mode-match (assq mode which-key--prefix-title-alist)) (title-mode-alist (cdr mode-match)) (title-cons (cons keys title-prefix))) (cond (mode (push title-cons title-mode-alist) (if mode-match (setcdr mode-match title-mode-alist) (push (cons mode title-mode-alist) which-key--prefix-title-alist))) (t (push title-cons which-key--prefix-title-alist))))) (defun general--remove-map (keymap) "Remove \"-map\" from the symbol KEYMAP." ; (intern (replace-regexp-in-string "-map$" "" (symbol-name keymap)))) ;; TODO better documentation (defun general-extended-def-:which-key (_state keymap key edef kargs) "Add a which-key description for KEY. If :major-modes is specified in EDEF, add the description for the corresponding major mode. KEY should not be in the kbd format (kbd should have already been run on it)." (general-with-eval-after-load 'which-key (let* ((wk (general--getf2 edef :which-key :wk)) (major-modes (general--getf edef kargs :major-modes)) (keymaps (plist-get kargs :keymaps)) ;; index of keymap in :keymaps (keymap-index (cl-dotimes (ind (length keymaps)) (when (eq (nth ind keymaps) keymap) (cl-return ind)))) (mode (let ((mode (if (and major-modes (listp major-modes)) (nth keymap-index major-modes) major-modes))) (if (eq mode t) (general--remove-map keymap) mode))) (key (key-description key)) (key-regexp (concat (when (general--getf edef kargs :wk-full-keys) "\\`") (regexp-quote key) "\\'")) (prefix (plist-get kargs :prefix)) (binding (or (when (and (plist-get edef :def) (not (plist-get edef :keymp))) (plist-get edef :def)) (when (and prefix (string= key prefix)) (plist-get kargs :prefix-command)))) (replacement (cond ((stringp wk) (cons nil wk)) (t wk))) (match/replacement (cons (cons (when (general--getf edef kargs :wk-match-keys) key-regexp) (when (and (general--getf edef kargs :wk-match-binding) binding (symbolp binding)) (symbol-name binding))) replacement))) (general--add-which-key-replacement mode match/replacement) (when (and (consp replacement) ;; lambda (not (functionp replacement))) (general--add-which-key-title-prefix mode key (cdr replacement)))))) (defalias 'general-extended-def-:wk #'general-extended-def-:which-key) (declare-function evil-add-command-properties "evil-common") (defun general-extended-def-:properties (_state _keymap _key edef kargs) "Use `evil-add-command-properties' to add properties to a command. The properties should be specified with :properties in either EDEF or KARGS." (general-with-eval-after-load 'evil (let ((properties (general--getf edef kargs :properties)) (command (cl-getf edef :def))) (apply #'evil-add-command-properties command properties)))) (defun general-extended-def-:repeat (_state _keymap _key edef kargs) "Use `evil-add-command-properties' to set the :repeat property for a command. The repeat property should be specified with :repeat in either EDEF or KARGS." (general-with-eval-after-load 'evil (let ((repeat-property (general--getf edef kargs :repeat)) (command (cl-getf edef :def))) (evil-add-command-properties command :repeat repeat-property)))) (defun general-extended-def-:jump (_state _keymap _key edef kargs) "Use `evil-add-command-properties' to set the :jump property for a command. The jump property should be specified with :jump in either EDEF or KARGS." (general-with-eval-after-load 'evil (let ((jump-property (general--getf edef kargs :jump)) (command (cl-getf edef :def))) (evil-add-command-properties command :jump jump-property)))) ;; ** Extended Defintion Functions That Alter the Definition (defun general-extended-def-:keymap (state keymap _key edef kargs) "Return an extended definition for a keymap or a \"autoloaded\" keymap. If the specified keymap does not exist, create a function that binds the keys it was invoked with in STATE and KEYMAP to the keymap specified in the extended definition EDEF and then act as if it was originally bound to that keymap (subsequent keys will be looked up in the keymap). KARGS or EDEF should contain the package in which the keymap is created (as specified with :package). If the keymap already exists, it will simply be returned." (let ((bind-keymap-sym (plist-get edef :def)) (package (general--getf edef kargs :package)) (definer (general--getf edef kargs :definer))) (if (boundp bind-keymap-sym) (setf (cl-getf edef :def) (symbol-value bind-keymap-sym)) (if package (setf (cl-getf edef :def) ;; relying on lexical binding here (lambda () (interactive) (unless (or (featurep package) (require package nil t)) (error (format "Failed to load package: %s" package))) (unless (and (boundp bind-keymap-sym) (keymapp (symbol-value bind-keymap-sym))) (error (format "A keymap called %s is not defined in the %s package" bind-keymap-sym package))) ;; use `this-command-keys' as `key' may not be the full sequence (let ((keys (this-command-keys)) (general-implicit-kbd nil)) (general-define-key :states state :keymaps keymap :definer definer keys (symbol-value bind-keymap-sym)) (setq prefix-arg current-prefix-arg unread-command-events (mapcar (lambda (ev) (cons t ev)) (listify-key-sequence keys)))))) (error "In order to \"autoload\" a keymap, :package must be specified")))) edef) (defun general--define-prefix (command-name &optional map-name menu-name) "Define a prefix command and/or keymap. COMMAND-NAME corresponds to the prefix command name. When COMMAND-NAME is non-nil, `define-prefix-command' will be used and will be passed MAP-NAME and MENU-NAME. When COMMAND-NAME is nil and MAP-NAME is non-nil, only a prefix keymap will be created, and its menu name/prompt will be set to MENU-NAME (if MENU-NAME is non-nil). Existing prefix keymaps/commands will not be recreated/rebound." (if (or (and command-name (fboundp command-name)) (and map-name (boundp map-name))) (or command-name (symbol-value map-name)) (cond (command-name (define-prefix-command command-name map-name menu-name)) (map-name (eval `(defvar ,map-name (make-sparse-keymap ,menu-name))))))) (defun general-extended-def-:prefix-command (_state _keymap _key edef _kargs) "Create and return a prefix command or map for the extended definition EDEF. The :prefix-command, :prefix-map, and :prefix-name properties from EDEF are passed to `general--define-prefix'." ;; NOTE will be called twice if both specified, but doesn't matter because ;; won't recreate prefix-command (setf (cl-getf edef :def) (general--define-prefix (plist-get edef :prefix-command) (plist-get edef :prefix-map) (plist-get edef :prefix-name))) edef) (defalias 'general-extended-def-:prefix-map #'general-extended-def-:prefix-command) ;; http://endlessparentheses.com/define-context-aware-keys-in-emacs.html (defun general-extended-def-:predicate (_state _keymap _key edef kargs) "Return an altered extended definition EDEF with a predicate applied. The predicate is obtained either from EDEF or KARGS." (let ((def (cl-getf edef :def)) (predicate (general--getf edef kargs :predicate))) (setf (cl-getf edef :def) `(menu-item "" nil :filter (lambda (&optional _) (when ,predicate ',def)))) edef)) ;; ** Parsing Extended Definitions (defun general--extended-def-p (def) "Return whether DEF is an extended definition." (and (listp def) (not (keymapp def)) ;; lambda (not (functionp def)) (not (eq (car def) 'menu-item)) ;; will error on cons (ignore-errors (cl-some #'keywordp def)))) (defun general--normalize-extended-def (edef) "Rewrite the extended definition EDEF to include a :def property. If EDEF is not an extended defintion, make it into one. This handles the allowed shorthand syntax. For example, these are the same: (some-func) (:def some-func) Some extended definition keywords can be used instead of :def (mainly for backwards compatibility). For example, these are the same: (some-keymap :keymap t) (:keymap some-keymap) (:def some-keymap :keymap t)" ;; NOTE: This is absolutely necessary for plist functions to work (if (general--extended-def-p edef) (unless (keywordp (car edef)) (setq edef (cons :def edef))) (setq edef (list :def edef))) ;; :keymap checks :def always instead of :keymap, and :which-key also checks ;; :def always (instead of :prefix-command) ;; note that :keymap and :prefix-map will later rewrite their :def to the ;; actual keymap value (unless (plist-get edef :def) (setf (cl-getf edef :def) (cl-getf edef :keymap (cl-getf edef :prefix-command (plist-get edef :prefix-map))))) edef) (defun general--extract-def (edef) "Return the bindable definition from the extended definition EDEF." (if (plist-get edef :ignore) ;; just for side effects (e.g. which-key description for prefix) ;; return something that isn't a valid definition :ignore (plist-get edef :def))) (defun general--run-extended-def-functions (state keymap key edef kargs) "Run the extended definition functions for the matched keywords. Pass each extended definition function STATE, KEYMAP, KEY, EDEF, and KARGS. For each keyword from `general-extended-def-keywords', `general-rewrite-def-keywords', and `general-rewrite-def-after-keywords' found in EDEF or KARGS, call the corresponding function named general-extended-def-:. The functions for `general-rewrite-def-keywords' will rewrite the extended definition plist before the functions for `general-extended-def-keywords' are called, and the functions for `general-rewrite-def-after-keywords' are called after that. Functions are called in the order they appear in each list. Finally, return the potentially altered extended definition plist." (cl-flet ((run-edef-functions (keywords &optional alter-def) (dolist (keyword keywords) (when (or (plist-get edef keyword) (and (not (memq keyword general-extended-def-global-ignore-keywords)) (plist-get kargs keyword))) (let ((ret (funcall (intern (format "general-extended-def-%s" keyword)) state keymap key edef kargs))) (when alter-def (setq edef ret))))))) (run-edef-functions general-rewrite-def-keywords t) (run-edef-functions general-extended-def-keywords) (run-edef-functions general-rewrite-def-after-keywords t)) edef) (defun general--parse-def (state keymap key def kargs) "Rewrite DEF into a valid/bindable definition. This function will execute all extended definitions, potentially rewriting the original definition (e.g. applying a predicate). Pass STATE, KEYMAP, KEY, DEF, and KARGS to each matched extended definition function. See `general--run-extended-def-functions' for more information." (setq def (general--normalize-extended-def def)) (general--extract-def (general--run-extended-def-functions state keymap key def kargs))) (defun general--parse-maps (state keymap maps kargs) "Rewrite MAPS so that the definitions are bindable. This includes possibly calling `kbd' on keys and parsing extended definitions. Turn key/binding pairs in MAPS into triples in the form of (key parsed-def original-def) where parsed-def is the bindable form and original-def is the original definition as an extended definition plist (turn normal definitions into extended definition plists and implicitly add \":def\" to the beginning of extended definitions when necessary)." (let (bindable-def) (cl-loop for (key def) on maps by 'cddr do (setq bindable-def (general--parse-def state keymap key def kargs)) unless (eq bindable-def :ignore) collect key and collect (if general-implicit-kbd (general--kbd bindable-def) bindable-def) and collect (general--normalize-extended-def def)))) ;; * Helper Key Definers (declare-function evil-define-minor-mode-key "evil-core") (defun general-minor-mode-define-key (state mode key def _orig-def _kargs) "A wrapper for `evil-define-minor-mode-key'." (general-with-eval-after-load 'evil (evil-define-minor-mode-key state mode key def))) (declare-function lispy-define-key "lispy") (defun general-lispy-define-key (_state keymap key def orig-def kargs) "A wrapper for `lispy-define-key'." (general-with-eval-after-load 'lispy (let* ((keymap (general--get-keymap nil keymap)) (key (key-description key)) (plist (general--getf orig-def kargs :lispy-plist))) (apply #'lispy-define-key keymap key def plist)))) (declare-function worf-define-key "worf") (defun general-worf-define-key (_state keymap key def orig-def kargs) "A wrapper for `worf-define-key'." (general-with-eval-after-load 'worf (let* ((keymap (general--get-keymap nil keymap)) (key (key-description key)) (plist (general--getf orig-def kargs :worf-plist))) (apply #'worf-define-key keymap key def plist)))) (declare-function lpy-define-key "lpy") (defun general-lpy-define-key (_state keymap key def _orig-def _kargs) "A wrapper for `lpy-define-key'." (general-with-eval-after-load 'lpy (let* ((keymap (general--get-keymap nil keymap)) (key (key-description key))) (lpy-define-key keymap key def)))) (declare-function evil-define-key* "evil-core") (defun general--define-key-dispatch (state keymap maps kargs) "In STATE (if non-nil) and KEYMAP, bind MAPS. MAPS is composed of triplets of (key parsed-def original-def). This function determines the appropriate base definer function to use based depending on whether :definer is present in original-def or KARGS or whether STATE is non-nil if no custom definer is specified." (when (and general-override-auto-enable (eq keymap 'general-override-mode-map) (not general-override-mode)) (general-override-mode)) (while maps (let* ((key (pop maps)) (def (pop maps)) (orig-def (pop maps)) (definer (general--getf orig-def kargs :definer))) (if definer (funcall (intern (format "general-%s-define-key" (symbol-name definer))) state keymap key def orig-def kargs) (cond (state ;; just to get the symbol-value of the keymap when it is not ;; global/local (setq keymap (general--get-keymap nil keymap nil t)) (general-with-eval-after-load 'evil (evil-define-key* state keymap key def))) (t (setq keymap (general--get-keymap nil keymap)) (define-key keymap key def))))))) (defvar general--definer-p nil "Whether the current keybinding is being created with `general-define-key'.") (defun general--define-key (states keymap maps non-normal-maps global-maps kargs) "A helper function for `general-define-key'. Choose based on STATES and KEYMAP which of MAPS, NON-NORMAL-MAPS, and GLOBAL-MAPS to use for the keybindings. This function will rewrite extended definitions, add predicates when applicable, and then choose the base function to bind the keys with by calling `general--define-key-dispatch'." (let ((general--definer-p t)) (dolist (state states) (let* ((non-normal-p (if state (memq state general-non-normal-states) (memq keymap (mapcar #'general--evil-keymap-for-state general-non-normal-states)))) (valid-maps (list (cond ((and non-normal-maps non-normal-p) non-normal-maps) ((and global-maps non-normal-p) nil) (t maps)) global-maps))) (dolist (maps valid-maps) (when maps (setq maps (general--parse-maps state keymap maps kargs)) ;; NOTE: :definer 'minor-mode cannot be specified locally (general--record-keybindings keymap state maps (eq (cl-getf kargs :definer) 'minor-mode)) (general--define-key-dispatch state keymap maps kargs))))))) ;; * Functions With Keyword Arguments ;;;###autoload (cl-defun general-define-key (&rest maps &key definer (states general-default-states) (keymaps general-default-keymaps keymaps-specified-p) (prefix general-default-prefix) (non-normal-prefix general-default-non-normal-prefix) (global-prefix general-default-global-prefix) infix prefix-command prefix-map prefix-name predicate ;; related to extended definitions package properties repeat jump major-modes (wk-match-keys t) (wk-match-binding t) (wk-full-keys t) ;; for custom key definers lispy-plist worf-plist &allow-other-keys) "The primary key definition function provided by general.el. Define MAPS, optionally using DEFINER, in the keymap(s) corresponding to STATES and KEYMAPS. MAPS consists of paired keys (vectors or strings; also see `general-implicit-kbd') and definitions (those mentioned in `define-key''s docstring and general.el's \"extended\" definitions). All pairs (when not ignored) will be recorded and can be later displayed with `general-describe-keybindings'. If DEFINER is specified, a custom key definer will be used to bind MAPS. See general.el's documentation/README for more information. Unlike with normal key definitions functions, the keymaps in KEYMAPS should be quoted (this allows using the keymap name for other purposes, e.g. deferring keybindings if the keymap symbol is not bound, optionally inferring the corresponding major mode for a symbol by removing \"-map\" for :which-key, easily storing the keymap name for use with `general-describe-keybindings', etc.). Note that general.el provides other key definer macros that do not require quoting keymaps. STATES corresponds to the evil state(s) to bind the keys in. Non-evil users should not set STATES. When STATES is non-nil, `evil-define-key*' will be used (the evil auxiliary keymaps corresponding STATES and KEYMAPS will be used); otherwise `define-key' will be used (unless DEFINER is specified). KEYMAPS defaults to 'global. There is also 'local, which create buffer-local keybindings for both evil and non-evil keybindings. There are other special, user-alterable \"shorthand\" symbols for keymaps and states (see `general-keymap-aliases' and `general-state-aliases'). Note that STATES and KEYMAPS can either be lists or single symbols. If any keymap does not exist, those keybindings will be deferred until the keymap does exist, so using `eval-after-load' is not necessary with this function. PREFIX corresponds to a key to prefix keys in MAPS with and defaults to none. To bind/unbind a key specified with PREFIX, \"\" can be specified as a key in MAPS (e.g. ...:prefix \"SPC\" \"\" nil... will unbind space). The keywords in this paragraph are only useful for evil users. If NON-NORMAL-PREFIX is specified, this prefix will be used instead of PREFIX for states in `general-non-normal-states' (e.g. the emacs and insert states). This argument will only have an effect if one of these states is in STATES or if corresponding global keymap (e.g. `evil-insert-state-map') is in KEYMAPS. Alternatively, GLOBAL-PREFIX can be used with PREFIX and/or NON-NORMAL-PREFIX to bind keys in all states under the specified prefix. Like with NON-NORMAL-PREFIX, GLOBAL-PREFIX will prevent PREFIX from applying to `general-non-normal-states'. INFIX can be used to append a string to all of the specified prefixes. This is potentially useful when you are using GLOBAL-PREFIX and/or NON-NORMAL-PREFIX so that you can sandwich keys in between all the prefixes and the specified keys in MAPS. This may be particularly useful if you are using default prefixes in a wrapper function/macro so that you can add to them without needing to re-specify all of them. If none of the other prefix keyword arguments are specified, INFIX will have no effect. If PREFIX-COMMAND or PREFIX-MAP is specified, a prefix command and/or keymap will be created. PREFIX-NAME can be additionally specified to set the keymap menu name/prompt. If PREFIX-COMMAND is specified, `define-prefix-command' will be used. Otherwise, only a prefix keymap will be created. Previously created prefix commands/keymaps will never be redefined/cleared. All prefixes (including the INFIX key, if specified) will then be bound to PREFIX-COMMAND or PREFIX-MAP. If the user did not specify any PREFIX or manually specify any KEYMAPS, general will bind all MAPS in the prefix keymap corresponding to either PREFIX-MAP or PREFIX-COMMAND instead of in the default keymap. PREDICATE corresponds to a predicate to check to determine whether a definition should be active (e.g. \":predicate '(eobp)\"). Definitions created with a predicate will only be active when the predicate is true. When the predicate is false, key lookup will continue to search for a match in lower-precedence keymaps. In addition to the normal definitions supported by `define-key', general.el also provides \"extended\" definitions, which are plists containing the normal definition as well as other keywords. For example, PREDICATE can be specified globally or locally in an extended definition. New global (~general-define-key~) and local (extended definition) keywords can be added by the user. See `general-extended-def-keywords' and general.el's documentation/README for more information. PACKAGE is the global version of the extended definition keyword that specifies the package a keymap is defined in (used for \"autoloading\" keymaps) PROPERTIES, REPEAT, and JUMP are the global versions of the extended definition keywords used for adding evil command properties to commands. MAJOR-MODES, WK-MATCH-KEYS, WK-MATCH-BINDINGS, and WK-FULL-KEYS are the corresponding global versions of which-key extended definition keywords. They will only have an effect for extended definitions that specify :which-key or :wk. See the section on extended definitions in the general.el documentation/README for more information. LISPY-PLIST and WORF-PLIST are the global versions of extended definition keywords that are used for each corresponding custom DEFINER." ;; to silence compiler warning; variables that are later extracted from kargs (ignore definer predicate package properties repeat jump major-modes lispy-plist worf-plist) (let ((prefix-def (or prefix-command (when prefix-map (list :keymap prefix-map)))) non-normal-prefix-maps global-prefix-maps kargs) ;; don't force the user to wrap a single state or keymap in a list (general--ensure-lists states keymaps) ;; unalias states and keymaps (setq states (mapcar (lambda (state) (general--unalias state t)) states)) (setq keymaps (mapcar #'general--unalias keymaps)) ;; remove keyword arguments from rest var (let ((split-maps (general--remove-keyword-args maps))) (setq maps (car split-maps) ;; order will be preserved; matters for duplicates kargs (append (list ;; should be included even if not manually specified ;; (because have non-nil defaults) :wk-match-keys wk-match-keys :wk-match-binding wk-match-binding :wk-full-keys wk-full-keys ;; so :keymaps and :states are always lists in kargs ;; needed for matching against :major-modes :keymaps keymaps ;; for consistency; may be useful in future or for user :states states) (cadr split-maps)))) (general--define-prefix prefix-command prefix-map prefix-name) (when (and (or prefix-map prefix-command) (not (or prefix keymaps-specified-p))) (setq keymaps (list (or prefix-map prefix-command)))) ;; TODO reduce code duplication here (when non-normal-prefix (setq non-normal-prefix-maps (general--apply-prefix-and-kbd (general--concat t non-normal-prefix infix) (append (when (and prefix prefix-def) (list "" prefix-def)) maps)))) (when global-prefix (setq global-prefix-maps (general--apply-prefix-and-kbd (general--concat t global-prefix infix) (append (when (and prefix prefix-def) (list "" prefix-def)) maps)))) ;; last so not applying prefix twice (setq maps (general--apply-prefix-and-kbd (general--concat t prefix infix) (append (when (and prefix prefix-def) (list "" prefix-def)) maps))) (dolist (keymap keymaps) (general--delay `(or (memq ',keymap '(local global)) (boundp ',keymap)) `(general--define-key ',states ',keymap ',maps ',non-normal-prefix-maps ',global-prefix-maps ',kargs) 'after-load-functions t nil (symbol-name (cl-gensym (format "general-define-key-in-%s" keymap))))))) ;;;###autoload (defmacro general-emacs-define-key (keymaps &rest args) "A wrapper for `general-define-key' that is similar to `define-key'. It has a positional argument for KEYMAPS (that will not be overridden by a later :keymaps argument). Besides this, it acts the same as `general-define-key', and ARGS can contain keyword arguments in addition to keybindings. This can basically act as a drop-in replacement for `define-key', and unlike with `general-define-key', KEYMAPS does not need to be quoted." (declare (indent 1)) `(general-define-key :keymaps ,(if (and (listp keymaps) (eq (car keymaps) 'quote)) `,keymaps `',keymaps) ,@args)) ;;;###autoload (defmacro general-evil-define-key (states keymaps &rest args) "A wrapper for `general-define-key' that is similar to `evil-define-key'. It has positional arguments for STATES and KEYMAPS (that will not be overridden by a later :keymaps or :states argument). Besides this, it acts the same as `general-define-key', and ARGS can contain keyword arguments in addition to keybindings. This can basically act as a drop-in replacement for `evil-define-key', and unlike with `general-define-key', KEYMAPS does not need to be quoted." (declare (indent 2)) `(general-define-key :states ,(if (and (listp states) (eq (car states) 'quote)) `,states `',states) :keymaps ,(if (and (listp keymaps) (eq (car keymaps) 'quote)) `,keymaps `',keymaps) ,@args)) (defun general--positional-arg-p (arg) "Return whether ARG is a positional argument for a key definer. Keyword arguments and strings/vectors are not considered positional arguments." (and arg (or (symbolp arg) (listp arg)) (not (keywordp arg)))) ;;;###autoload (defmacro general-def (&rest args) "General definer that takes a variable number of positional arguments in ARGS. This macro will act as `general-define-key', `general-emacs-define-key', or `general-evil-define-key' based on how many of the initial arguments do not correspond to keybindings. All quoted and non-quoted lists and symbols before the first string, vector, or keyword are considered to be positional arguments. This means that you cannot use a function or variable for a key that starts immediately after the positional arguments. If you need to do this, you should use one of the definers that `general-def' dispatches to or explicitly separate the positional arguments from the maps with a bogus keyword pair like \":start-maps t\"" (declare (indent defun)) (let ((pos-args 0)) (while (general--positional-arg-p (nth pos-args args)) (cl-incf pos-args)) (cl-case pos-args (0 `(general-define-key ,@args)) (1 `(general-emacs-define-key ,@args)) (2 `(general-evil-define-key ,@args))))) ;;;###autoload (cl-defmacro general-create-definer (name &rest defaults &key wrapping &allow-other-keys) "A helper macro to create wrappers for `general-def'. This can be used to create key definers that will use a certain keymap, evil state, prefix key, etc. by default. NAME is the wrapper name and DEFAULTS are the default arguments. WRAPPING can also be optionally specified to use a different definer than `general-def'. It should not be quoted." (declare (indent defun)) (let ((defaults (cl-loop for (key val) on defaults by 'cddr unless (eq key :wrapping) collect key and collect val)) (definer (or wrapping 'general-def))) `(defmacro ,name (&rest args) (declare (indent defun)) ,(let ((print-quoted t)) (format "A wrapper for `%s'. It has the following defaults: %s" definer defaults)) ;; can still override keywords afterwards (first keyword takes precedence) `(,',definer ,@args ,@',defaults)))) (defun general--starter-arg-p (arg) "Return whether ARG is a keyword or positional argument for a key definer." (or (keywordp arg) (general--positional-arg-p arg))) ;;;###autoload (defmacro general-defs (&rest args) "A wrapper that splits into multiple `general-def's. Each consecutive grouping of positional argument followed by keyword/argument pairs (having only one or the other is fine) marks the start of a new section. Each section corresponds to one use of `general-def'. This means that settings only apply to the keybindings that directly follow." (declare (indent defun) (debug [&rest sexp])) (let (arglists arglist) (while args (while (and args (general--starter-arg-p (car args))) (when (keywordp (car args)) (push (pop args) arglist)) (push (pop args) arglist)) (while (and args (not (general--starter-arg-p (car args)))) (push (pop args) arglist) (push (pop args) arglist)) (push (nreverse arglist) arglists) (setq arglist nil)) `(progn ,@(mapcar (lambda (arglist) (cons 'general-def arglist)) (nreverse arglists))))) ;;;###autoload (cl-defmacro general-unbind (&rest args) "A wrapper for `general-def' to unbind multiple keys simultaneously. Insert after all keys in ARGS before passing ARGS to `general-def.' \":with #'func\" can optionally specified to use a custom function instead (e.g. `ignore')." (declare (indent defun)) ;; Note: :with can be at an odd position, so must handle internally and not ;; with &key (let* (with (split-args (general--remove-keyword-args args)) (kargs (cl-loop for (key val) on (cadr split-args) by 'cddr if (eq key :with) do (setq with val) else collect key and collect val)) (positional-args-and-maps ;; interleave appropriate definitions into maps (cl-loop for key in (car split-args) collect key and unless (general--positional-arg-p key) collect (if (eq with t) nil with))) (args (append positional-args-and-maps kargs))) `(general-def ,@args))) ;; * Displaying Keybindings (defun general--to-string (x) "Convert key vector or symbol X to a string." (cond ((vectorp x) (key-description x)) ((symbolp x) (symbol-name x)) (t x))) ;; these sorting functions assume x != y (which will hold true for ;; `general-keybindings') (defun general--< (x y) "Return t if X is alphabetically less than Y. Each should be either a string, symbol, or vector. Nil is a special case and is considered the \"smallest\"." (cond ((null x) t) ((null y) nil) (t (setq x (general--to-string x) y (general--to-string y)) (string< x y)))) (defun general-sort-by-car (list) "Sort LIST by comparing the car of each element with `general--<'." (cl-sort list #'general--< :key #'car)) (defun general-sort-by-cadr (list) "Sort LIST by comparing the cadr of each element with `general--<'." (cl-sort list #'general--< :key #'cadr)) (defvar general-describe-evil-states '(nil insert emacs hybrid normal visual motion operator replace) "Ordered list of evil states used for `general--evil-state-<'.") (defun general--evil-state-< (x y) "Return t if evil state X should come before state Y. If X and Y are conses, the first element will be compared. Ordering is based on `general-describe-evil-states' or the symbol names for states not in the list." (let ((xind (cl-position x general-describe-evil-states)) (yind (cl-position y general-describe-evil-states))) (cond ((and (null xind) (null yind)) (general--< x y)) ((null xind) nil) ((null yind) t) (t (< xind yind))))) (defun general--sort-evil-state-conses (state-conses) "Sort STATE-CONSES using `general--evil-state-<'." (cl-sort state-conses #'general--evil-state-< :key #'car)) (defun general--print-map (map) "Print the keybinding MAP." (cl-destructuring-bind (key command previous) map (princ (format "|=%.50s=|~%.50s~|~%.50s~|\n" (replace-regexp-in-string "|" "¦" (key-description key)) command previous)))) (defun general--print-maps-table (maps) "Print an org table for MAPS." (when general-describe-keybinding-sort-function (setq maps (funcall general-describe-keybinding-sort-function maps))) (princ "|key|command|previous|\n|-+-|\n") (dolist (map maps) (general--print-map map)) (princ "\n")) (defun general--print-state-heading (state-cons) "Print a table and possibly a heading for STATE-CONS." (let ((state (car state-cons)) (maps (cdr state-cons))) (unless (null state) (princ (capitalize (concat "** " (symbol-name state) " State\n")))) (general--print-maps-table maps))) (defun general--print-keymap-heading (keymap-cons) "Print headings and tables for KEYMAP-CONS." (let ((keymap (car keymap-cons)) (state-conses (cdr keymap-cons))) (princ (capitalize (concat "* " (symbol-name keymap) " Keybindings\n"))) (when general-describe-state-sort-function (setq state-conses (funcall general-describe-state-sort-function state-conses))) (dolist (state-cons state-conses) (general--print-state-heading state-cons)))) (declare-function org-at-heading-p "org") (declare-function org-table-align "org-table") (declare-function outline-next-heading "outline") (defvar org-startup-folded) ;;;###autoload (defun general-describe-keybindings (&optional arg) "Show all keys that have been bound with general in an org buffer. Any local keybindings will be shown first followed by global keybindings. With a non-nil prefix ARG only show bindings in active maps." (interactive "P") (with-output-to-temp-buffer "*General Keybindings*" (let* ((keybindings (append (copy-alist general-keybindings) (list (cons 'local general-local-keybindings)))) (active-maps (current-active-maps))) ;; print prioritized keymaps first (if any) (dolist (keymap general-describe-priority-keymaps) (let ((keymap-cons (assq keymap keybindings))) (when (and keymap-cons (or (null arg) (and (boundp (car keymap-cons)) (memq (symbol-value (car keymap-cons)) active-maps)))) (general--print-keymap-heading keymap-cons) (setq keybindings (assq-delete-all keymap keybindings))))) ;; sort the remaining and then print them (when general-describe-keymap-sort-function (setq keybindings (funcall general-describe-keymap-sort-function keybindings))) (dolist (keymap-cons keybindings) (when (or (null arg) (and (boundp (car keymap-cons)) (memq (symbol-value (car keymap-cons)) active-maps))) (general--print-keymap-heading keymap-cons))))) (with-current-buffer "*General Keybindings*" (let ((org-startup-folded 'showall)) (org-mode)) (read-only-mode -1) (while (progn (while (progn (forward-line) (org-at-heading-p))) (org-table-align) (outline-next-heading))) (goto-char (point-min)) (read-only-mode))) ;; * Functions/Macros to Aid Key Definition ;; ** Helpers (cl-defun general--call-interactively (function &optional (remap t) record-flag keys) "Like `call-interactively' but use the remapped FUNCTION if it exists. If REMAP is specified as nil (it is true by default), this is the same as `call-interactively'. FUNCTION, RECORD-FLAG, and KEYS are passed to `call-interactively'." (when remap (setq function (or (key-binding (kbd (format " <%s>" function))) function))) (call-interactively function record-flag keys)) ;; ** Key Simulation ;; https://emacs.stackexchange.com/questions/6037/emacs-bind-key-to-prefix/13432#13432 ;; altered to ;; - allow execution in an arbitrary state and keymap ;; - create a named function with a docstring ;; - optionally dynamically lookup the key(s) up in the correct keymap to try to ;; match a command to execute instead ;; - handle more edge cases like correctly working with macros/repeating ;; TODO ;; - rename keys arguments to key for consistency with builtin functions (declare-function evil-change-state "evil-core") (defvar evil-no-display) (defvar evil-state) (defvar evil-previous-state) (defvar evil-previous-state-alist) (defvar evil-next-state) (defmacro general--save-state (&rest body) "Save the current state; execute BODY; restore the state. This is a combination of `evil-without-display' and `evil-save-state'. It is necessary to define this directly in general so that it is available when general is compiled (as evil is an optional dependency and may not be installed when general is compiled)." (declare (indent defun) (debug t)) `(let* ((evil-no-display t) (evil-state evil-state) (evil-previous-state evil-previous-state) (evil-previous-state-alist (copy-tree evil-previous-state-alist)) (evil-next-state evil-next-state) (old-state evil-state) (inhibit-quit t) (buf (current-buffer))) (unwind-protect (progn ,@body) (when (buffer-live-p buf) (with-current-buffer buf (evil-change-state old-state)))))) ;;;###autoload (cl-defmacro general-key (key &key state docstring accept-default no-remap position) "Act as KEY's definition in the current context. This uses an extended menu item's capability of dynamically computing a definition. It is recommended over `general-simulate-key' wherever possible. KEY should be a string given in `kbd' notation and should correspond to a single definition (as opposed to a sequence of commands). When STATE is specified, look up KEY with STATE as the current evil state. When specified, DOCSTRING will be the menu item's name/description. ACCEPT-DEFAULT, NO-REMAP, and POSITION are passed to `key-binding'." `'(menu-item ,(or docstring "") nil :filter (lambda (&optional _) ,(if state `(general--save-state (evil-change-state ,state) (key-binding (general--kbd ,key) ,accept-default ,no-remap ,position)) `(key-binding (general--kbd ,key) ,accept-default ,no-remap ,position))))) (defvar general--last-simulated-command nil "Holds the last simulated command (or nil for incomplete key sequence).") (defvar general--simulate-next-as-is nil "Whether to fake keys unconditionally in the next `general--simulate-keys'. This is used for testing (but could potentially be useful for a user). Since `general--simulate-keys' will normally assume it is being run inside a macro that was manually recorded, this is needed when executing a keyboard macro that ends up running `general--simulate-keys' for the first time.") (defvar general--simulate-as-is nil "Whether to fake the keys unconditionally in any `general--simulate-keys'.") (defun general--key-binding (keys &optional state keymap) "Look up KEYS in the keymap corresponding to STATE and/or KEYMAP. Continually check whether subsequences of KEYS are bound to a command or keymap starting with the full KEYS and ending when a match is found or no subsequences remain. Unlike `lookup-key' if KEYS is not matched, fall back to checking with `key-binding'. If STATE is specified and KEYMAP is not, temporarily switch to STATE to look up the keys (this means that keybindings inherited from a different evil state can still be detected). Return a list of the match and the leftover keys (or nil if the full KEYS was matched)." (let* ((keymap (when keymap (general--get-keymap state keymap))) (len (length keys)) (ind len) match) (while (and (> ind 0) (not match)) (let* ((key (substring keys 0 ind)) (result (cond (keymap (or (lookup-key keymap key) (key-binding key))) (state ;; this also works fine when evil-local-mode is off (general--save-state (evil-change-state state) (key-binding key))) (t (key-binding key))))) (if (or (commandp result) (keymapp result)) (setq match result) (cl-decf ind)))) (list match (if (= ind len) nil (substring keys ind len))))) (cl-defun general--simulate-keys (command keys &optional state keymap (lookup t) (remap t)) "Simulate COMMAND followed by KEYS in STATE and/or KEYMAP. If COMMAND is nil, just simulate KEYS. If STATE and KEYMAP are nil, simulate the keys in the current context. When COMMAND is non-nil, STATE and KEYMAP will have no effect. KEYS should be a string that can be passed to `kbd' or nil. If KEYS is nil, the COMMAND will just be called interactively. If COMMAND is nil and LOOKUP is non-nil, KEYS will be looked up in the correct context to determine if any subsequence corresponds to a command or keymap. If a command is matched, that command will be called followed by the simulation of any leftover keys. To simulate the keys as-is without any lookup, LOOKUP can be explicitly specified as nil. When COMMAND has been remapped (i.e. [remap COMMAND] is currently bound), the remapped version will be used instead of the original command unless REMAP is specified as nil (it is true by default)." (let* ((keys (when keys (general--kbd keys))) ;; TODO remove when get rid of `general-simulate-keys' (state (if (eq state t) 'emacs state))) (unless (or command (not lookup)) (cl-destructuring-bind (match leftover-keys) (general--key-binding keys state keymap) (cond ((commandp match) (setq command match keys leftover-keys)) ;; not documented because no current use case ;; left in because may be useful later ((and (eq lookup 'always) (keymapp match)) (setq keymap match state nil ;; should be nil keys leftover-keys))))) ;; set context for keys (when (and keymap (not command)) ;; TODO is it possible to set transient map and then use e.g. ;; `evil-execute-in-normal-state' (so that commands bound in the motion ;; state auxiliary map could also be executed)? (set-transient-map (general--get-keymap state keymap))) (when keys ;; only set prefix-arg when only keys ;; (otherwise will also affect the next command) (unless command (setq prefix-arg current-prefix-arg)) (when (or general--simulate-as-is general--simulate-next-as-is (not executing-kbd-macro)) (setq general--simulate-next-as-is nil) ;; keys are incorrectly saved as this-command-keys when recording macros ;; these keys will be played back, so don't simulate them (setq unread-command-events (nconc ;; force keys to be added to this-command-keys ;; this happens normally already for macros but it needs to be ;; forced for evil-repeat though, which will only include the ;; first key otherwise (ideally no keys would ever be added in ;; either case) (mapcar (lambda (ev) (cons t ev)) (listify-key-sequence keys)) unread-command-events)))) (when command (let ((this-command command)) (general--call-interactively command remap))) (setq general--last-simulated-command command))) ;;;###autoload (cl-defmacro general-simulate-keys (keys &optional state keymap (lookup t) docstring name) "Deprecated. Please use `general-simulate-key' instead." (let* ((command (when (listp keys) (car keys))) (keys (if (listp keys) (cadr keys) keys)) (state (if (eq state t) ''emacs state)) (name (or name (intern (concat (format "general-simulate-%s" (if command (eval command) "")) (when command "-") (replace-regexp-in-string " " "_" keys) (when state (concat "-in-" (symbol-name (eval state)) "-state")) (when keymap (concat "-in-" (symbol-name keymap)))))))) `(progn (eval-after-load 'evil '(evil-set-command-property #',name :repeat 'general--simulate-repeat)) (defun ,name () ,(or docstring (concat "Simulate " (when command (concat "`" (symbol-name (eval command)) "' then ")) "'" keys "' in " (cond ((and state keymap) (concat (symbol-name (eval state)) " state in `" (symbol-name keymap) "'.")) (keymap (concat (symbol-name keymap) ".")) (state (concat (symbol-name (eval state)) " state.")) (t "the current context.")))) (interactive) (general--simulate-keys ,command ,keys ,state ,keymap ,lookup))))) (make-obsolete 'general-simulate-keys 'general-simulate-key "2018-01-14") ;;;###autoload (cl-defmacro general-simulate-key (keys &key state keymap name docstring (lookup t) which-key (remap t)) "Create and return a command that simulates KEYS in STATE and KEYMAP. KEYS should be a string given in `kbd' notation. It can also be a list of a single command followed by a string of the key(s) to simulate after calling that command. STATE should only be specified by evil users and should be a quoted evil state. KEYMAP should not be quoted. Both STATE and KEYMAP aliases are supported (but they have to be set when the macro is expanded). When neither STATE or KEYMAP are specified, the key(s) will be simulated in the current context. If NAME is specified, it will replace the automatically generated function name. NAME should not be quoted. If DOCSTRING is specified, it will replace the automatically generated docstring. Normally the generated function will look up KEY in the correct context to try to match a command. To prevent this lookup, LOOKUP can be specified as nil. Generally, you will want to keep LOOKUP non-nil because this will allow checking the evil repeat property of matched commands to determine whether or not they should be recorded. See the docstring for `general--simulate-keys' for more information about LOOKUP. When a WHICH-KEY description is specified, it will replace the command name in the which-key popup. When a command name is specified and that command has been remapped (i.e. [remap command] is currently bound), the remapped version will be used instead of the original command unless REMAP is specified as nil (it is true by default). The advantages of this over a keyboard macro are as follows: - Prefix arguments are supported - The user can control the context in which the keys are simulated - The user can simulate both a named command and keys - The user can simulate an incomplete key sequence (e.g. for a keymap)" (declare (indent defun)) (let* ((command (when (listp keys) (car keys))) (keys (if (listp keys) (cadr keys) keys)) (state (general--unalias (eval state) t)) (keymap (general--unalias keymap)) (name (or name (intern (concat (format "general-simulate-%s" (if command (eval command) "")) (when command "-") (replace-regexp-in-string " " "_" keys) (when state (concat "-in-" (symbol-name state) "-state")) (when keymap (concat "-in-" (symbol-name keymap)))))))) `(progn (eval-after-load 'evil '(evil-set-command-property #',name :repeat 'general--simulate-repeat)) (when ,which-key (general-with-eval-after-load 'which-key (push '((nil . ,(symbol-name name)) nil . ,which-key) which-key-replacement-alist))) (defun ,name () ,(or docstring (concat "Simulate " (when command (concat "`" (symbol-name (eval command)) "' then ")) "'" keys "' in " (cond ((and state keymap) (concat (symbol-name state) " state in `" (symbol-name keymap) "'.")) (keymap (concat (symbol-name keymap) ".")) (state (concat (symbol-name state) " state.")) (t "the current context.")))) (interactive) (general--simulate-keys ,command ,keys ',state ,keymap ,lookup ,remap)) #',name))) (defun general--repeat-abort-p (repeat-prop) "Return t if repeat recording should be aborted based on REPEAT-PROP." (or (memq repeat-prop (list nil 'abort 'ignore)) (and (eq repeat-prop 'motion) (not (memq evil-state '(insert replace)))))) (declare-function evil-repeat-record "evil-repeat") (declare-function evil-get-command-property "evil-common") (declare-function evil-repeat-abort "evil-repeat") (declare-function evil-this-command-keys "evil-repeat") (declare-function evil-clear-command-keys "evil-repeat") (defvar evil-this-register) (defun general--simulate-repeat (flag) "Modified version of `evil-repeat-keystrokes'. It behaves as normal but will check the repeat property of a simulated command to determine whether to abort recording." (cond ((eq flag 'pre) (when evil-this-register (evil-repeat-record `(set evil-this-register ,evil-this-register)))) ((eq flag 'post) (let* ((command general--last-simulated-command) (repeat-prop (evil-get-command-property command :repeat t))) (if (and command (general--repeat-abort-p repeat-prop)) (evil-repeat-abort) (evil-repeat-record (evil-this-command-keys t)) (evil-clear-command-keys)))))) ;; ** Key Dispatch (defvar general--last-dispatch-command nil "Holds the last command run from a `general-key-dispatch' function.") (defun general--extend-key-sequence (keys) "Read a key and append it to KEYS. KEYS should be a string given in `kbd' notation." (let ((key (read-event))) (concat keys (when keys " ") (key-description (if (characterp key) (char-to-string key) (vector key)))))) ;;;###autoload (cl-defmacro general-key-dispatch (fallback-command &rest maps &key timeout inherit-keymap name docstring which-key (remap t) &allow-other-keys) "Create and return a command that runs FALLBACK-COMMAND or a command in MAPS. MAPS consists of pairs. If a key in MAPS is matched, the corresponding command will be run. Otherwise FALLBACK-COMMAND will be run with the unmatched keys. So, for example, if \"ab\" was pressed, and \"ab\" is not one of the key sequences from MAPS, the FALLBACK-COMMAND will be run followed by the simulated keypresses of \"ab\". Prefix arguments will still work regardless of which command is run. This is useful for binding under non-prefix keys. For example, this can be used to redefine a sequence like \"cw\" or \"cow\" in evil but still have \"c\" work as `evil-change'. If TIMEOUT is specified, FALLBACK-COMMAND will also be run in the case that the user does not press the next key within the TIMEOUT (e.g. 0.5). NAME and DOCSTRING are optional keyword arguments. They can be used to replace the automatically generated name and docstring for the created function. By default, `cl-gensym' is used to prevent name clashes (e.g. allows the user to create multiple different commands using `self-insert-command' as the FALLBACK-COMMAND without explicitly specifying NAME to manually prevent clashes). When INHERIT-KEYMAP is specified, all the keybindings from that keymap will be inherited in MAPS. When a WHICH-KEY description is specified, it will replace the command name in the which-key popup. When command to be executed has been remapped (i.e. [remap command] is currently bound), the remapped version will be used instead of the original command unless REMAP is specified as nil (it is true by default)." (declare (indent 1)) (let ((name (or name (cl-gensym (format "general-dispatch-%s-" (eval fallback-command))))) ;; remove keyword arguments from maps (maps (car (general--remove-keyword-args maps)))) `(progn (eval-after-load 'evil '(evil-set-command-property #',name :repeat 'general--dispatch-repeat)) (when ,which-key (general-with-eval-after-load 'which-key (push '((nil . ,(symbol-name name)) nil . ,which-key) which-key-replacement-alist))) ;; TODO list all of the bound keys in the docstring (defun ,name () ,(or docstring (format (concat "Run %s or something else based" "on the next keypresses.") (eval fallback-command))) (interactive) (let ((map (make-sparse-keymap)) (maps (list ,@maps)) (invoked-keys (this-command-keys)) (timeout ,timeout) (inherit-keymap ,inherit-keymap) matched-command fallback char timed-out-p) (when inherit-keymap (set-keymap-parent map inherit-keymap)) (while maps (define-key map (general--kbd (pop maps)) (pop maps))) (while (progn (if timeout (with-timeout (timeout (setq timed-out-p t)) ;; TODO rename char (setq char (general--extend-key-sequence char))) (setq char (general--extend-key-sequence char))) (and (not timed-out-p) (keymapp (lookup-key map (kbd char)))))) (cond ((and (not timed-out-p) (setq matched-command (lookup-key map (kbd char)))) ;; necessary for evil-this-operator checks because ;; evil-define-operator sets evil-this-operator to this-command (let ((this-command matched-command)) (general--call-interactively matched-command ,remap))) (t (setq matched-command ,fallback-command) (general--simulate-keys ,fallback-command char nil nil t ,remap))) (setq general--last-dispatch-command matched-command))) #',name))) (defun general--dispatch-repeat (flag) "Modified version of `evil-repeat-keystrokes'. It behaves as normal but will check the repeat property of a simulated command to determine whether to abort recording." (cond ((eq flag 'pre) (when evil-this-register (evil-repeat-record `(set evil-this-register ,evil-this-register)))) ((eq flag 'post) (let ((repeat-prop (evil-get-command-property general--last-dispatch-command :repeat t))) (if (general--repeat-abort-p repeat-prop) (evil-repeat-abort) (evil-repeat-record (evil-this-command-keys t)) (evil-clear-command-keys)))))) ;; ** Predicate Dispatch ;;;###autoload (cl-defmacro general-predicate-dispatch (fallback-def &rest defs &key docstring &allow-other-keys) (declare (indent 1)) "Create a menu item that will run FALLBACK-DEF or a definition from DEFS. DEFS consists of pairs. Binding this menu-item to a key will cause that key to act as the corresponding definition (a command, keymap, etc) for the first matched predicate. If no predicate is matched FALLBACK-DEF will be run. When FALLBACK-DEF is nil and no predicates are matched, the keymap with the next highest precedence for the pressed key will be checked. DOCSTRING can be specified as a description for the menu item." ;; remove keyword arguments from defs and sort defs into pairs (let ((defs (cl-loop for (key value) on defs by 'cddr unless (keywordp key) collect (list key value)))) `'(menu-item ,(or docstring "") nil :filter (lambda (&optional _) (cond ,@(mapcar (lambda (pred-def) `(,(car pred-def) ,(cadr pred-def))) defs) (t ,fallback-def)))))) ;; ** Key "Translation" ;;;###autoload (cl-defun general-translate-key (states keymaps &rest maps &key destructive &allow-other-keys) "Translate keys in the keymap(s) corresponding to STATES and KEYMAPS. STATES should be the name of an evil state, a list of states, or nil. KEYMAPS should be a symbol corresponding to the keymap to make the translations in or a list of keymap names. Keymap and state aliases are supported (as well as 'local and 'global for KEYMAPS). MAPS corresponds to a list of translations (key replacement pairs). For example, specifying \"a\" \"b\" will bind \"a\" to \"b\"'s definition in the keymap. If DESTRUCTIVE is non-nil, the keymap will be destructively altered without creating a backup. If DESTRUCTIVE is nil, a backup of the keymap will be stored on the initial invocation, and future invocations will always look up keys in the backup keymap. On the other hand, if DESTRUCTIVE is non-nil, calling this function multiple times with \"a\" \"b\" \"b\" \"a\", for example, would continue to swap and unswap the definitions of these keys. This means that when DESTRUCTIVE is non-nil, all related swaps/cycles should be done in the same invocation." (declare (indent defun)) (general--ensure-lists states keymaps) (dolist (keymap-name keymaps) (dolist (state states) (setq keymap-name (general--unalias keymap-name) state (general--unalias state t)) (let* ((keymap (general--get-keymap state keymap-name)) (backup-keymap (intern (format "general-%s%s-backup-map" keymap-name (if state (format "-%s-state" state) "")))) (lookup-keymap (if (and (not destructive) (boundp backup-keymap)) (symbol-value backup-keymap) (copy-keymap keymap))) (maps (cl-loop for (key replacement) on maps by 'cddr ;; :destructive can be in MAPS unless (keywordp key) collect (general--kbd key) and collect (lookup-key lookup-keymap (general--kbd replacement))))) (unless (or destructive (boundp backup-keymap)) (set backup-keymap lookup-keymap)) (apply #'general-define-key :states state :keymaps keymap-name maps))))) ;;;###autoload (defmacro general-swap-key (states keymaps &rest args) "Wrapper around `general-translate-key' for swapping keys. STATES, KEYMAPS, and ARGS are passed to `general-translate-key'. ARGS should consist of key swaps (e.g. \"a\" \"b\" is equivalent to \"a\" \"b\" \"b\" \"a\" with `general-translate-key') and optionally keyword arguments for `general-translate-key'." (declare (indent defun)) (setq args (cl-loop for (key replacement) on args by 'cddr collect key and collect replacement and unless (keywordp key) collect replacement and collect key)) `(general-translate-key ,states ,keymaps ,@args)) ;; ** Automatic Key Unbinding (defun general-unbind-non-prefix-key (define-key keymap key def) "Use DEFINE-KEY in KEYMAP to unbind an existing non-prefix subsequence of KEY. When a general key definer is in use and a subsequnece of KEY is already bound in KEYMAP, unbind it using DEFINE-KEY. Always bind KEY to DEF using DEFINE-KEY." (when general--definer-p (let ((key (if (stringp key) (string-to-vector key) key))) (while (numberp (lookup-key keymap key)) (setq key (cl-subseq key 0 -1))) (funcall define-key keymap key nil))) (funcall define-key keymap key def)) ;;;###autoload (defun general-auto-unbind-keys (&optional undo) "Advise `define-key' to automatically unbind keys when necessary. This will prevent errors when a sub-sequence of a key is already bound (e.g. the user attempts to bind \"SPC a\" when \"SPC\" is bound, resulting in a \"Key sequnce starts with non-prefix key\" error). When UNDO is non-nil, remove advice." (if undo ;; using general versions in case start recording advice for later display (general-advice-remove 'define-key #'general-unbind-non-prefix-key) (general-advice-add 'define-key :around #'general-unbind-non-prefix-key))) ;; ** Interactive Lambdas (defmacro general-lambda (&rest body) "Wrap BODY in an interactive lamba" `(lambda () (interactive) ,@body)) (defalias 'general-l #'general-lambda) ;; * Functions/Macros to Aid Other Configuration ;; ** Settings (defmacro general-setq (&rest settings) "A stripped-down `customize-set-variable' with the syntax of `setq'. Like `setq', multiple variables can be set at once; SETTINGS should consist of variable value pairs. Some variables have a custom setter (specified with `defcustom' and :set) that is used to run code necessary for changes to take effect (e.g. `auto-revert-interval'). If a package has already been loaded, and the user uses `setq' to set one of these variables, the :set code will not run (e.g. in the case of `auto-revert-interval', the timer will not be updated). Like with `customize-set-variable', `general-setq' will use the custom :set setter when necessary. If the package defining the variable has not yet been loaded, the custom setter will not be known, but it will still be run upon loading the package. Unlike `customize-set-variable', `general-setq' does not attempt to load any dependencies for the variable and does not support giving variables comments." `(progn ,@(cl-loop for (var val) on settings by 'cddr collect `(funcall (or (get ',var 'custom-set) #'set-default) ',var ,val)))) ;; ** Hooks ;;;###autoload (defun general-add-hook (hooks functions &optional append local) "A drop-in replacement for `add-hook'. Unlike `add-hook', HOOKS and FUNCTIONS can be single items or lists. APPEND and LOCAL are passed directly to `add-hook'." (general--ensure-lists hooks functions) (dolist (hook hooks) (dolist (func functions) (add-hook hook func append local)))) ;;;###autoload (defun general-remove-hook (hooks functions &optional local) "A drop-in replacement for `remove-hook'. Unlike `remove-hook', HOOKS and FUNCTIONS can be single items or lists. LOCAL is passed directly to `remove-hook'." (general--ensure-lists hooks functions) (dolist (hook hooks) (dolist (func functions) (remove-hook hook func local)))) ;; ** Advice ;;;###autoload (defun general-advice-add (symbols where functions &optional props) "A drop-in replacement for `advice-add'. SYMBOLS, WHERE, FUNCTIONS, and PROPS correspond to the arguments for `advice-add'. Unlike `advice-add', SYMBOLS and FUNCTIONS can be single items or lists." (general--ensure-lists symbols functions) (dolist (symbol symbols) (dolist (func functions) (advice-add symbol where func props)))) ;; specify full autoload to prevent function indirection (autoload generation ;; will put a /flipped/ defalias into the autoloads file causing an infinite ;; loop) ;;;###autoload (autoload 'general-add-advice "general") (defalias 'general-add-advice #'general-advice-add) ;;;###autoload (defun general-advice-remove (symbols functions) "A drop-in replacement for `advice-remove'. Unlike `advice-remove', SYMBOLS and FUNCTIONS can be single items or lists." (general--ensure-lists symbols functions) (dolist (symbol symbols) (dolist (func functions) (advice-remove symbol func)))) ;;;###autoload (autoload 'general-remove-advice "general") (defalias 'general-remove-advice #'general-advice-remove) ;; * Optional Setup ;;;###autoload (defun general-evil-setup (&optional short-names _) "Set up some basic equivalents for vim mapping functions. This creates global key definition functions for the evil states. Specifying SHORT-NAMES as non-nil will create non-prefixed function aliases such as `nmap' for `general-nmap'." (general-create-definer general-imap :states 'insert) (general-create-definer general-emap :states 'emacs) (general-create-definer general-nmap :states 'normal) (general-create-definer general-vmap :states 'visual) (general-create-definer general-mmap :states 'motion) (general-create-definer general-omap :states 'operator) (general-create-definer general-rmap :states 'replace) (general-create-definer general-iemap :states '(insert emacs)) (general-create-definer general-nvmap :states '(normal visual)) ;; these two don't have corresponding states (general-create-definer general-itomap :keymaps 'evil-inner-text-objects-map) (general-create-definer general-otomap :keymaps 'evil-outer-text-objects-map) (general-create-definer general-tomap :keymaps '(evil-outer-text-objects-map evil-inner-text-objects-map)) (when short-names (defalias 'imap #'general-imap) (defalias 'emap #'general-emap) (defalias 'nmap #'general-nmap) (defalias 'vmap #'general-vmap) (defalias 'mmap #'general-mmap) (defalias 'omap #'general-omap) (defalias 'rmap #'general-rmap) (defalias 'iemap #'general-iemap) (defalias 'nvmap #'general-nvmap) (defalias 'itomap #'general-itomap) (defalias 'otomap #'general-otomap) (defalias 'tomap #'general-tomap))) ;; * Use-package Integration ;; maybe useful for something else in future (defun general--extract-autoloadable-symbol (def) "Extract an autoloadable symbol from DEF, a normal or extended definition. This will also correctly extract the definition from a cons of the form (STRING . DEFN). If the extracted definition is nil, a string, a lambda, a keymap symbol from an extended definition, or some other definition that cannot be autoloaded, return nil." ;; explicit null checks not required because nil return value means no def (when (general--extended-def-p def) ;; extract definition (let ((first (car def))) (setq def (if (keywordp first) (plist-get def :def) first)))) (cond ((symbolp def) def) ((and (consp def) (symbolp (cdr def))) (cdr def)))) (general-with-eval-after-load 'use-package-core (declare-function use-package-concat "use-package") (declare-function use-package-process-keywords "use-package") (defvar use-package-keywords) (defvar use-package-deferring-keywords) ;; ** :general Keyword (setq use-package-keywords ;; should go in the same location as :bind ;; adding to end may not cause problems, but see issue #22 (cl-loop for item in use-package-keywords if (eq item :bind-keymap*) collect :bind-keymap* and collect :general else ;; don't add duplicates unless (eq item :general) collect item)) ;; altered args will be passed to the autoloads and handler functions (defun use-package-normalize/:general (_name _keyword general-arglists) "Return a plist containing the original ARGLISTS and autoloadable symbols." (let* ((sanitized-arglist ;; combine arglists into one without function names or ;; positional arguments (let (result) (dolist (arglist general-arglists result) (while (general--positional-arg-p (car arglist)) (setq arglist (cdr arglist))) (setq result (append result arglist))))) (commands (cl-loop for (key def) on sanitized-arglist by 'cddr when (and (not (keywordp key)) (not (null def)) (ignore-errors ;; TODO use cdr instead if possible (setq def (eval def)) (setq def (general--extract-autoloadable-symbol def)))) collect def))) (list :arglists general-arglists :commands commands))) (defun use-package-autoloads/:general (_name _keyword args) "Return an alist of commands extracted from ARGS. Return something like '((some-command-to-autoload . command) ...)." (mapcar (lambda (command) (cons command 'command)) (plist-get args :commands))) (defun use-package-handler/:general (name _keyword args rest state) "Use-package handler for :general." (use-package-concat (use-package-process-keywords name rest state) `(,@(mapcar (lambda (arglist) ;; Note: prefix commands are not valid functions (if (or (functionp (car arglist)) (macrop (car arglist))) `(,@arglist :package ',name) `(general-def ,@arglist :package ',name))) (plist-get args :arglists))))) ;; ** :ghook and :gfhook Keyword (setq use-package-keywords ;; should go in the same location as :bind ;; adding to end may not cause problems, but see issue #22 (cl-loop for item in use-package-keywords if (eq item :hook) collect :hook and collect :ghook and collect :gfhook else ;; don't add duplicates unless (memq item '(:ghook :gfhook)) collect item)) (defun general-normalize-hook-arglist (arglist mode-enable mode-hook &optional symbol-is-function-p) "Rewrite a :g(f)hook ARGLIST to a `general-add-hook' arglist. MODE-ENABLE is the inferred command to enable the package's mode, and MODE-HOOK is the mode inferred hook to enable the package's mode. When ARGLIST is a symbol instead of a list, it will be considered to be a hook name unless SYMBOL-IS-FUNCTION-P is non-nil, in which case it will considered to be a function." ;; standalone symbols are quoted automatically; unquote (when (ignore-errors (memq (car arglist) (list 'quote 'function))) (setq arglist (cadr arglist))) (cond ((listp arglist) ;; necessary to extract commands because they could be stored in a ;; variable or returned by a macro/function ;; e.g. (list #'func1 #'func2) needs to be evaluated (setq arglist (mapcar (lambda (arg) (eval arg)) arglist)) (if (= (length arglist) 1) ;; #'-mode (append arglist (list mode-enable)) (let ((hooks (car arglist)) (functions (cadr arglist))) (when (or (null hooks) (not (or (symbolp hooks) (listp hooks)))) (setq hooks mode-hook)) (when (or (null functions) (not (or (symbolp functions) (listp functions)))) (setq functions mode-enable)) (cons hooks (cons functions (cddr arglist)))))) (t (if symbol-is-function-p ;; '-mode-hook (list mode-hook arglist) ;; #'-mode (list arglist mode-enable))))) ;; altered args will be passed to the autoloads and handler functions (defun general-normalize-hook (name _keyword args &optional gfhookp) "Return a plist containing arglists and autoloadable commands. Transform ARGS into arglists suitable for `general-add-hook'." (let* ((mode (if (string-match-p "mode\\'" (symbol-name name)) name (intern (format "%s-mode" name)))) (mode-hook (intern (format "%s-hook" mode)))) (cl-loop for arg in args collect (general-normalize-hook-arglist arg mode mode-hook gfhookp)))) (defalias 'use-package-normalize/:ghook #'general-normalize-hook) (defun use-package-autoloads/:ghook (_name _keyword arglists) "Return an alist of commands extracted from ARGLISTS. Return something like '((some-command-to-autoload . command) ...)." (let ((commands (cl-loop for (_ functions) in arglists if (symbolp functions) collect functions else unless (functionp functions) append (cl-loop for function in functions when (symbolp function) collect function)))) (mapcar (lambda (command) (cons command 'command)) commands))) (defun use-package-handler/:ghook (name _keyword arglists rest state) "Use-package handler for :ghook and :gfhook." (use-package-concat (use-package-process-keywords name rest state) `(,@(mapcar (lambda (arglist) arglist ;; requote (unfortunately need to evaluate in normalizer) `(general-add-hook ,@(mapcar (lambda (x) `',x) arglist))) arglists)))) (defun use-package-normalize/:gfhook (name keyword args) "Use-package normalizer for :gfhook." (general-normalize-hook name keyword args t)) (defalias 'use-package-handler/:gfhook #'use-package-handler/:ghook)) ;; * Key-chord "Integration" (defun general-chord (keys) "Rewrite the string KEYS into a valid key-chord vector." ;; taken straight from key-chord.el (if (/= 2 (length keys)) (error "Key-chord keys must have two elements")) ;; Exotic chars in a string are >255 but define-key wants 128..255 for those (let ((key1 (logand 255 (aref keys 0))) (key2 (logand 255 (aref keys 1)))) (vector 'key-chord key1 key2))) (provide 'general) ;;; general.el ends here