Subject: bug#77253: 30.1.50; Support RET choosing the selected completion without rebinding arrow keys

From: Spencer Baugh <[email-masked]>
Date: Thu, 03 Apr 2025 13:52:44 -0400

Juri Linkov <[email-masked]> writes:

>> - It's not necessarily obvious that closing the completions window will
>>   make the arrow keys operate in the minibuffer.  Actually, maybe we
>>   should update the completion help text to say that?  That might help a
>>   lot.
>
> Maybe it's possible to squeeze this help text just into one additional line?
> Something like:
>
>   Type 'C-g' to close this window and restore arrows to move point.

Maybe we don't even need an additional line?  How about:

Click or type RET on a completion to select it, or C-g to close this window.
Type <right>, <left>, <down>, <up> to move point between completions.

>> - The new completion-eager-display makes *Completions* pop up
>>   immediately; when this happens, it can be surprising that the arrow
>>   keys don't work in the minibuffer from the very start of the
>>   minibuffer session.
>
> Then another possible value for 'minibuffer-visible-completions'
> would be to not rebind left/right arrows when the completions window
> contains only one column (like on the browser's address bar).

True.  Though I guess that could maybe be the default behavior when
completions-format=one-column.

>>> Not sure if it's possible for RET to accept the selected candidate
>>> by default since users might prefer to accept text in the minibuffer.
>>
>> If there's a selected candidate, though, then users have already decided
>> to use M-<up>/M-<down> to interact with completions.  (Or they've
>> switched to the completions buffer and selected one)
>>
>> If they decide they don't want the candidate they selected, and want to
>> continue with text in the minibuffer, completion-auto-deselect will
>> automatically deselect the candidate if they type anything.
>>
>> I think these two facts combined make it possible for RET to accept the
>> selected candidate by default.
>
> Makes sense.  So we need to try how well it performs.

How about this?

(BTW, after writing minibuffer--completions-visible, I'm wondering if
most of the code which calls (get-buffer-window "*Completions*" 0) is
actually buggy, because it's not checking completion-reference-buffer.
Should we replace ~all the calls to (get-buffer-window "*Completions*"
0) with (minibuffer--completions-visible) which does check that?)

diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el
index 404fa143194..c026e41a818 100644
--- a/lisp/minibuffer.el
+++ b/lisp/minibuffer.el
@@ -3209,6 +3209,12 @@ completion-help-at-point
   (define-key map "\n" 'exit-minibuffer)
   (define-key map "\r" 'exit-minibuffer))
 
+(defun minibuffer-choose-completion-just-exit (&optional no-exit)
+  "Choose the selected completion from the minibuffer or call `exit-minibuffer'."
+  (interactive "P")
+  (or (minibuffer-choose-completion-if-selected no-exit)
+      (minibuffer-exit)))
+
 (defvar-keymap minibuffer-local-completion-map
   :doc "Local keymap for minibuffer input with completion."
   :parent minibuffer-local-map
@@ -3218,6 +3224,7 @@ minibuffer-local-completion-map
   ;; another binding for it.
   ;; "M-TAB"  #'minibuffer-force-complete
   "SPC"       #'minibuffer-complete-word
+  "RET"       #'minibuffer-choose-completion-just-exit
   "?"         #'minibuffer-completion-help
   "<prior>"   #'switch-to-completions
   "M-v"       #'switch-to-completions
@@ -3229,7 +3236,7 @@ minibuffer-local-completion-map
 (defvar-keymap minibuffer-local-must-match-map
   :doc "Local keymap for minibuffer input with completion, for exact match."
   :parent minibuffer-local-completion-map
-  "RET" #'minibuffer-complete-and-exit
+  "RET" #'minibuffer-choose-completion-or-exit
   "C-j" #'minibuffer-complete-and-exit)
 
 (defvar-keymap minibuffer-local-filename-completion-map
@@ -3326,18 +3333,34 @@ minibuffer-visible-completions
 (defvar minibuffer-visible-completions--always-bind nil
   "If non-nil, force the `minibuffer-visible-completions' bindings on.")
 
+(defun minibuffer--completions-visible ()
+  "Return the window where the *Completions* buffer for this minibuffer is visible."
+  (when-let ((window (get-buffer-window "*Completions*" 0)))
+    (when (eq (buffer-local-value 'completion-reference-buffer
+                                  (window-buffer window))
+              (window-buffer (active-minibuffer-window)))
+      window)))
+
+(defun minibuffer-choose-completion-if-selected (&optional no-exit no-quit)
+  "Like `minibuffer-choose-completion', but do nothing if no candidate is selected.
+
+Return non-nil if a completion was chosen."
+  (when-let* ((window (minibuffer--completions-visible)))
+    (with-selected-window window
+      ;; Detect selection as if `choose-completion-deselect-if-after' is nil.
+      (when (get-text-property (point) 'completion--string)
+        (choose-completion nil no-exit no-quit)
+        t))))
+
 (defun minibuffer-visible-completions--filter (cmd)
   "Return CMD if `minibuffer-visible-completions' bindings should be active."
   (if minibuffer-visible-completions--always-bind
       cmd
-    (when-let ((window (get-buffer-window "*Completions*" 0)))
-      (when (and (eq (buffer-local-value 'completion-reference-buffer
-                                         (window-buffer window))
-                     (window-buffer (active-minibuffer-window)))
-                 (if (eq cmd #'minibuffer-choose-completion-or-exit)
-                     (with-current-buffer (window-buffer window)
-                       (get-text-property (point) 'completion--string))
-                   t))
+    (when-let ((window (minibuffer--completions-visible)))
+      (when (if (eq cmd #'minibuffer-choose-completion-or-exit)
+                (with-current-buffer (window-buffer window)
+                  (get-text-property (point) 'completion--string))
+              t)
         cmd))))
 
 (defun minibuffer-visible-completions--bind (binding)
@@ -5109,10 +5132,8 @@ minibuffer-choose-completion-or-exit
 in the completions window, then exit the minibuffer using its present
 contents."
   (interactive "P")
-  (condition-case nil
-      (let ((choose-completion-deselect-if-after t))
-        (minibuffer-choose-completion no-exit no-quit))
-    (error (minibuffer-complete-and-exit))))
+  (or (minibuffer-choose-completion-if-selected no-exit no-quit)
+      (minibuffer-complete-and-exit)))
 
 (defun minibuffer-complete-history ()
   "Complete as far as possible using the minibuffer history.