bug#81645: 32.0.50; Annotations break *Completions* navigation in vertical format

Stephen Berman via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]>
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
On Thu, 20 Aug 2026 08:24:04 +0300 Eli Zaretskii <[email protected]> wrote:

>> From: Sean Whitton <[email protected]>
>> Cc: [email protected]
>> Date: Tue, 18 Aug 2026 11:15:54 +0100
>> 
>> Eli Zaretskii [17/Aug  5:26pm +03] wrote:
>> >> Cc: Eli Zaretskii <[email protected]>, Sean Whitton <[email protected]>
>> >> From: Stephen Berman <[email protected]>
>> >> Date: Mon, 17 Aug 2026 16:06:06 +0200
>> >>
>> >> 0. emacs -Q
>> >> 1. (setopt completions-format 'vertical)
>> >> 2. Sanity check: Typing `M-x auto- TAB' pops up the *Completions* buffer
>> >>    and you can navigate up and down through each column by typing
>> >>    `M-<DOWN>' and `M-<UP>' with wrapping from the last to the first
>> >>    candidate and vice versa.
>> >> 3. Now type `M-x abbrev- TAB', then repeatedly type `M-<DOWN>': after
>> >>    the last candidate point wraps to the second instead of the first
>> >>    candidate.
>> >> 4. Now type `M-x dabbrev- TAB'.  Now typing `M-<DOWN>' once moves to
>> >>    last candidate, but repeating `M-<DOWN>' does not move point (in
>> >>    particular, no wrapping).  Now typing `M-<UP>' moves point back to
>> >>    the first candidate, but typing `M-<UP>' again move point not to the
>> >>    beginning of the last candidate but to the middle of the annotation
>> >>    "(M-/)" affixed to the candidate.
>> >>
>> >> The problems in 3 and 4 are due to the annotations in the *Completions*
>> >> buffer.  The attached patch fixes these problems, so that navigation in
>> >> 3 and 4 work as in 2 according to my testing.  I think this patch should
>> >> be applied to the release branch because the type of navigation in
>> >> vertical format illustrated by 2 was introduced in Emacs 31 by commit
>> >> 77ca60b48d01, which, however, failed to take annotations into account.
>> >
>> > I think this is fine to install on the emacs-31 release branch,
>> > because the code it changes is specific to the 'vertical'
>> > completions-format, which is new in Emacs 31.
>> >
>> > Sean?
>> 
>> Okay with me too.
>
> Thanks.  Stephen, please install on the release branch when you have
> time.

Thanks for the go-ahead, but in the mean time there's a complication.  I
wrote some tests for completions with annotations (augmenting the
function `completions-format-navigation--tests' I added in 77ca60b48d01,
which tests navigation with and without wrapping in both the horizontal
and vertical formats), but I found that some of the tests failed with
the patch I posted for `last-completion'.  I was able to fix the
failures by replacing the text property `mouse-face' by the
text-property `completion--string' (completion candidate annotations
have the latter but not the former) in numerous places; see the attached
patch.  With this patch all minibuffer-tests pass, but these changes may
still be considered too risky for the release branch.  If so, we could
install just my first patch for `last-completion' in emacs-31, omitting
the new tests, and install the more extensive changes and the tests in
master.  What do you two think?

Steve Berman
(unnamed) (text/x-patch, 12 KB)
diff --git a/lisp/simple.el b/lisp/simple.el
index 7ea287fd6ff..37ceeef9543 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -10173,9 +10173,8 @@ last-completion
           (col (current-column))
           (last-col (progn
                       (first-completion)
-                      (goto-char (pos-eol))
-                      (goto-char (previous-single-property-change
-                                  (point) 'mouse-face))
+                      (goto-char (1- (pos-eol)))
+                      (completion--move-to-candidate-start)
                       (current-column))))
       (if (zerop last-col)
           ;; If there is only one column of completions, the last
@@ -10209,18 +10208,20 @@ previous-column-completion
   (next-column-completion (- n)))
 
 (defun completion--move-to-candidate-start ()
-  "If in a completion candidate, move point to its start."
-  (when (and (get-text-property (point) 'mouse-face)
+  "If in a completion candidate, move point to its start.
+If the candidate has a prefixed annotation, move to the start of the
+annotation."
+  (when (and (get-text-property (point) 'completion--string)
              (not (bobp))
-             (get-text-property (1- (point)) 'mouse-face))
-    (goto-char (previous-single-property-change (point) 'mouse-face))))
+             (get-text-property (1- (point)) 'completion--string))
+    (goto-char (previous-single-property-change (point) 'completion--string))))
 
 (defun completion--move-to-candidate-end ()
   "If in a completion candidate, move point to its end.
 More precisely, point moves the the position immediately after the last
-character of the completion candidate."
-  (when (get-text-property (point) 'mouse-face)
-    (goto-char (or (next-single-property-change (point) 'mouse-face)
+character of the completion candidate (including any annotations)."
+  (when (get-text-property (point) 'completion--string)
+    (goto-char (or (next-single-property-change (point) 'completion--string)
                    (point-max)))))
 
 (defun next-column-completion (n)
@@ -10353,24 +10354,24 @@ next-line-completion
   (let (line column pos found last first)
     (when (and (bobp)
                (> n 0)
-               (get-text-property (point) 'mouse-face)
+               (get-text-property (point) 'completion--string)
                (not (get-text-property (point) 'first-completion)))
       (let ((inhibit-read-only t))
         (add-text-properties (point) (1+ (point)) '(first-completion t)))
       (setq n (1- n)))
 
-    (if (get-text-property (point) 'mouse-face)
+    (if (get-text-property (point) 'completion--string)
         ;; If in a completion, move to the start of it.
         (completion--move-to-candidate-start)
       ;; Try to move to the previous completion.
-      (setq pos (previous-single-property-change (point) 'mouse-face))
+      (setq pos (previous-single-property-change (point) 'completion--string))
       (if pos
           ;; Move to the start of the previous completion.
           (progn
             (goto-char pos)
-            (unless (get-text-property (point) 'mouse-face)
+            (unless (get-text-property (point) 'completion--string)
               (goto-char (previous-single-property-change
-                          (point) 'mouse-face nil (point-min)))))
+                          (point) 'completion--string nil (point-min)))))
         (cond ((> n 0) (setq n (1- n)) (first-completion))
               ((< n 0) (first-completion)))))
 
@@ -10386,12 +10387,12 @@ next-line-completion
                     (eq (forward-line 1) 0)
                     (not (eobp))
                     (move-to-column column))
-          (when (get-text-property (point) 'mouse-face)
+          (when (get-text-property (point) 'completion--string)
             (setq found t)))
         (when (not found)
           (if (and (not completion-auto-wrap)
                    (if (eq completions-format 'vertical)
-                       (and (or last (get-text-property (point) 'mouse-face))
+                       (and (or last (get-text-property (point) 'completion--string))
                             (last-completion))
                      (goto-char pos)))
               t
@@ -10399,12 +10400,12 @@ next-line-completion
               (setq pos nil)
               (goto-char (point-min))
               (when (and (eq (move-to-column column) column)
-                         (get-text-property (point) 'mouse-face))
+                         (get-text-property (point) 'completion--string))
                 (setq pos (point)))
               (while (and (not pos) (> line (line-number-at-pos)))
                 (forward-line 1)
                 (when (and (eq (move-to-column column) column)
-                           (get-text-property (point) 'mouse-face))
+                           (get-text-property (point) 'completion--string))
                   (setq pos (point)))))
             (if pos (goto-char pos))
             (when (eq completions-format 'vertical)
@@ -10422,13 +10423,13 @@ next-line-completion
         (while (and (not found)
                     (eq (forward-line -1) 0)
                     (move-to-column column))
-          (when (get-text-property (point) 'mouse-face)
+          (when (get-text-property (point) 'completion--string)
             (setq found t)))
         (when (not found)
           (if (and (not completion-auto-wrap)
                    (if (eq completions-format 'vertical)
                        (and (or last first
-                                (get-text-property (point) 'mouse-face))
+                                (get-text-property (point) 'completion--string))
                             first (first-completion))
                      (goto-char pos)))
               t
@@ -10436,12 +10437,12 @@ next-line-completion
               (setq pos nil)
               (goto-char (point-max))
               (when (and (eq (move-to-column column) column)
-                         (get-text-property (point) 'mouse-face))
+                         (get-text-property (point) 'completion--string))
                 (setq pos (point)))
               (while (and (not pos) (< line (line-number-at-pos)))
                 (forward-line -1)
                 (when (and (eq (move-to-column column) column)
-                           (get-text-property (point) 'mouse-face))
+                           (get-text-property (point) 'completion--string))
                   (setq pos (point)))))
             (if pos (goto-char pos))
             (when (eq completions-format 'vertical)
diff --git a/test/lisp/minibuffer-tests.el b/test/lisp/minibuffer-tests.el
index 2f70d248711..fd8702b4a65 100644
--- a/test/lisp/minibuffer-tests.el
+++ b/test/lisp/minibuffer-tests.el
@@ -873,7 +873,20 @@ completions-format-navigation--tests
                         (dotimes (_ x)
                           (push (concat (car comps) (pop letters)) comps))
                         (nreverse comps))))
-         (completions (funcall gen-compl n))
+         (completions
+          (pcase n
+            ('annot2 '("dabbrev-"
+                       ("dabbrev-completion" ""
+                        #(" (C-M-/)" 0 8 (face completions-annotations)))
+                       ("dabbrev-expand" ""
+                        #(" (M-/)" 0 6 (face completions-annotations)))))
+            ('annot5 '("abbrev-"
+                       ("abbrev-edit-save-buffer" "" "")
+                       ("abbrev-edit-save-to-file" "" "") ("abbrev-mode" "" "")
+                       ("abbrev-prefix-mark"
+                        "" #(" (M-')" 0 6 (face completions-annotations)))
+                       ("abbrev-suggest-show-report" "" "")))
+            (_ (funcall gen-compl n))))
 
          ;; Navigation tests.
          ;; (i) For both horizontal and vertical formats.
@@ -889,8 +902,26 @@ completions-format-navigation--tests
                              ('line 'previous-line-completion))))
               (completing-read-with-minibuffer-setup completions
                 (insert (car completions))
-                (minibuffer-completion-help)
-                (switch-to-completions)
+                (pcase n
+                  ((or 'annot2 'annot5)
+                   (progn
+                     (with-help-window "*Completions*"
+                       (display-completion-list (cdr completions)))
+                     (switch-to-buffer "*Completions*")
+                     (first-completion)))
+                  (_ (progn
+                       (minibuffer-completion-help)
+                       (switch-to-completions))))
+                (let ((completions (pcase n
+                                     ('annot2 '("dabbrev-completion"
+                                                "dabbrev-expand"))
+                                     ('annot5 '("abbrev-edit-save-buffer"
+                                                "abbrev-edit-save-to-file"
+                                                "abbrev-mode"
+                                                "abbrev-prefix-mark"
+                                                "abbrev-suggest-show-report"))
+                                     (_ n completions)))
+                      (n (pcase n ('annot2 2) ('annot5 5) (_ n))))
                 ;; Sanity check that we're on first completion candidate.
                 (should
                  (equal (car completions)
@@ -947,7 +978,7 @@ completions-format-navigation--tests
                       (should
                        (equal (car completions)
                               (get-text-property (point)
-                                                 'completion--string))))))))))
+                                                 'completion--string)))))))))))
 
          ;; (ii) Only for horizontal format.
          (within-column
@@ -1015,12 +1046,29 @@ completions-format-navigation--tests
           (lambda ()
             (completing-read-with-minibuffer-setup completions
               (insert (car completions))
-              (minibuffer-completion-help)
-              (switch-to-completions)
-              (let ((one-col (save-excursion
-                                (first-completion)
-                                (completion--move-to-candidate-end)
-                                (eolp))))
+              (pcase n
+                ((or 'annot2 'annot5 5)
+                 (progn
+                   (with-help-window "*Completions*"
+                     (display-completion-list (cdr completions)))
+                   (switch-to-buffer "*Completions*")
+                   (first-completion)))
+                (_ (progn
+                     (minibuffer-completion-help)
+                     (switch-to-completions))))
+              (let ((completions (pcase n
+                                   ('annot2 '("dabbrev-completion"
+                                              "dabbrev-expand"))
+                                   ('annot5 '("abbrev-edit-save-buffer"
+                                              "abbrev-edit-save-to-file"
+                                              "abbrev-mode"
+                                              "abbrev-prefix-mark"
+                                              "abbrev-suggest-show-report"))
+                                   (_ n completions)))
+                    (one-col (save-excursion
+                               (first-completion)
+                               (completion--move-to-candidate-end)
+                               (eolp))))
                 (while (not (eobp))
                   (let ((first (get-text-property (point) 'completion--string))
                          last pos)
@@ -1150,6 +1198,12 @@ completions-format-navigation-test-15
 (ert-deftest completions-format-navigation-test-16 ()
   (completions-format-navigation--tests 16))
 
+(ert-deftest completions-format-navigation-test-annot2 ()
+  (completions-format-navigation--tests 'annot2))
+
+(ert-deftest completions-format-navigation-test-annot5 ()
+  (completions-format-navigation--tests 'annot5))
+
 (ert-deftest completion-cycle ()
   (completing-read-with-minibuffer-setup '("aaa" "bbb" "ccc")
     (let ((completion-cycle-threshold t))
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.