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))