Subject: bug#78742: [PATCH] Improve repeat-continue property handling

From: "Paul D. Nelson" <[email-masked]>
Date: Tue, 10 Jun 2025 10:07:01 +0200

This patch adds support for a 'repeat-continue' property value of t to
continue any active repeat map, following a suggestion by Karthik
Chikmagalur on emacs-devel.  Any feedback welcome.

>From 492a34a173cd2404cddf8ac97c98a1f968496d98 Mon Sep 17 00:00:00 2001
From: Paul Nelson <[email-masked]>
Date: Tue, 10 Jun 2025 09:21:52 +0200
Subject: [PATCH] Improve repeat-continue property handling

* lisp/repeat.el (repeat-get-map-sym): Support 'repeat-continue'
property value of t to continue any active repeat map (suggested
by Karthik Chikmagalur <[email-masked]>).
Simplify logic so that repeat-continue does not interfere with
repeat-map activation.
(repeat-check-map): Allow commands with 'repeat-continue'
property value of t to continue without checking the key.
(repeat-mode): Update and tweak docstring.
* lisp/bind-key.el (bind-keys-form): Update handling of
':continue-only' keyword: only add keymap to 'repeat-continue'
if current value is a list.
(bind-keys): Update documentation, mirroring bind-keys-form.
* lisp/keymap.el (defvar-keymap): Update handling of ':continue'
keyword: only add keymap to 'repeat-continue' if current value
is a list.
* test/lisp/repeat-tests.el (repeat-tests-continue)
(repeat-tests-continue-another): Enable previously commented
tests that now work correctly.
* etc/NEWS: Update announcement of 'repeat-continue'.
---
 etc/NEWS                  | 15 +++++++++------
 lisp/bind-key.el          | 15 ++++++++-------
 lisp/keymap.el            |  6 ++++--
 lisp/repeat.el            | 29 +++++++++++++++++++----------
 test/lisp/repeat-tests.el | 12 ++++++------
 5 files changed, 46 insertions(+), 31 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index d5b62b9850a..83e050ebff4 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2442,12 +2442,15 @@ will be calculated based on the window width.
 
 +++
 ** New symbol property 'repeat-continue' for 'repeat-mode'.
-A command with this symbol property whose value is a list of repeat
-maps will not activate the repeat map in 'repeat-mode'.  It will only
-continue the already activated repeating sequence.  Also 'defvar-keymap'
-supports a new keyword ':continue' with a list of commands that only
-continue the active repeating sequence, and the 'use-package' and
-'bind-keys' macros support a similar keyword ':continue-only'.
+A command with the 'repeat-continue' symbol property, which can be a
+list of keymaps or t, will continue an already active repeating sequence
+for a keymap in that list (resp. all keymaps).  The new property does
+not affect whether the command starts a repeating sequence, which
+remains governed by the 'repeat-map' property.  'defvar-keymap' supports
+a new keyword ':continue', a list of commands, and adds the keymap to
+the 'repeat-continue' property of each command in that list.  The
+'use-package' and 'bind-keys' macros support a similar keyword
+':continue-only'.
 
 ** New function 'completion-table-with-metadata'.
 It offers a more concise way to create a completion table with metadata.
diff --git a/lisp/bind-key.el b/lisp/bind-key.el
index 12417106783..49f5bd30351 100644
--- a/lisp/bind-key.el
+++ b/lisp/bind-key.el
@@ -402,13 +402,11 @@ bind-keys-form
                           ;; repeat-map is non-nil, map is always
                           ;; non-nil
                           (if (eq repeat-type :continue-only)
-                              `((unless (memq ',repeat-map
-                                              (or (get ,fun 'repeat-continue)
-                                                  '()))
-                                  (put ,fun 'repeat-continue
-                                       (append (or (get ,fun 'repeat-continue)
-                                                   '())
-                                               (list ',repeat-map))))
+                              `((let ((cur (get ,fun 'repeat-continue)))
+                                  (when (and (listp cur)
+                                             (not (memq ',repeat-map cur)))
+                                    (put ,fun 'repeat-continue
+                                         (append cur (list ',repeat-map)))))
                                 (bind-key ,(car form) ,fun ,map ,filter))
                             `(,@(when (and repeat-map (not (eq repeat-type :exit)))
                                   `((put ,fun 'repeat-map ',repeat-map)))
@@ -446,6 +444,9 @@ bind-keys
                          same behavior as if no special keyword had
                          been used (that is, the command is bound, and
                          it's `repeat-map' property set)
+:continue-only BINDINGS - Within the scope of `:repeat-map', will make
+                         the command continue but not enter the repeat
+                         map, via the `repeat-continue' property
 :filter FORM           - optional form to determine when bindings apply
 
 The rest of the arguments are conses of keybinding string and a
diff --git a/lisp/keymap.el b/lisp/keymap.el
index a12084a3ad4..cf4c8d90bb1 100644
--- a/lisp/keymap.el
+++ b/lisp/keymap.el
@@ -754,8 +754,10 @@ defvar-keymap
         (dolist (def (plist-get repeat :enter))
           (push `(put ',def 'repeat-map ',variable-name) props))
         (dolist (def (plist-get repeat :continue))
-          (push `(put ',def 'repeat-continue
-                      (cons ',variable-name (get ',def 'repeat-continue)))
+          (push `(let ((val (get ',def 'repeat-continue)))
+                   (when (listp val)
+                     (put ',def 'repeat-continue
+                          (cons ',variable-name val))))
                 props))
         (while defs
           (pop defs)
diff --git a/lisp/repeat.el b/lisp/repeat.el
index daa53fe7195..9308f972a91 100644
--- a/lisp/repeat.el
+++ b/lisp/repeat.el
@@ -427,9 +427,18 @@ repeat-mode
 When Repeat mode is enabled, certain commands bound to multi-key
 sequences can be repeated by typing a single key, after typing the
 full key sequence once.
-The commands which can be repeated like that are those whose symbol
- has the property `repeat-map' which specifies a keymap of single
-keys for repeating.
+
+The commands that can be repeated in this way are those whose symbols
+have the `repeat-map' property, which specifies a keymap of single keys
+for repeating.
+
+Normally, invoking a command outside that keymap terminates the
+repeating sequence.  However, if the command's `repeat-continue'
+property is non-nil, it may instead continue the current repeating
+sequence: if the property is a list of keymaps, then the command
+continues when the current repeat map is in the list; if the property is
+t, the command always continues the sequence.
+
 See `describe-repeat-maps' for a list of all repeatable commands."
   :global t :group 'repeat
   (if (not repeat-mode)
@@ -460,12 +469,11 @@ repeat-get-map-sym
   (when repeat-mode
     (let ((map-sym (or repeat-map (repeat--command-property 'repeat-map)))
           (continue (repeat--command-property 'repeat-continue)))
-      (when continue
-        (if repeat-in-progress
-            (when (and (consp continue)
-                       (memq repeat-in-progress continue))
-              (setq map-sym repeat-in-progress))
-          (setq map-sym nil)))
+      (when (and repeat-in-progress
+                 (or (eq continue t)
+                     (and (consp continue)
+                          (memq repeat-in-progress continue))))
+        (setq map-sym repeat-in-progress))
       map-sym)))
 
 (defun repeat-get-map (map)
@@ -495,7 +503,8 @@ repeat-check-map
        ;; in the middle of repeating sequence (bug#47566).
        (or (< (minibuffer-depth) (car repeat--prev-mb))
            (eq current-minibuffer-command (cdr repeat--prev-mb)))
-       (repeat-check-key last-command-event map)
+       (or (eq (repeat--command-property 'repeat-continue) t)
+           (repeat-check-key last-command-event map))
        t))
 
 (defun repeat-pre-hook ()
diff --git a/test/lisp/repeat-tests.el b/test/lisp/repeat-tests.el
index f96d8df2ebd..6bf9badf31d 100644
--- a/test/lisp/repeat-tests.el
+++ b/test/lisp/repeat-tests.el
@@ -224,9 +224,9 @@ repeat-tests-continue
        "C-M-e c z"
        '((1 e)) "cz")
       ;; 'C-M-o' should also activate
-      ;; (repeat-tests--check
-      ;;  "C-M-o c z"
-      ;;  '((1 o) (1 c)) "z")
+      (repeat-tests--check
+       "C-M-o c z"
+       '((1 o) (1 c)) "z")
       )))
 
 (ert-deftest repeat-tests-continue-another ()
@@ -246,9 +246,9 @@ repeat-tests-continue-another
        "C-M-e t z"
        '((1 e)) "tz")
       ;; 'C-M-u' should also activate
-      ;; (repeat-tests--check
-      ;;  "C-M-u t z"
-      ;;  '((1 u) (1 t)) "z")
+      (repeat-tests--check
+       "C-M-u t z"
+       '((1 u) (1 t)) "z")
       ;; 'C-M-o' shared with another map should continue current map
       (repeat-tests--check
        "C-M-s t C-M-o C-M-o t z"
-- 
2.39.3 (Apple Git-145)