bug#81635: 32.0.50; Change in TAB cycling *Completions* and minibuffer

Spencer Baugh via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]>
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
Stephen Berman <[email protected]> writes:
> When `completion-auto-select' is set to t and `completion-auto-wrap' is
> also t (the default), then TAB and S-TAB cycle through the *Completions*
> buffer and after the last (resp. first) completion candidate switches to
> the minibuffer.  In Emacs 30 typing TAB in the minibuffer continues the
> cycling, i.e., TAB continues with the first candidate, S-TAB with the
> last.  This behavior changed with commit 5b19ca56f1d9 (bug#74019): since
> then TAB in the minibuffer returns to the last candidate and S-TAB
> returns to the first, and further TABs or S-TABs just bounce back and
> forth between that candidate and the minibuffer rather than cycling
> through *Completions*.  That is, after returning to *Completions* from
> the minibuffer you have to switch from TAB to S-TAB or vice versa in
> order to resume cycling through *Completions*.  This change in behavior
> seems like unintended fallout from commit 5b19ca56f1d9.  The attached
> patch restores the previous cyclic behavior.  I've added Spencer Baugh
> (the author of that commit) and Stefan Monnier in Cc:, in case they have
> any comments.

Thanks for the detailed report.  This should fix it, and includes a
test.
0001-Fix-TAB-to-wrap-with-completion-auto-select-t.patch (text/x-patch, 5.1 KB)
From d24b1c6119814a0eb0fc0b1b7906251b5b3be0b2 Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email protected]>
Date: Thu, 20 Aug 2026 13:49:28 -0400
Subject: [PATCH] Fix TAB to wrap with completion-auto-select=t

In 5b19ca56f1d9, *Completions* started keeping the same
candidate selected when refreshed.  This broke the
completion-auto-select=t completion-auto-wrap=t behavior of
wrapping into the minibuffer and then back into *Completions*
after repeated TABs.

Now, when we wrap into the minibuffer, we clear the selected
candidate, which fixes this behavior.

* lisp/minibuffer.el (completions--clear-selection): Add.
(minibuffer-hide-completions): Call
completions--clear-selection.
* lisp/simple.el (next-column-completion): Call
completions--clear-selection. (bug#81635)
* test/lisp/minibuffer-tests.el
(completion-auto-select-test-bug81635): Add testing.
---
 lisp/minibuffer.el            |  9 ++++++++-
 lisp/simple.el                |  8 +++++---
 test/lisp/minibuffer-tests.el | 33 +++++++++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index a7cc04e32ce..396c696f012 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -2747,6 +2747,13 @@ completions--deselect
     (goto-char (or (next-single-property-change (point) 'completion--string)
                    (point-max)))))
 
+(defun completions--clear-selection ()
+  "Clear the selected candidate in the completions buffer.
+
+Unlike `completions--deselect' this fully clears all selected-completion
+state from the buffer."
+  (goto-char (point-min)))
+
 (defun completions--should-show-p (metadata &optional force-eager-update)
   "Return non-nil if *Completions* should be automatically updated or displayed.
 
@@ -3022,7 +3029,7 @@ minibuffer-hide-completions
     (with-selected-window win
       ;; Move point off any completions, so we don't move point there
       ;; again the next time `minibuffer-completion-help' is called.
-      (goto-char (point-min))
+      (completions--clear-selection)
       (bury-buffer))))
 
 (defun exit-minibuffer ()
diff --git a/lisp/simple.el b/lisp/simple.el
index d92abe331c8..5af6467d73d 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -10267,7 +10267,9 @@ next-column-completion
                          (not (eq completions-format 'vertical))))
             (if (and (eq completion-auto-select t) tabcommand
                      (minibufferp completion-reference-buffer))
-                (throw 'bound nil)
+                (progn
+                  (completions--clear-selection)
+                  (throw 'bound nil))
               (first-completion))))
         (when (and (eq completions-format 'vertical)
                    (or last
@@ -10319,8 +10321,8 @@ next-column-completion
                    (completion--move-to-candidate-start))
                   ((and (eq completion-auto-select t) tabcommand
                         (minibufferp completion-reference-buffer))
-                   (progn
-                     (throw 'bound nil)))
+                   (completions--clear-selection)
+                   (throw 'bound nil))
                   (t
                    (last-completion)))))
         (setq n (1+ n))))
diff --git a/test/lisp/minibuffer-tests.el b/test/lisp/minibuffer-tests.el
index cd59d201109..ab40b40c7e4 100644
--- a/test/lisp/minibuffer-tests.el
+++ b/test/lisp/minibuffer-tests.el
@@ -641,6 +641,39 @@ completion-auto-select-test
       (execute-kbd-macro (kbd "TAB TAB"))
       (should (eq (current-buffer) (get-buffer "*Completions*"))))))
 
+(ert-deftest completion-auto-select-test-bug81635 ()
+  (let ((completion-auto-select t)
+        (completion-auto-wrap t))
+    (completing-read-with-minibuffer-setup
+        '("aa" "ab" "ac")
+      (cl-flet ((selected ()
+                  (and (eq (current-buffer) (get-buffer "*Completions*"))
+                       (get-text-property (point) 'completion--string))))
+        ;; TAB cycles forward through all candidates, then to the
+        ;; minibuffer, then back to the first candidate.
+        (execute-kbd-macro (kbd "a TAB"))
+        (should (equal (selected) "aa"))
+        (execute-kbd-macro (kbd "TAB"))
+        (should (equal (selected) "ab"))
+        (execute-kbd-macro (kbd "TAB"))
+        (should (equal (selected) "ac"))
+        (execute-kbd-macro (kbd "TAB"))
+        (should (minibufferp))
+        (execute-kbd-macro (kbd "TAB"))
+        (should (equal (selected) "aa"))
+        (execute-kbd-macro (kbd "TAB"))
+        (should (equal (selected) "ab"))
+        ;; S-TAB cycles backward, then to the minibuffer, then to the
+        ;; last candidate.
+        (execute-kbd-macro (kbd "<backtab>"))
+        (should (equal (selected) "aa"))
+        (execute-kbd-macro (kbd "<backtab>"))
+        (should (minibufferp))
+        (execute-kbd-macro (kbd "<backtab>"))
+        (should (equal (selected) "ac"))
+        (execute-kbd-macro (kbd "<backtab>"))
+        (should (equal (selected) "ab"))))))
+
 (ert-deftest completion-auto-wrap-test ()
   (let ((completion-auto-wrap nil))
     (completing-read-with-minibuffer-setup
-- 
2.43.7
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.