Navigate *Completions* buffer based on 'completions-format'

This patch makes 'next-completion' and 'previous-completion' work
in the vertical completions format analogously to how they work in
the default horizontal format (bug#78959).  It also fixes wrapping
in the vertical format and confines navigation (including
wrapping) in column-wise movement in the vertical format to the
current line, analogously to how navigation (including wrapping)
in line-wise movement in the horizontal format is confined to the
current column.

* doc/emacs/mini.texi (Completion): Fix several typos and improve
wording is several places.
(Completion Commands): Document navigation of the *Completions*
buffer in the vertical format.  Document the difference between
format-sensitive movement and strictly column-wise or line-wise
movement.  Document 'minibuffer-complete-and-exit' and update the
documentation of 'minibuffer-completion-auto-choose' and
'minibuffer-choose-completion'.  Document the use of a numeric
prefix argument with the navigation commands.
(Completion Options): Rearrange and improve documentation of
'completions-sort', 'completions-format' and
'completion-auto-wrap', updating the latter to document the new
behavior.

* lisp/minibuffer.el (minibuffer-visible-completions-map): Rebind
"<left>" to 'minibuffer-previous-column-completion' and "<right>"
to 'minibuffer-next-column-completion'.
(minibuffer-next-completion): Add check for whether completions
format is vertical to decide whether to call
'next-line-completion' and replace calling 'next-completion' by
'next-column-completion'.
(minibuffer-next-column-completion)
(minibuffer-previous-column-completion): New commands.

* lisp/simple.el (completion-list-mode-map): Rebind "<left>" to
'previous-column-completion' and "<right>" to 'next-column-completion'.
(last-completion): Add handling for vertical completions format.
(completion--move-to-candidate-end): Always move point to the
position immediately after the last character of the completion
candidate.  This unifies the behavior, simplifies the
implementation and facilitates implementing the improved
navigation of the *Completions* buffer.
(previous-column-completion, next-column-completion): New
commands, replacing the previous definitions of
'previous-completion' and 'next-completion' to reflect their
column-wise operation.  Confine navigation (including wrapping) in
vertical format to the current line.
(previous-line-completion, next-line-completion): Implement
line-wise navigation (including wrapping) through all completions
in vertical format, not just those in the current column as in
horiztonal format.  Update doc strings.
(next-completion, previous-completion): Redefine to call
'{next,previous}-line-completion' when completions format is
vertical and '{next,previous}-column-completion' otherwise.

* test/lisp/minibuffer-tests.el
(completions-format-navigation--tests): New function providing a
template to define tests of the navigation and wrapping behavior
with specified numbers of completion candidates.
(completions-format-navigation-test-{2,3,4,5,10,15,16}): New tests.
This commit is contained in:
Stephen Berman 2025-09-22 16:04:42 +02:00
parent aeadaf7748
commit 77ca60b48d
5 changed files with 670 additions and 137 deletions

View file

@ -276,9 +276,11 @@ can fill in the rest, or some of it, based on what was typed so far.
@key{RET}, and @key{SPC}) are rebound in the minibuffer to special
completion commands (@pxref{Completion Commands}). These commands
attempt to complete the text in the minibuffer, based on a set of
@dfn{completion alternatives} provided by the command that requested
the argument. You can usually type @kbd{?} to see a list of
completion alternatives.
@dfn{completion alternatives} provided by the command that requested the
argument. You can usually type @kbd{?} to have Emacs pop up a buffer
(named @samp{*Completions*}) displaying the relevant completion
alternatives. As described in more detail below, you can navigate this
buffer and choose the desired completion from it.
Although completion is usually done in the minibuffer, the feature
is sometimes available in ordinary buffers too. @xref{Symbol
@ -336,8 +338,8 @@ at the end of the minibuffer, so that the minibuffer contains
@node Completion Commands
@subsection Completion Commands
Here is a list of the completion commands defined in the minibuffer
when completion is allowed.
Here is a list of the completion commands defined in the minibuffer or
the completions buffer when completion is available.
@table @kbd
@item @key{TAB}
@ -357,7 +359,7 @@ Display a list of completions and a few useful key bindings
(@code{minibuffer-completion-help}).
@item M-@key{DOWN}
@itemx M-@key{UP}
Navigate through list of completions.
Navigate through the list of completions.
@item M-v
@itemx M-g M-c
@itemx @key{PageUp}
@ -369,13 +371,17 @@ In the completions buffer, choose the completion at point.
@itemx mouse-2
In the completions buffer, choose the completion at mouse click.
@item @key{TAB}
@itemx @key{RIGHT}
@itemx @key{n}
@itemx n
In the completions buffer, move to the following completion candidate.
@item @key{S-TAB}
@itemx @key{LEFT}
@itemx @key{p}
@item S-@key{TAB}
@itemx p
In the completions buffer, move to the previous completion candidate.
@item @key{RIGHT}
@itemx @key{LEFT}
In the completions buffer, navigate column-wise through the completion list.
@item @key{DOWN}
@itemx @key{UP}
In the completions buffer, navigate line-wise through the completion list.
@item q
Quit the completions window and switch to the minibuffer window.
@item z
@ -416,19 +422,24 @@ with the completion list:
@findex minibuffer-next-completion
@findex minibuffer-previous-completion
@findex minibuffer-choose-completion
While in the minibuffer or in the completion list buffer, @kbd{M-@key{DOWN}}
(@code{minibuffer-next-completion} and @kbd{M-@key{UP}}
(@code{minibuffer-previous-completion}) navigate through the
completions displayed in the completions buffer. When
@code{minibuffer-completion-auto-choose} is non-@code{nil} (which is
the default), using these commands also inserts the current completion
candidate into the minibuffer. If
@code{minibuffer-completion-auto-choose} is @code{nil}, you can use
the @kbd{M-@key{RET}} command (@code{minibuffer-choose-completion}) to
insert the completion candidates into the minibuffer. By default,
that exits the minibuffer, but with a prefix argument, @kbd{C-u
M-@key{RET}} inserts the currently active candidate without exiting
the minibuffer.
@findex minibuffer-complete-and-exit
While in the minibuffer or in the completion list buffer,
@kbd{M-@key{DOWN}} (@code{minibuffer-next-completion}) and
@kbd{M-@key{UP}} (@code{minibuffer-previous-completion}) navigate
through the completions displayed in the completions buffer. These
commands are sensitive to the completion list format: they move point
column-wise when the value of @code{completions-format} is
@code{horizontal} and line-wise when its value is @code{vertical}
(@pxref{Completion Options}). If you set
@code{minibuffer-completion-auto-choose} to non-@code{nil}, using these
commands also inserts the current completion candidate into the
minibuffer. You can use @kbd{M-@key{RET}}
(@code{minibuffer-choose-completion}) or @kbd{@key{RET}}
(@code{minibuffer-complete-and-exit}) to choose the selected completion
candidate. By default, these commands exit the minibuffer, but with a
prefix argument (that is, @kbd{C-u M-@key{RET}} or @kbd{C-u @key{RET}})
they insert the currently selected candidate into the minibuffer without
exiting it.
@findex switch-to-completions
Typing @kbd{M-v}, while in the minibuffer, selects the window showing
@ -446,11 +457,27 @@ minibuffer, but doesn't exit the minibuffer---thus, you can change your
mind and choose another candidate.
@findex next-completion
While in the completion list buffer, you can use @kbd{@key{TAB}},
@kbd{@key{RIGHT}}, or @kbd{n} to move point to the following completion
candidate (@code{next-completion}). You can also use @kbd{@key{S-TAB}},
@kbd{@key{LEFT}}, and @kbd{p} to move point to the previous completion
alternative (@code{previous-completion}).
@findex previous-completion
While in the completion list buffer, you can use @kbd{@key{TAB}} or
@kbd{n} to move point to the following completion candidate
(@code{next-completion}) and @kbd{S-@key{TAB}} or @kbd{p} to move point
to the previous completion alternative (@code{previous-completion}).
Like @kbd{M-@key{DOWN}} and @kbd{M-@key{UP}}, these commands are also
sensitive to the completion list format.
@findex next-column-completion
@findex next-line-completion
@findex previous-column-completion
@findex previous-line-completion
In contrast, @kbd{@key{RIGHT}} (@code{next-column-completion}) and
@kbd{@key{LEFT}} (@code{previous-column-completion}) always move point
column-wise, regardless of the completion list format. Likewise,
@kbd{@key{DOWN}} (@code{next-line-completion}) and @kbd{@key{UP}}
(@code{previous-line-completion}) always move point line-wise.
All of these movement commands (and also @kbd{M-@key{DOWN}} and
@kbd{M-@key{UP}}) accept a numeric prefix argument @var{n}, which makes
point move to the @var{n}th following or preceding completion candidate.
@findex minibuffer-complete-history
@findex minibuffer-complete-defaults
@ -475,10 +502,14 @@ completion buffer and delete the window showing it
If the variable @code{minibuffer-visible-completions} is customized to
a non-@code{nil} value, it changes the commands bound to the arrow keys:
instead of moving in the minibuffer, they move between completion
candidates, like meta-arrow keys do by default. Similarly,
@kbd{@key{RET}} selects the current candidate, like @kbd{M-@key{RET}}
does normally. @code{C-g} hides the completion window, but leaves the
minibuffer active, so you can continue typing at the prompt.
candidates, like meta-arrow keys do by default (but note that, just as
when the window showing the completion list is selected, here too,
@kbd{@key{RIGHT}} and @kbd{@key{LEFT}} only move point column-wise and
@kbd{@key{DOWN}} and @kbd{@key{UP}} only move point line-wise,
regardless of the completion list format). Similarly, @kbd{@key{RET}}
selects the current candidate, like @kbd{M-@key{RET}} does normally.
@code{C-g} hides the completion window, but leaves the minibuffer
active, so you can continue typing at the prompt.
@node Completion Exit
@subsection Completion Exit
@ -725,18 +756,6 @@ then move to a candidate by cursor motion commands and select it with
@code{second-tab}, then the first @kbd{@key{TAB}} will pop up the
completions list buffer, and the second one will switch to it.
@findex previous-line-completion
@findex next-line-completion
@vindex completion-auto-wrap
When the window showing the completions is selected, either because
you customized @code{completion-auto-select} or because you switched to
it by typing @kbd{C-x o}, the @kbd{@key{UP}} and @kbd{@key{DOWN}} arrow
keys (@code{previous-line-completion} and @code{next-line-completion},
respectively) move by lines between completion candidates; with a prefix
numeric argument, they move that many lines. If
@code{completion-auto-wrap} is non-@code{nil}, these commands will wrap
at bottom and top of the candidate list.
@vindex completion-cycle-threshold
If @code{completion-cycle-threshold} is non-@code{nil}, completion
commands can cycle through completion alternatives. Normally, if
@ -750,24 +769,57 @@ in a cyclic manner. If you give @code{completion-cycle-threshold} a
numeric value @var{n}, completion commands switch to this cycling
behavior only when there are @var{n} or fewer alternatives.
@vindex completions-format
When displaying completions, Emacs will normally pop up a new buffer
to display the completions. The completions will by default be sorted
horizontally, using as many columns as will fit in the window-width,
but this can be changed by customizing the @code{completions-format}
user option. If its value is @code{vertical}, Emacs will sort the
completions vertically instead, and if it's @code{one-column}, Emacs
will use just one column.
@vindex completions-sort
The @code{completions-sort} user option controls the order in which
the completions are sorted in the @samp{*Completions*} buffer. The
default is @code{alphabetical}, which sorts in alphabetical order.
The value @code{nil} disables sorting; the value @code{historical}
sorts alphabetically first, and then rearranges according to the order
of the candidates in the minibuffer history. The value can also be a
function, which will be called with the list of completions, and
should return the list in the desired order.
The user option @code{completions-sort} controls the order in which
the completions are sorted in the @samp{*Completions*} buffer. With the
default value @code{alphabetical}, the completions are displayed in
alphabetical order. The value @code{nil} disables sorting; the value
@code{historical} sorts alphabetically first, and then rearranges
according to the order of the candidates in the minibuffer history. The
value can also be a function, which will be called with the list of
completions, and should return the list in the desired order.
@vindex completions-format
By default, the @samp{*Completions*} buffer lists the sorted
completions horizontally (that is, from left to right across the columns
and continuing to the next line below), using as many columns as will
fit in the window-width. You can change this by customizing the user
option @code{completions-format}. If its value is @code{vertical}, the
@samp{*Completions*} buffer lists the sorted completions vertically
(that is, within each column and continuing with the next column to the
left), and with the value @code{one-column}, all completions appear in a
single column.@footnote{If a completion candidate is too long for more
than one column to fit in the window, or it there are three or less
candidates, all completions appear in a single column even if the value
of @code{completions-format} is @code{horizontal} or @code{vertical}.}
@vindex completion-auto-wrap
When point is on the last completion candidate, typing a key that
moves point to the next candidate (according to the sorting order of
current format) ``wraps around'' by default, that is, moves point to the
first candidate in the completions buffer. Likewise, when point is on
the first candidate, typing a key to move to the previous candidate puts
point on the last candidate in the buffer. You can suppress this
wrapping around by customizing the user option
@code{completion-auto-wrap} to @code{nil}; then typing @kbd{@key{TAB}}
or @kbd{n} on the last candidate, or typing @kbd{S-@key{TAB}} or @kbd{p}
on the first candidate, does not move point.
With the arrow keys, the wrapping behavior depends on the completions
format in use. In the horizontal format, you can use @kbd{@key{RIGHT}}
and @kbd{@key{LEFT}}, and in the vertical format @kbd{@key{DOWN}} and
@kbd{@key{UP}}, exactly like the format-sensitive keys for moving
between completion candidates in the sorting order, and with the same
wrapping behavior. In contrast, in the horizontal format
@kbd{@key{DOWN}} and @kbd{@key{UP}} move---and wrap, if wrapping is
enabled---only within the current column of completions, and in the
vertical format @kbd{@key{RIGHT}} and @kbd{@key{LEFT}} move (and wrap,
if enabled) only within the current line of completions.@footnote{If the
completions buffer contains only single column even in the horizontal or
vertical format, then both the format-sensitive keys and the arrow keys
move point to the next (respectively, previous) candidate, wrapping only
at the last (respectively, first) candidate, if the value of
@code{completion-auto-wrap} is non-@code{nil}.}
@vindex completions-max-height
When @code{completions-max-height} is non-@code{nil}, it limits the

View file

@ -216,6 +216,23 @@ different values for completion-affecting variables like
applies for the styles configuration in 'completion-category-overrides'
and 'completion-category-defaults'.
+++++
*** Navigating "*Completions*" now accommodates 'completions-format'.
When 'completions-format' is set to 'vertical', typing 'n', '<TAB>' or
'M-<down>' in the "*Completions*" buffer (the latter also in the
minibuffer) now moves point to the completion candidate in the next line
in the current column, and wraps to the next column when typed on the
last completion candidate of the current column. Likewise, typing 'p',
'S-<TAB>' or 'M-<up>' moves point to the completion candidate in the
previous line or wraps to the previous column. Previously, these keys
ignored the vertical format, i.e., moved point only to the item in the
same line of the next or previous column, in accordance with the default
horizontal format. In vertical format, typing '<left>' and '<right>' in
the "*Completions*" buffer (and when 'minibuffer-visible-completions' is
non-nil, also in the minibuffer) moves point only within the current
line, analogously to how, in horizontal format, '<down>' and '<up>' move
point only within the current column.
---
*** Selected completion candidate is preserved across "*Completions*" updates.
When point is on a completion candidate in the "*Completions*" buffer
@ -2936,6 +2953,19 @@ by default and controlled by this variable; it can be set to non-nil
to keep the old behavior. This change is to accomodate screen
readers.
---
** 'next-completion' and 'previous-completion' now use 'completions-format'.
Previously, these commands only took horizontal format into account;
now, they call either '{next,previous}-line-completion' or the new
commands '{next,previous}-column-completion', depending on the value of
'completions-format'. The latter two commands improve and extend the
previous implementations of '{next,previous}-completion', which better
reflect that they only take the (default) horizontal completions format
into account. Any external code using '{next,previous}-completion' that
assumes the previous implementation must be adjusted accordingly; see
'minibuffer-next-completion' for an example of such an adjustment in
Emacs core.
+++
** A thread's current buffer can now be killed.
We introduce a new attribute for threads called 'buffer-disposition'.

View file

@ -3440,8 +3440,8 @@ displaying the *Completions* buffer exists."
(defvar-keymap minibuffer-visible-completions-map
:doc "Local keymap for minibuffer input with visible completions."
"<left>" (minibuffer-visible-completions--bind #'minibuffer-previous-completion)
"<right>" (minibuffer-visible-completions--bind #'minibuffer-next-completion)
"<left>" (minibuffer-visible-completions--bind #'minibuffer-previous-column-completion)
"<right>" (minibuffer-visible-completions--bind #'minibuffer-next-column-completion)
"<up>" (minibuffer-visible-completions--bind #'minibuffer-previous-line-completion)
"<down>" (minibuffer-visible-completions--bind #'minibuffer-next-line-completion)
"C-g" (minibuffer-visible-completions--bind #'minibuffer-hide-completions))
@ -5224,15 +5224,15 @@ selected by these commands to the minibuffer."
"Move to the next item in its completions window from the minibuffer.
When the optional argument VERTICAL is non-nil, move vertically
to the next item on the next line using `next-line-completion'.
Otherwise, move to the next item horizontally using `next-completion'.
Otherwise, move to the next item horizontally using `next-column-completion'.
When `minibuffer-completion-auto-choose' is non-nil, then also
insert the selected completion candidate to the minibuffer."
(interactive "p")
(let ((auto-choose minibuffer-completion-auto-choose))
(with-minibuffer-completions-window
(if vertical
(if (or vertical (eq completions-format 'vertical))
(next-line-completion (or n 1))
(next-completion (or n 1)))
(next-column-completion (or n 1)))
(when auto-choose
(let ((completion-auto-deselect nil))
(choose-completion nil t t))))))
@ -5262,6 +5262,26 @@ insert the selected completion candidate to the minibuffer."
(interactive "p")
(minibuffer-next-completion (- (or n 1)) t))
(defun minibuffer-next-column-completion (&optional n)
"Move to the next completion column from the minibuffer.
This means to move to the completion candidate in the next column
in the *Completions* buffer while point stays in the minibuffer.
When `minibuffer-completion-auto-choose' is non-nil, then also
insert the selected completion candidate to the minibuffer."
(interactive "p")
(with-minibuffer-completions-window
(next-column-completion (or n 1))))
(defun minibuffer-previous-column-completion (&optional n)
"Move to the previous completion column from the minibuffer.
This means to move to the completion candidate on the previous column
in the *Completions* buffer while point stays in the minibuffer.
When `minibuffer-completion-auto-choose' is non-nil, then also
insert the selected completion candidate to the minibuffer."
(interactive "p")
(with-minibuffer-completions-window
(next-column-completion (- (or n 1)))))
(defun minibuffer-choose-completion (&optional no-exit no-quit)
"Run `choose-completion' from the minibuffer in its completions window.
With prefix argument NO-EXIT, insert the completion candidate at point to

View file

@ -10068,8 +10068,8 @@ makes it easier to edit it."
(define-key map [remap keyboard-quit] #'delete-completion-window)
(define-key map [up] 'previous-line-completion)
(define-key map [down] 'next-line-completion)
(define-key map [left] 'previous-completion)
(define-key map [right] 'next-completion)
(define-key map [left] 'previous-column-completion)
(define-key map [right] 'next-column-completion)
(define-key map [?\t] 'next-completion)
(define-key map [backtab] 'previous-completion)
(define-key map [M-up] 'minibuffer-previous-completion)
@ -10159,21 +10159,54 @@ the completions is popped up and down."
(defun last-completion ()
"Move to the last item in the completions buffer."
(interactive)
;; Move to the last item in horizontal or one-column format.
(goto-char (previous-single-property-change
(point-max) 'mouse-face nil (point-min)))
;; Move to the start of last one.
;; Move to the start of the item.
(unless (get-text-property (point) 'mouse-face)
(when-let* ((pos (previous-single-property-change (point) 'mouse-face)))
(goto-char pos))))
(goto-char pos)))
;; In vertical format the last item is in the last column even if its
;; line number is less than that of the last item in earlier columns.
(when (eq completions-format 'vertical)
(let ((pt (point))
(col (current-column))
(last-col (progn
(first-completion)
(goto-char (pos-eol))
(goto-char (previous-single-property-change
(point) 'mouse-face))
(current-column))))
(if (zerop last-col)
;; If there is only one column of completions, the last
;; completion in vertical format is the same as in horizontal
;; format, so go there now.
(goto-char pt)
;; Otherwise, we set `pt' to the beginning of first item in last
;; column here because if the last column contains only one
;; item, `pt' will not be set below.)
(setq pt (point))
;; If all columns contain the same number of items, `col' (which
;; specifies the column of the last item in horizontal format)
;; equals `last-col', so the test must be with `>=', not `>'.
(when (>= last-col col)
(while (= (current-column) last-col)
(forward-line)
(unless (eobp)
(goto-char (pos-eol))
(move-to-column last-col)
(when (= (current-column) last-col)
(setq pt (point))))))
(goto-char pt)))))
(defun previous-completion (n)
"Move to the previous item in the completions buffer.
With prefix argument N, move back N items (negative N means move
(defun previous-column-completion (n)
"Move to the item in the previous column of the completions buffer.
With prefix argument N, move back N columns (negative N means move
forward).
Also see the `completion-auto-wrap' variable."
(interactive "p")
(next-completion (- n)))
(next-column-completion (- n)))
(defun completion--move-to-candidate-start ()
"If in a completion candidate, move point to its start."
@ -10183,21 +10216,26 @@ Also see the `completion-auto-wrap' variable."
(goto-char (previous-single-property-change (point) 'mouse-face))))
(defun completion--move-to-candidate-end ()
"If in a completion candidate, move point to its end."
(when (and (get-text-property (point) 'mouse-face)
(not (eobp))
(get-text-property (1+ (point)) 'mouse-face))
(goto-char (or (next-single-property-change (point) 'mouse-face) (point-max)))))
"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)
(point-max)))))
(defun next-completion (n)
"Move to the next item in the completions buffer.
With prefix argument N, move N items (negative N means move
(defun next-column-completion (n)
"Move to the item in the next column of the completions buffer.
With prefix argument N, move N columns (negative N means move
backward).
Also see the `completion-auto-wrap' variable."
(interactive "p")
(let ((tabcommand (member (this-command-keys) '("\t" [backtab])))
pos)
(one-col (save-excursion
(first-completion)
(completion--move-to-candidate-end)
(eolp)))
pos line last first)
(catch 'bound
(when (and (bobp)
(> n 0)
@ -10208,32 +10246,61 @@ Also see the `completion-auto-wrap' variable."
(setq n (1- n)))
(while (> n 0)
(setq pos (point))
(setq pos (point) line (line-number-at-pos)
last (if one-col
(save-excursion (and (forward-line) (eobp)))
(save-excursion (completion--move-to-candidate-end) (eolp))))
;; If in a completion, move to the end of it.
(when (get-text-property pos 'mouse-face)
(setq pos (next-single-property-change pos 'mouse-face)))
(when pos (setq pos (next-single-property-change pos 'mouse-face)))
(if pos
(if (and pos
(if last
(not (eq completions-format 'vertical))
t))
;; Move to the start of next one.
(goto-char pos)
;; If at the last completion option, wrap or skip
;; to the minibuffer, if requested.
(when completion-auto-wrap
(when (and completion-auto-wrap
(or one-col
(not (eq completions-format 'vertical))))
(if (and (eq completion-auto-select t) tabcommand
(minibufferp completion-reference-buffer))
(throw 'bound nil)
(first-completion))))
(when (and (eq completions-format 'vertical)
(or last
(= (point) (save-excursion (first-completion) (point)))))
(if (> (line-number-at-pos) line)
(forward-line -1)
(when completion-auto-wrap
(goto-char (pos-bol))
(completion--move-to-candidate-start))))
(setq n (1- n)))
(while (< n 0)
(setq pos (point))
(setq pos (point) line (line-number-at-pos)
first (if one-col
(save-excursion
(forward-line -1)
(not (get-text-property (point) 'mouse-face)))
(save-excursion (completion--move-to-candidate-start)
(bolp))))
;; If in a completion, move to the start of it.
(when (and (get-text-property pos 'mouse-face)
(not (bobp))
(get-text-property (1- pos) 'mouse-face))
(setq pos (previous-single-property-change pos 'mouse-face)))
(when pos (setq pos (previous-single-property-change pos 'mouse-face)))
(if pos
(if (and pos
(not (and completion-auto-wrap
(eq completions-format 'vertical)
(not one-col)
(bolp)))
(if first
(not (eq completions-format 'vertical))
t))
(progn
(goto-char pos)
;; Move to the start of that one.
@ -10243,11 +10310,19 @@ Also see the `completion-auto-wrap' variable."
;; If at the first completion option, wrap or skip
;; to the minibuffer, if requested.
(when completion-auto-wrap
(if (and (eq completion-auto-select t) tabcommand
(minibufferp completion-reference-buffer))
(progn
(throw 'bound nil))
(last-completion))))
(cond ((and (eq completions-format 'vertical)
(not one-col)
(or first (not pos)))
(when (> line (line-number-at-pos))
(forward-line))
(goto-char (1- (pos-eol)))
(completion--move-to-candidate-start))
((and (eq completion-auto-select t) tabcommand
(minibufferp completion-reference-buffer))
(progn
(throw 'bound nil)))
(t
(last-completion)))))
(setq n (1+ n))))
(when (/= 0 n)
@ -10255,7 +10330,11 @@ Also see the `completion-auto-wrap' variable."
(defun previous-line-completion (&optional n)
"Move to completion candidate on the previous line in the completions buffer.
With prefix argument N, move back N lines (negative N means move forward).
With prefix argument N, move back N lines (negative N means move
forward). In vertical format (see user option `completions-format')
this command moves line-wise through all columns in the completions
buffer, in horizontal format movement is confined to the current column
of completions.
Also see the `completion-auto-wrap' variable."
(interactive "p")
@ -10263,11 +10342,15 @@ Also see the `completion-auto-wrap' variable."
(defun next-line-completion (&optional n)
"Move to completion candidate on the next line in the completions buffer.
With prefix argument N, move N lines forward (negative N means move backward).
With prefix argument N, move N lines forward (negative N means move
backward). In vertical format (see user option `completions-format')
this command moves line-wise through all columns in the completions
buffer, in horizontal format movement is confined to the current column
of completions.
Also see the `completion-auto-wrap' variable."
(interactive "p")
(let (line column pos found)
(let (line column pos found last first)
(when (and (bobp)
(> n 0)
(get-text-property (point) 'mouse-face)
@ -10292,54 +10375,109 @@ Also see the `completion-auto-wrap' variable."
((< n 0) (first-completion)))))
(while (> n 0)
(setq found nil pos nil column (current-column) line (line-number-at-pos))
(completion--move-to-candidate-end)
(while (and (not found)
(eq (forward-line 1) 0)
(not (eobp))
(move-to-column column))
(when (get-text-property (point) 'mouse-face)
(setq found t)))
(when (not found)
(if (not completion-auto-wrap)
(last-completion)
(save-excursion
(goto-char (point-min))
(when (and (eq (move-to-column column) column)
(get-text-property (point) 'mouse-face))
(setq pos (point)))
(while (and (not pos) (> line (line-number-at-pos)))
(forward-line 1)
(setq found nil pos (point) column (current-column)
line (line-number-at-pos)
last (= (point) (save-excursion (last-completion) (point))))
(if (and (eq completions-format 'vertical)
completion-auto-wrap last)
(first-completion) ; Wrap from last to first item.
(completion--move-to-candidate-end)
(while (and (not found)
(eq (forward-line 1) 0)
(not (eobp))
(move-to-column column))
(when (get-text-property (point) 'mouse-face)
(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))
(last-completion))
(goto-char pos)))
t
(save-excursion
(setq pos nil)
(goto-char (point-min))
(when (and (eq (move-to-column column) column)
(get-text-property (point) 'mouse-face))
(setq pos (point)))))
(if pos (goto-char pos))))
(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))
(setq pos (point)))))
(if pos (goto-char pos))
(when (eq completions-format 'vertical)
(next-column-completion 1))))) ; Move to next column.
(setq n (1- n)))
(while (< n 0)
(setq found nil pos nil column (current-column) line (line-number-at-pos))
(completion--move-to-candidate-start)
(while (and (not found)
(eq (forward-line -1) 0)
(move-to-column column))
(when (get-text-property (point) 'mouse-face)
(setq found t)))
(when (not found)
(if (not completion-auto-wrap)
(first-completion)
(save-excursion
(goto-char (point-max))
(when (and (eq (move-to-column column) column)
(get-text-property (point) 'mouse-face))
(setq pos (point)))
(while (and (not pos) (< line (line-number-at-pos)))
(forward-line -1)
(setq found nil pos (point) column (current-column)
line (line-number-at-pos)
first (= (point) (save-excursion (first-completion) (point))))
(if (and (eq completions-format 'vertical)
completion-auto-wrap first)
(last-completion) ; Wrap from first to last item.
(completion--move-to-candidate-start)
(while (and (not found)
(eq (forward-line -1) 0)
(move-to-column column))
(when (get-text-property (point) 'mouse-face)
(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))
first (first-completion))
(goto-char pos)))
t
(save-excursion
(setq pos nil)
(goto-char (point-max))
(when (and (eq (move-to-column column) column)
(get-text-property (point) 'mouse-face))
(setq pos (point)))))
(if pos (goto-char pos))))
(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))
(setq pos (point)))))
(if pos (goto-char pos))
(when (eq completions-format 'vertical)
(previous-column-completion 1) ; Move to previous column.
(setq column (current-column))
;; Move to last item in this column (previous column may
;; have fewer items).
(while (not (eobp))
(move-to-column column)
(setq pos (point))
(forward-line))
(goto-char pos)))))
(setq n (1+ n)))))
(defun next-completion (&optional n)
"Move according to `completions-format' to next completion item.
In horizontal format movement is between columns within the same line,
in vertical format between lines within the same column. With non-nil
`completion-auto-wrap', movement continues to the next line or column,
respectively."
(interactive "p")
(pcase completions-format
('vertical (next-line-completion n))
(_ (next-column-completion n))))
(defun previous-completion (&optional n)
"Move according to `completions-format' to previous completion item.
In horizontal format movement is between columns within the same line,
in vertical format between lines within the same column. With non-nil
`completion-auto-wrap', movement continues to the next line or column,
respectively."
(interactive "p")
(pcase completions-format
('vertical (previous-line-completion n))
(_ (previous-column-completion n))))
(defvar choose-completion-deselect-if-after nil
"If non-nil, don't choose a completion candidate if point is right after it.

View file

@ -734,10 +734,303 @@
(let ((completion-auto-wrap nil))
(first-completion)
(next-line-completion 7)
(should (equal "ac2" (get-text-property (point) 'completion--string)))
(should (equal "ac1" (get-text-property (point) 'completion--string)))
(previous-line-completion 7)
(should (equal "aa1" (get-text-property (point) 'completion--string))))))
(defun completions-format-navigation--tests (n)
"Make tests for navigating buffer of N completion candidate.
The tests check expected results of navigating with and without wrapping
for combinations of the values of `completion-auto-wrap' and
`completions-format' (see bug#78959 for motivation and discussion of the
expected behavior). The tests are actually run by calling this
function, with specific values of N (> 1 to have a \"*Completions*\"
buffer), from functions defined by `ert-deftest.'"
(let* (
;; Make list of N unique completions.
(letters (mapcar 'string (number-sequence 97 122)))
(gen-compl (lambda (x)
(let (comps)
(dotimes (_ x)
(push (concat (car comps) (pop letters)) comps))
(nreverse comps))))
(completions (funcall gen-compl n))
;; Navigation tests.
;; (i) For both horizontal and vertical formats.
(all-completions
(lambda (type)
(let ((next-fn (pcase type
('any 'next-completion)
('column 'next-column-completion)
('line 'next-line-completion)))
(prev-fn (pcase type
('any 'previous-completion)
('column 'previous-column-completion)
('line 'previous-line-completion))))
(completing-read-with-minibuffer-setup completions
(insert (car completions))
(minibuffer-completion-help)
(switch-to-completions)
;; Sanity check that we're on first completion candidate.
(should
(equal (car completions)
(get-text-property (point) 'completion--string)))
;; Double check.
(first-completion)
(should
(equal (car completions)
(get-text-property (point) 'completion--string)))
;; Test moving from first to Ith next completion
;; candidate (0<I<N-1).
(dolist (i (number-sequence 1 (1- n)))
(funcall next-fn i)
(should
(equal (nth i completions)
(get-text-property (point) 'completion--string)))
(if (< i (1- n))
(first-completion)
(last-completion)
(should
(equal (nth i completions)
(get-text-property (point) 'completion--string)))
(funcall next-fn 1)
(if completion-auto-wrap
;; Wrap around to first completion candidate.
(should
(equal (car completions)
(get-text-property (point) 'completion--string)))
;; No wrapping.
(should
(equal (nth i completions)
(get-text-property (point) 'completion--string))))))
(last-completion)
;; Test moving from last to Ith previous completion
;; candidate (0<I<N-1).
(dolist (i (number-sequence 1 (1- n)))
(funcall prev-fn i)
(should
(equal (nth (- n i 1) completions)
(get-text-property (point) 'completion--string)))
(if (< i (1- n))
(last-completion)
(first-completion)
(should
(equal (car completions)
(get-text-property (point) 'completion--string)))
(funcall prev-fn 1)
(if completion-auto-wrap
;; Wrap around to last completion candidate.
(should
(equal (nth (1- n) completions)
(get-text-property (point) 'completion--string)))
;; No wrapping.
(should
(equal (car completions)
(get-text-property (point)
'completion--string))))))))))
;; (ii) Only for horizontal format.
(within-column
(lambda ()
(completing-read-with-minibuffer-setup completions
(insert (car completions))
(minibuffer-completion-help)
(switch-to-completions)
(while (not (eolp))
(completion--move-to-candidate-start)
(let* ((first (get-text-property (point) 'completion--string))
(pos (point))
(i 0)
last1 last2)
;; Keep moving to next completion candidate in this
;; column until we reach the last one, and then wrap
;; back to the first candidate, if
;; `completion-auto-wrap' is non-nil, otherwise stay
;; on the last one.
(while (or (and completion-auto-wrap
(not (equal last1 first)))
(not (equal last1
(get-text-property
(point) 'completion--string))))
(setq last2 last1)
(next-line-completion 1)
(incf i)
;; Set `last1' to either the first or last
;; candidate, depending on the value of
;; `completion-auto-wrap'.
(setq last1 (get-text-property (point) 'completion--string)))
(setq last1 last2
last2 nil)
(decf i)
(when completion-auto-wrap
(should (equal (get-text-property (point) 'completion--string)
first))
;; Test wrapping from last to first line in this column.
(next-line-completion i) ; Move to last candidate.
(should (equal (get-text-property (point) 'completion--string)
last1)))
;; Now keeping move from last to first completion
;; candidate in this column.
(while (or (not (equal last2 first))
(not (equal last2
(get-text-property
(point) 'completion--string))))
(previous-line-completion 1)
(setq last2 (get-text-property (point) 'completion--string)))
;; Test wrapping from first to last line in this column.
(when completion-auto-wrap
(should (equal (get-text-property (point) 'completion--string)
first))
(next-line-completion i)
(should (equal (get-text-property (point) 'completion--string)
last1)))
;; Move to first candidate in next column to continue loop
(completion--move-to-candidate-end)
(unless (eolp)
(goto-char pos)
(next-column-completion 1)))))))
;; (iii) Only for vertical format.
(within-line
(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))))
(while (not (eobp))
(let ((first (get-text-property (point) 'completion--string))
last pos)
;; Test moving to next column in this line.
(while (not (eolp))
(next-column-completion 1)
(let ((next (get-text-property (point) 'completion--string)))
(should
;; FIXME: tautology?
(equal (nth (seq-position completions next) completions)
next)))
;; If this is the last completion in this line,
;; exit the loop.
(when (or (> (current-column) 0)
(save-excursion (and (forward-line) (eobp)))
(unless one-col
(save-excursion
(and (progn
(completion--move-to-candidate-start)
(bolp))
(progn
(completion--move-to-candidate-end)
(eolp))))))
(completion--move-to-candidate-end)))
(backward-char)
(completion--move-to-candidate-start)
(setq last (get-text-property (point) 'completion--string)
pos (point))
;; We're on the last column, so next move either
;; wraps or stays put.
(next-column-completion 1)
(if completion-auto-wrap
;; We wrapped around to first candidate in this line.
(progn
(should (bolp))
(should
(equal (get-text-property (point) 'completion--string)
first))
;; Go back to last completion in this line for next test.
(goto-char (if one-col pos (pos-eol)))
(backward-char))
(should
(equal (get-text-property (point) 'completion--string)
last)))
;; Test moving to previous column in this line.
(while (if one-col
(save-excursion
(forward-line -1)
(get-text-property (point) 'completion--string))
(not (bolp)))
(previous-column-completion 1)
(let ((prev (get-text-property (point) 'completion--string)))
(should
;; FIXME: tautology?
(equal (nth (seq-position completions prev) completions)
prev))))
;; We're on the first column, so next move either
;; wraps or stay put.
(previous-column-completion 1)
(if completion-auto-wrap
;; We wrapped around to last candidate in this line.
(progn
(completion--move-to-candidate-end)
(should (eolp))
(backward-char)
(should
(equal (get-text-property (point) 'completion--string)
last)))
;; We stayed on the first candidate.
(should
(equal (get-text-property (point) 'completion--string)
first)))
(if one-col
(goto-char (point-max))
(forward-line)))))))))
;; Run tests.
;; Test navigation with wrapping...
(let ((completion-auto-wrap t))
;; ...in horizontal format,
(let ((completions-format 'horizontal))
(funcall all-completions 'any)
(funcall all-completions 'column)
(funcall within-column))
;; ...in vertical format.
(let ((completions-format 'vertical))
(funcall all-completions 'any)
(funcall all-completions 'line)
(funcall within-line)))
;; Test navigation without wrapping...
(let ((completion-auto-wrap nil))
;; ...in horizontal format,
(let ((completions-format 'horizontal))
(funcall all-completions 'any)
(funcall all-completions 'column)
(funcall within-column))
;; ...in vertical format.
(let ((completions-format 'vertical))
(funcall all-completions 'any)
(funcall all-completions 'line)
(funcall within-line)))))
;; (ert-deftest completions-format-navigation-test-1 ()
;; (completions-format-navigation--tests 1))
(ert-deftest completions-format-navigation-test-2 ()
(completions-format-navigation--tests 2))
(ert-deftest completions-format-navigation-test-3 ()
(completions-format-navigation--tests 3))
(ert-deftest completions-format-navigation-test-4 ()
(completions-format-navigation--tests 4))
(ert-deftest completions-format-navigation-test-5 ()
(completions-format-navigation--tests 5))
(ert-deftest completions-format-navigation-test-9 ()
(completions-format-navigation--tests 9))
(ert-deftest completions-format-navigation-test-10 ()
(completions-format-navigation--tests 10))
(ert-deftest completions-format-navigation-test-15 ()
(completions-format-navigation--tests 15))
(ert-deftest completions-format-navigation-test-16 ()
(completions-format-navigation--tests 16))
(ert-deftest completion-cycle ()
(completing-read-with-minibuffer-setup '("aaa" "bbb" "ccc")
(let ((completion-cycle-threshold t))