commit 902b4c5aa8dfc5c35fa7f8640e370688777b7f0e Author: Case Duckworth Date: Mon Sep 6 22:36:49 2021 -0500 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..13eb896 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# define-repeat-map.el + +Easily define repeat-maps + +Emacs 28 comes built-in with repeat.el (which see), which allows users to +define their own maps to repeat common commands easily. This package +attempts to make the definition of those maps a one-sexp affair, through the +macro `define-repeat-map'. See its docstring for details. + +## The problem + +Most of the time, defining a repeat-map and adding commands to keys is a +multi-step affair, involving something like the following: + +```lisp +(defvar other-window-repeat-map (make-sparse-keymap) + "A map for repeating `other-window' keys.") + +(define-key other-window-repeat-map "o" #'other-window) + +(put 'other-window 'repeat-map 'other-window-repeat-map)) +``` + +With more keys, it gets more complex; there has to be a better way! + +## Enter `define-repeat-map` + +With this macro, you can easily define these maps, and bind keys to them, using +only one form. The code above looks like this with `define-repeat-map`: + +```lisp +(define-repeat-map other-window + ("o" other-window)) +``` + +Which isn't *so* much more convenient, but the convenience compounds with more +complex maps. For example, here's a snippet from my `init.el`, +pre-`define-repeat-map`: + +```lisp +(defalias 'forward-word-with-case 'forward-word + "Alias for `forward-word' for use in `case-repeat-map'.") +(defalias 'backward-word-with-case 'backward-word + "Alias for `backward-word for use in `case-repeat-map'.") + +(defvar case-repeat-map + (let ((map (make-sparse-keymap))) + (define-key map "c" #'capitalize-word) + (define-key map "u" #'upcase-word) + (define-key map "l" #'downcase-word) + ;; movement + (define-key map "f" #'forward-word-with-case) + (define-key map "b" #'backward-word-with-case) + map) + "A map to repeat word-casing commands. For use with `repeat-mode'.") + +(dolist (command '(capitalize-word + capitalize-dwim + upcase-word + upcase-dwim + downcase-word + downcase-dwim + forward-word-with-case + backward-word-with-case)) + (put command 'repeat-map 'case-repeat-map)) +``` + +And here it is using this macro: + +```lisp +(define-repeat-map case + ("c" capitalize-word + "u" upcase-word + "l" downcase-word) + (:continue "f" forward-word + "b" backward-word) + (:enter downcase-dwim + upcase-dwim + capitalize-dwim)) +``` + +Much easier, in this author's humble opinion. + +## Similar packages + +I've found one other package that tries to simplify repeat-map definition, but +it's not quite as flexible as this. In fact, that's why I wrote +`define-repeat-map`. But here's the other: + +- [repeaters.el](https://github.com/mmarshall540/repeaters) diff --git a/define-repeat-map.el b/define-repeat-map.el new file mode 100644 index 0000000..8763bd4 --- /dev/null +++ b/define-repeat-map.el @@ -0,0 +1,127 @@ +;;; define-repeat-map.el --- Easy-define repeat-maps -*- lexical-binding: t -*- + +;; Copyright (C) 2021 Case Duckworth + +;; Author: Case Duckworth +;; Keywords: convenience +;; URL: + +;;; License: + +;; Everyone is permitted to do whatever with this software, without +;; limitation. This software comes without any warranty whatsoever, +;; but with two pieces of advice: +;; - Don't hurt yourself. +;; - Make good choices. + +;;; Commentary: + +;; Emacs 28 comes built-in with repeat.el (which see), which allows users to +;; define their own maps to repeat common commands easily. This package +;; attempts to make the definition of those maps a one-sexp affair, through the +;; macro `define-repeat-map'. See its docstring for details. + +;;; Code: + +(defun define-repeat-map--make-alias (cmd map) + (intern (concat (symbol-name cmd) "|" + (symbol-name map)))) + +(defun define-repeat-map--map-commands (map fn args) + (let (result) + (dolist (arg args) + (unless (stringp arg) + (push (funcall fn arg) result))) + (nreverse result))) + +(defun define-repeat-map--define-keys (map fn args) + (unless (zerop (mod (length args) 2)) + (error "Wrong number of args")) + (let (result) + (while args + (let ((key (pop args)) + (cmd (funcall fn (pop args)))) + (push `(define-key ,map (kbd ,key) #',cmd) result))) + (nreverse result))) + +;;;###autoload +(defmacro define-repeat-map (name &rest args) + "Define a repeat-map, NAME-repeat-map, and bind keys to it. + Each ARG is a list of lists containing keybind definitions of + the form (KEY DEFINITION) KEY is anything `kbd' can recognize, + and DEFINITION is passed directly to `define-key'. + + Optionally, the car of an arglist can contain the following + symbols, which changes the behavior of the key definitions in the + rest of the list: + + :enter - Provided commands can enter the repeat-map, but aren't + bound in the map. They need to be bound elsewhere, however. + + :exit - Keys are bound in the repeat-map, but can't enter the + map. Their invocation exits the repeat-map. + + :continue - Keys are bound in the repeat-map, but can't enter the + map. However, their invocations keep the repeat-map active." + (declare (indent 1)) + (let ((result) + (map (intern (concat (symbol-name name) "-repeat-map")))) + ;; Create the keymap + (push `(defvar ,map (make-sparse-keymap) + "Defined by `define-repeat-map'.") + result) + + ;; Iterate through ARGS + (dolist (arg args) + (pcase (car arg) + (:enter + ;; Add the map to the commands' repeat-map property. + (push `(progn + ,@(define-repeat-map--map-commands + `,map + (lambda (cmd) `(put ',cmd 'repeat-map ',map)) + (cdr arg))) + result)) + + (:exit + ;; Bind the commands in the map. + (push `(progn + ,@(define-repeat-map--define-keys + `,map #'identity (cdr arg))) + result)) + + (:continue + ;; Make an alias for each command, and process that alias like the + ;; default, below. + (push `(progn + ,@(define-repeat-map--define-keys + `,map + (lambda (cmd) (define-repeat-map--make-alias cmd map)) + (cdr arg)) + ,@(define-repeat-map--map-commands + `,map + (lambda (cmd) + (let ((alias (define-repeat-map--make-alias cmd map))) + `(progn + (defalias ',alias ',cmd + "Defined by `define-repeat-map'.") + (put ',alias + 'repeat-map ',map)))) + (cdr arg))) + result)) + + (_ + ;; Default: bind the commands in the map, and add the map to the + ;; commands' repeat-map property. + (push `(progn + ,@(define-repeat-map--define-keys `,map #'identity arg) + ,@(define-repeat-map--map-commands + `,map + (lambda (cmd) `(put ',cmd 'repeat-map ',map)) + arg)) + result)))) + + `(with-eval-after-load 'repeat + ,@(nreverse result)))) + +;;; define-repeat-map.el ends here