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