diff --git a/doc/emacs/mini.texi b/doc/emacs/mini.texi index c1b11f1ab1c..a9dfefc58f8 100644 --- a/doc/emacs/mini.texi +++ b/doc/emacs/mini.texi @@ -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 diff --git a/etc/NEWS b/etc/NEWS index d3e25bf5d5d..602c375dc2f 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -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', '' or +'M-' 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-' or 'M-' 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 '' and '' 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, '' and '' 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'. diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el index 70ad0c7abc5..ccd2314fac7 100644 --- a/lisp/minibuffer.el +++ b/lisp/minibuffer.el @@ -3440,8 +3440,8 @@ displaying the *Completions* buffer exists." (defvar-keymap minibuffer-visible-completions-map :doc "Local keymap for minibuffer input with visible completions." - "" (minibuffer-visible-completions--bind #'minibuffer-previous-completion) - "" (minibuffer-visible-completions--bind #'minibuffer-next-completion) + "" (minibuffer-visible-completions--bind #'minibuffer-previous-column-completion) + "" (minibuffer-visible-completions--bind #'minibuffer-next-column-completion) "" (minibuffer-visible-completions--bind #'minibuffer-previous-line-completion) "" (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 diff --git a/lisp/simple.el b/lisp/simple.el index 2a13d59e5cd..fe5eb4da60a 100644 --- a/lisp/simple.el +++ b/lisp/simple.el @@ -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. diff --git a/test/lisp/minibuffer-tests.el b/test/lisp/minibuffer-tests.el index 6954643976a..27e9bbbefb4 100644 --- a/test/lisp/minibuffer-tests.el +++ b/test/lisp/minibuffer-tests.el @@ -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 (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))