├── familiar.el └── rationale.org /familiar.el: -------------------------------------------------------------------------------- 1 | ;;; familiar.el --- A more convenient key definer -*- lexical-binding: t; -*- 2 | 3 | ;; Author: Fox Kiester 4 | ;; URL: https://github.com/emacs-magus/familiar 5 | ;; Created: May 02, 2020 6 | ;; Keywords: convenience keybindings keys config startup dotemacs 7 | ;; Package-Requires: ((emacs "26.1")) 8 | ;; Version: 0.1.0 9 | 10 | ;; This file is not part of GNU Emacs. 11 | 12 | ;; This program is free software: you can redistribute it and/or modify 13 | ;; it under the terms of the GNU General Public License as published by 14 | ;; the Free Software Foundation, either version 3 of the License, or 15 | ;; (at your option) any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; You should have received a copy of the GNU General Public License 23 | ;; along with this program. If not, see . 24 | 25 | ;;; Commentary: 26 | ;; 27 | ;; A more convenient key definer. 28 | ;; 29 | 30 | ;; For more information see the README in the online repository. 31 | 32 | ;;; Code: 33 | (require 'familiar-usul) 34 | 35 | (defcustom familiar-dwim nil 36 | "Whether `familiar' will allow guessing which arguments are positional. 37 | If nil, : or a keyword is required to separate any positional arguments from 38 | keybindings. If non-nil, symbols and lists (quoted or unquoted) will be 39 | considered positional arguments, and the start of the keybindings should be 40 | marked by a string/vector key." 41 | :type 'boolean) 42 | 43 | (defun familiar--positional-arg-p (arg) 44 | "Return whether ARG is a positional argument for a key definer. 45 | Symbols and lists (quoted or unquoted) are considered positional arguments. 46 | Keyword arguments and strings/vectors are not considered positional arguments." 47 | (and arg 48 | (not (keywordp arg)) 49 | (or (not familiar-dwim) 50 | (or (symbolp arg) (listp arg))))) 51 | 52 | (defun familiar--normalize-positional-arg (arg) 53 | "Return ARG but quoted if it is not quoted." 54 | (if (eq (car-safe arg) 'quote) 55 | arg 56 | `',arg)) 57 | 58 | (defun familiar--default-parse-positional-argument (positional-args) 59 | "If POSITIONAL-ARGS convert it to a plist that sets :keymaps to the value." 60 | (when (= (length positional-args) 1) 61 | (cons :keymaps positional-args))) 62 | 63 | (defvar familiar--positional-argument-parsers 64 | (list #'familiar--default-parse-positional-argument) 65 | "List of positional argument parsers for `familiar'. 66 | Each one will be called with the list of positional arguments as a single 67 | argument until one returns non-nil. The return value should be a list of 68 | keyword arguments.") 69 | 70 | (defun familiar--convert-positional-args-to-kargs (positional-args) 71 | "Convert POSITIONAL-ARGS to a list of keyword arguments. 72 | Try each parser in `familiar--positional-argument-parsers' until one returns 73 | non-nil." 74 | (cl-dolist (parser familiar--positional-argument-parsers) 75 | (when-let ((kargs (funcall parser positional-args))) 76 | (cl-return kargs)))) 77 | 78 | (defun familiar--parse-arglist (args) 79 | "Parse ARGS to form a single `familiar--define-key' arglist. 80 | Return (list ). The remaining args will be a 81 | list of unprocessed arguments that correspond to 1+ separate 82 | `familiar--define-key' arglists." 83 | (let (arg 84 | positional-args 85 | kargs 86 | keybinds 87 | all-extended) 88 | (while (familiar--positional-arg-p (car args)) 89 | (push (familiar--normalize-positional-arg (pop args)) positional-args)) 90 | 91 | (when positional-args 92 | (setq kargs 93 | (nreverse 94 | (familiar--convert-positional-args-to-kargs positional-args)))) 95 | 96 | (when (eq (car args) :) 97 | (pop args)) 98 | (while (and (keywordp (car args)) 99 | (not (eq (car args) :))) 100 | (push (pop args) kargs) 101 | (push (pop args) kargs)) 102 | 103 | (while (and (setq arg (car args)) 104 | (or (not (keywordp arg)) 105 | (eq arg :ext))) 106 | (let ((key (pop args)) 107 | (val (if all-extended 108 | (car args) 109 | (pop args)))) 110 | (cond ((and (eq key :ext) 111 | (memq val '(t nil))) 112 | (setq all-extended val)) 113 | ((eq key :ext) 114 | (push (cons 'list val) keybinds)) 115 | (t 116 | (if all-extended 117 | (push (cons 'list key) keybinds) 118 | (push (list 'list key val) keybinds)))))) 119 | 120 | (list (nreverse kargs) (nreverse keybinds) args))) 121 | 122 | (defun familiar--merge-plists (a b) 123 | "Return the result of merging the plists A and B. 124 | When the same property exists in both A and B, prefer A's value." 125 | (let ((res (cl-copy-list a))) 126 | (cl-loop for (prop val) on b by #'cddr 127 | unless (plist-get res prop) 128 | do (setq res (plist-put res prop val))) 129 | res)) 130 | 131 | (defun familiar--parse-combined-arglists (arglist) 132 | "Parse the unevaluated ARGLIST into multiple `familiar--define-key' arglists." 133 | (let (parsed-arglists 134 | accumulated-kargs) 135 | (while arglist 136 | (let ((next-parsed-arglist (familiar--parse-arglist arglist))) 137 | (setq accumulated-kargs 138 | (familiar--merge-plists (car next-parsed-arglist) 139 | accumulated-kargs)) 140 | ;; can specify default keywords and no keybinds, ignore if no keybinds 141 | (when-let ((keybinds (cadr next-parsed-arglist))) 142 | (push (cons (cons 'list accumulated-kargs) 143 | keybinds) 144 | parsed-arglists)) 145 | (setq arglist (cl-caddr next-parsed-arglist)) 146 | (cl-case (car arglist) 147 | (: (pop arglist)) 148 | (:: 149 | (pop arglist) 150 | (setq accumulated-kargs nil))))) 151 | (nreverse parsed-arglists))) 152 | 153 | ;;;###autoload 154 | (defmacro familiar (&rest args) 155 | (declare (indent defun)) 156 | (let ((parsed-arglists (familiar--parse-combined-arglists args))) 157 | `(progn 158 | ,@(cl-loop for arglist in parsed-arglists 159 | collect `(familiar--define-key ,@arglist))))) 160 | 161 | ;;;###autoload 162 | (defmacro familiar-create-definer (name &rest defaults) 163 | "Create a key definer over `familiar' called NAME. 164 | DEFAULTS should be default keyword arguments for the definer (e.g. a default 165 | keymap or prefix)." 166 | (declare (indent defun)) 167 | `(defmacro ,name (&rest args) 168 | (declare (indent defun)) 169 | ,(let ((print-quoted t)) 170 | (format 171 | "A wrapper to run `familiar' with ARGS. 172 | 173 | It has the following defaults: 174 | %s" 175 | defaults)) 176 | `(familiar : ,@',defaults : ,@args))) 177 | 178 | (provide 'familiar) 179 | ;;; familiar.el ends here 180 | -------------------------------------------------------------------------------- /rationale.org: -------------------------------------------------------------------------------- 1 | This document describes some problems with general.el's syntax and solutions to fixing them in familiar.el. 2 | 3 | * Choosing the Syntax of the Internal Base Definer 4 | The internal base definer should be unambiguous (no guesswork to determine what is a positional argument like ~general-def~ did) and easy to parse.. Using keyword arguments and a list for every keybinding seems simplest: 5 | #+begin_src emacs-lisp 6 | ;; provide keyword arguments as first argument so no extra parsing is needed 7 | (familiar--define-key (list :keymaps 'foo-map) 8 | (list "a" #'a) 9 | (list "b" #'b :extended-def-keyword t) 10 | ;; all positional arguments but the last can be keys 11 | ;; this handles the case where it is possible for a key to be a list of 12 | ;; strings in a future version of Emacs 13 | (list "key1" "key2" #'c :extend-def-keyword t)) 14 | #+end_src 15 | 16 | Basically there is a global plist of settings (or defaults) for all keybindings and a local plist of settings for each keybinding. 17 | 18 | * Choosing the Syntax of the Main User-facing Definer 19 | Goals: 20 | - Be as simple as possible 21 | - Maintain full compatibility with ~define-key~ for the KEY and DEF pairs 22 | - Preferably maintain some backwards-compatibility with the argument lists of ~define-key~, ~evil-define-key~ (though separate replacements can be provided instead as an alternative) 23 | - Involve minimal or no guesswork. ~general-def~ guesses if the first arguments are keymaps, states, or keys. It cannot tell the difference between a keymap and a variable that holds a key, which can cause issues. 24 | 25 | ** Choosing the Argument Structure (Positional vs. Keyword) 26 | *** Option 1: Just Use Keywords Like the Underlying Function 27 | Problem: This is unnecessarily verbose. A keymap is usually specified (or a keymap and state for evil), so it makes sense for these to be positional arguments. 28 | 29 | *** Option 2: Have Different Functions With Different Positional Arguments 30 | General.el had these (e.g. ~general-evil-define-key~). The flexibility of being able to use a single shortly named function for everything like ~general-def~ is nice though. It would be simpler and less confusing to only use this single definer throughout the documentation. 31 | 32 | *** Option 3: Use One Positional Argument 33 | The definer could have a single positional argument for keymap information. For example: 34 | #+begin_src emacs-lisp 35 | ;; it's a keymap: `define-key' compatible 36 | (familiar mode-map "a" #'a) 37 | ;; list of keymaps: `general-def' compatible 38 | (familiar (mode1-map mode2-map) "a" #'a) 39 | (familiar '(mode1-map mode2-map) "a" #'a) 40 | ;; state and keymap: a little weird; not `evil-define-key' compatible 41 | (familiar ((state1 keymap2) (keymap1 keymap2)) "a" #'a) 42 | (familiar (state1 state2 : keymap1 keymap2) "a" #'a) 43 | #+end_src 44 | 45 | With this option, it is always clear where the keymap specification is, and the definer can support all keybindings. However, the syntax is a bit strange, and it is incompatible with the syntax of ~global-set-key~ and ~evil-define-key~. 46 | 47 | *** Option 4: Require Keymaps to Be Quoted 48 | #+begin_src emacs-lisp 49 | ;; like `global-set-key' 50 | (familiar "a" #'a) 51 | ;; like `define-key' 52 | (familiar 'mode-map "a" #'a) 53 | ;; like `evil-define-key' 54 | (familiar 'normal 'mode-map "a" #'a) 55 | ;; can distinguish the key as a variable since it is unquoted 56 | (familiar 'mode-map key-var #'key-def) 57 | #+end_src 58 | 59 | This still allows correctly determining when the keybindings start since a key cannot be a symbol. However, it breaks compatibility with all normal key definers because of the quoting. 60 | 61 | *** Option 5: Use an Explicit Positional Argument Separator 62 | We can take advantage of keywords to specify exactly where the keybindings start: 63 | #+begin_src emacs-lisp 64 | ;; like `global-set-key' 65 | (familiar : "a" #'a) 66 | ;; like `define-key' 67 | (familiar mode-map : "a" #'a) 68 | ;; like `evil-define-key' 69 | (familiar 'normal mode-map : "a" #'a) 70 | ;; can distinguish the key as a variable since it is unquoted 71 | (familiar mode-map : key-var #'key-def) 72 | #+end_src 73 | 74 | This is not a perfect drop-in replacement unless the separator is optional. However, this could be used to replace all existing ~define-key~ calls in one's config using a regexp search and replace that added the colon. 75 | 76 | *** Option 6: Guess the Number of Positional Arguments 77 | Guess that all symbols and lists (quoted or unquoted) are positional. This is like ~general-def~ and has the same downsides. You could run into a situation where a variable holding a key is thought to be a positional argument. 78 | 79 | We could add some extra checks to limit when the behavior is incorrect (e.g. set a variable for the maximum number of positional args depending on what extensions you are using), but there will always be edge cases. 80 | 81 | *** Decision 82 | We can use options 5 and 6. 83 | 84 | If someone wants 2, familiar can again provide exact equivalents like it did before in a separate package: 85 | - ~familiar-global-set-key~ - like ~global-set-key~ (new wrapper not in general.el since ~general-define-key~ could be used) 86 | - ~familiar-define-key~ - =KEYMAP= positional argument like ~define-key~ (not ~familiar-emacs-define-key~ since the base definer will be internal) 87 | - ~familiar-evil-define-key~ - =STATE KEYMAP= positional arguments like ~evil-define-key~ 88 | 89 | These will not be used in the documentation or recommended but will be listed as one option for migrating all existing keybindings if you want to later display your keybindings in a formatted buffer with [[https://github.com/noctuid/annalist.el][annalist]]. A simple search/replace would allow this. A more complicated regexp search and replace will be listed as an alternative to use ~familiar~. Alternatively they could do a simple search/replace if they don't use variables for any keys in their config. 90 | 91 | ~familiar~ will be the only definer used in the readme. The differences from ~general-def~ will be as follows: 92 | - It will specially handle =:= as a separator (~general-def~ would require =: t= as a separator) 93 | - The separator will not be required, but the user will have to opt-in to not using it. There will be a variable they must set at expansion time to confirm they understand the caveats (i.e. you must use a separator if the first key bound is no a string or vector). 94 | - Examples in the README will use an explicit separator (though it will be immediately mentioned that you can set =familiar-dwim= to allow excluding it) 95 | 96 | *** Solving Remaining Problems 97 | It should be possible to use a variable for the keymaps (e.g. my-lisp-maps). I need to test if this will work without changes on the new prototype: 98 | #+begin_src emacs-lisp 99 | (defvar my-cider-maps '(cider-mode-map cider-repl-mode-map)) 100 | 101 | (familiar my-cider-maps : "foo" #'bar) 102 | #+end_src 103 | 104 | I believe this will just be detected as a keymap named =my-cider-maps= and fail (which is what would happen in general.el). ~define-key~ doesn't support binding in multiple keymaps, so we don't have to worry about compatibility here. To support this, we can just add support for keymap aliases to multiple keymaps. Then you could just add to ~familiar-keymap-aliases~, or we could create a new helper that would also define a corresponding variable if the user needed it: 105 | #+begin_src emacs-lisp 106 | (familiar-keymap-alias my-cider-maps '(cider-mode-map cider-repl-mode-map)) 107 | #+end_src 108 | 109 | * Choosing the Keybinding Specification 110 | The key definition syntax of general.el is potentially problematic: 111 | - Distinguishing extended definitions from other definitions is currently doable but a little strange 112 | - It has to check ~keymapp~ to distinguish a keymap definition from an extended definition 113 | - It has to check ~function~ to distinguish a lambda definition from an extended definition 114 | - It has to check that the list does not start with =menu-item= 115 | - It has to check that the list contains keywords (since a cons is a valid definition) 116 | - If definitions in the future can be lists with keywords, the above solution will no longer work 117 | - It would be nice to be able to define multiple keys to the same definition (e.g. for self insert commands), and using lists in the key position has the same problem as above (i.e. they could be valid to pass to ~define-key~ or ~keymap-set~ or whatever) 118 | 119 | We can handle some keyword specially to specify an extended definition follows: 120 | #+begin_src emacs-lisp 121 | (familiar mode-map : 122 | "a" #'a 123 | :ext ("b" #'b :keyword 1 :desc "Run b") 124 | "c" #'c) 125 | #+end_src 126 | 127 | To handle multiple keys, all positional arguments but the last can be considered keys: 128 | #+begin_src emacs-lisp 129 | (familiar mode-map : 130 | "a" #'a 131 | :ext ("b" "B" #'b :keyword 1) 132 | "c" #'c) 133 | #+end_src 134 | 135 | Lists will probably never be allowed as keys, so we could potentially remove the keyword and just have the extended definition take the place of a key def pair. I need to consider this, but I think I prefer being safe and just using the extended definition syntax. 136 | 137 | Alternatively or additionally, =:ext t= could be specified to only use extended definition syntax: 138 | #+begin_src emacs-lisp 139 | (familiar mode-map 140 | :ext t 141 | ("a" #'a) 142 | ("b" "B" #'b :keyword 1) 143 | ("c" #'c)) 144 | #+end_src 145 | 146 | * Advanced Functionality 147 | ** Dynamically Changing Options 148 | Some people like to make keybindings with completely different settings in the same call. 149 | 150 | ~general-defs~ exists: 151 | #+begin_src emacs-lisp 152 | (general-defs 'normal mode-map 153 | "a" #'a 154 | 'visual other-mode-map 155 | "b" #'b) 156 | #+end_src 157 | 158 | The downside is that all settings are reset each time. Maybe you just want to change =:infix= or =:prefix= for example. Something like this might be nice: 159 | #+begin_src emacs-lisp 160 | (some-definer 'normal mode-map 161 | :prefix "SPC" 162 | :infix "b" 163 | "a" #'buffer-command-1 164 | "b" #'buffer-command-2 165 | :infix "w" 166 | "a" #'window-command-1) 167 | #+end_src 168 | 169 | General.el relies on keywords affecting all keybindings because ~general-create-definer~ creates definers by putting the default keywords at the very end. ~familiar~ will have to handle things differently and split these into multiple ~familiar--define-key~ calls. 170 | 171 | As for changing the keymap or state for subsequent keybindings without using =:keymaps= and =:states=, =:= can be reused here: 172 | #+begin_src emacs-lisp 173 | (familiar 'normal mode-map : 174 | :prefix "SPC" 175 | "a" #'a 176 | : 'visual other-mode-map : 177 | "b" #'b) 178 | #+end_src 179 | 180 | =::= could clear all settings. 181 | 182 | =:ext= would only apply to the current section. 183 | 184 | ** Other 185 | ~familiar~ Will not directly provide any conditionals or delayed evaluation keywords (like Doom ~map!~ =:when=, =:unless=, and =:after=). I see no reason to have these as part of the keybinding, when you can just wrap a call in ~with-eval-after-load~ or whatever. Users who want these can build on top of ~familiar~ like ~map!~ built on top of general.el. 186 | 187 | * Examples 188 | Here are what the expansions of the prototype implementation looks like currently: 189 | #+begin_src emacs-lisp 190 | ;; basic example without any option changing witchcraft 191 | (familiar foo-mode-map 192 | :prefix "C-c" 193 | "a" #'a 194 | "b" #'b) 195 | ;; expands to 196 | (progn 197 | (familiar--define-key 198 | (list 199 | :keymaps 'foo-mode-map 200 | :prefix "C-c") 201 | (list "a" #'a) 202 | (list "b" #'b))) 203 | 204 | 205 | ;; : is optional after positional arguments if `familiar-dwim' is non-nil 206 | (familiar (foo-mode-map bar-mode-map) 207 | :prefix "C-c" 208 | :predicate #'pred 209 | "a" #'a 210 | 211 | : bar-mode-map 212 | :prefix "f" 213 | "b" #'b 214 | :ext ("c" "C" #'c :desc "c!") 215 | "d" #'d) 216 | ;; expands to 217 | (progn 218 | (familiar--define-key 219 | (list 220 | :keymaps '(foo-mode-map bar-mode-map) 221 | :prefix "C-c" 222 | :predicate #'pred) 223 | (list "a" #'a)) 224 | (familiar--define-key 225 | (list 226 | :keymaps 'bar-mode-map 227 | :prefix "f" 228 | :predicate #'pred) 229 | (list "b" #'b) 230 | (list "c" "C" #'c :desc "c!") 231 | (list "d" #'d))) 232 | 233 | 234 | ;; :: clears 235 | (familiar (foo-mode-map bar-mode-map) 236 | :prefix "C-c" 237 | :infix "a" 238 | :predicate #'pred 239 | "a" #'a 240 | 241 | :infix "b" 242 | "b" #'b 243 | :ext ("c" "C" #'c :desc "c!") 244 | "d" #'d 245 | 246 | :: 247 | "e" #'e) 248 | ;; expands to 249 | (progn 250 | (familiar--define-key 251 | (list 252 | :keymaps '(foo-mode-map bar-mode-map) 253 | :prefix "C-c" 254 | :infix "a" 255 | :predicate #'pred) 256 | (list "a" #'a)) 257 | (familiar--define-key 258 | (list 259 | :infix "b" 260 | :keymaps '(foo-mode-map bar-mode-map) 261 | :prefix "C-c" 262 | :predicate #'pred) 263 | (list "b" #'b) 264 | (list "c" "C" #'c :desc "c!") 265 | (list "d" #'d)) 266 | (familiar--define-key 267 | (list) 268 | (list "e" #'e))) 269 | 270 | 271 | ;; :ext t 272 | (familiar foo-mode-map 273 | "a" #'a 274 | :ext ("b" "B" #'b) 275 | "c" #'c 276 | :ext t 277 | ("d" "D" #'d) 278 | ("e" "E" :desc "eee")) 279 | ;; expands to 280 | (progn 281 | (familiar--define-key 282 | (list :keymaps 'foo-mode-map) 283 | (list "a" #'a) 284 | (list "b" "B" #'b) 285 | (list "c" #'c) 286 | (list "d" "D" #'d) 287 | (list "e" "E" :desc "eee"))) 288 | #+end_src 289 | 290 | I personally think it's cleaner to not change settings for any keywords except maybe =:infix=, but it doesn't hurt to support it with this optional syntax. 291 | 292 | Default keywords can still be done by allowing an empty key section to allow for ~familiar-create-definer~: 293 | #+begin_src emacs-lisp 294 | ;; default arguments preceding first : 295 | (familiar :prefix "C-c" : 296 | foo-mode-map : 297 | "a" #'a 298 | :prefix "C-a" 299 | "b" #'b) 300 | ;; expands to 301 | (progn 302 | (familiar--define-key 303 | (list 304 | :keymaps 'foo-mode-map 305 | :prefix "C-c") 306 | (list "a" #'a)) 307 | (familiar--define-key 308 | (list 309 | :prefix "C-a" 310 | :keymaps 'foo-mode-map) 311 | (list "b" #'b))) 312 | #+end_src 313 | --------------------------------------------------------------------------------