bug#81411: 31.0.90; Selecting completion for completing-read-multiple does not respect completion boundaries

Aaron Zeng via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]>
Newsgroups gmane.emacs.bugs
Message-ID <CAB7SQMHYTSzfcJbY7qcXpAVMOUtcETg+iKAQuRt_qn5S_2ZAUA@mail.gmail.com>
On Thu, Aug 6, 2026 at 3:32 PM Stefan Monnier <[email protected]> wrote:
> >>From 16ae3cd8b28015e5baa8c8d0774e134c2ae4aaaf Mon Sep 17 00:00:00 2001
> > From: "Aaron L. Zeng" <[email protected]>
> > Date: Tue, 21 Jul 2026 14:02:33 -0400
> > Subject: [PATCH 2/2] Fix choose-completion in CRM with boundaries
> >
> > * lisp/emacs-lisp/crm.el (completing-read-multiple): Respect
> > completion boundaries when inserting a choice.
> > ---
> >  lisp/emacs-lisp/crm.el | 13 ++++++++++++-
> >  1 file changed, 12 insertions(+), 1 deletion(-)
> >
> > diff --git a/lisp/emacs-lisp/crm.el b/lisp/emacs-lisp/crm.el
> > index 9be7913a954..e46570d1a47 100644
> > --- a/lisp/emacs-lisp/crm.el
> > +++ b/lisp/emacs-lisp/crm.el
> > @@ -263,8 +263,19 @@ completing-read-multiple
> >                      'crm--choose-completion-string nil 'local)
> >            (setq-local completion-list-insert-choice-function
> >                        (lambda (_start _end choice)
> > +                        ;; Respect the boundaries of the current
> > +                        ;; element, and within that, any boundaries
> > +                        ;; specified by the completion table
> > +                        ;; (bug#81411).
> >                          (crm--completion-command beg end
> > -                          (completion--replace beg end choice))))
> > +                          (let ((bounds (completion-boundaries
> > +                                         (buffer-substring beg (point))
> > +                                         crm-completion-table
> > +                                         minibuffer-completion-predicate
> > +                                         (buffer-substring (point) end))))
> > +                            (completion--replace (+ beg (car bounds))
> > +                                                 (+ (point) (cdr bounds))
> > +                                                 choice)))))
> >            (setq-local crm-completion-table table)
> >            (use-local-map map))
> >        (setq input (completing-read
>
> But here I have the impression that it will misfire when, say, the user
> typed `/usr/s/z` and selects `share/zoneinfo` from the *Completions*
> buffer: AFAICT it will result in `/usr/s/share/zoneinfo` instead of
> `/usr/share/zoneinfo`: the boundaries information can't come directly
> from the completion-table but have to arrive from the
> *Completions* buffer.  I can't remember how we handle this in the
> non-CRM case, but we need to use the same info.

Thanks for catching that: it was indeed behaving as you described.
While writing new tests, I also found a different bug where completing
"d/b" with point between "d" and "/", then selecting "dir/" would
result in "dir//b" instead of the desired "dir/b".

In figuring out how to solve all these problems, it seems like the
best thing to do is not override
completion-list-insert-choice-function at all (and just use the one
set up by minibuffer-completion-help).  As far as I can tell, it is
fine to do this because crm-completion-help calls
minibuffer-completion-help with the appropriate field boundaries, so
that already solves the original issue.  I see that
completion-list-insert-choice-function is used in a few places, but it
seems to mostly be code that keeps it in sync between the minibuffer
and the completion list buffer.

I also added tests to confirm that CRM works with (1) multi-character
separators and (2) choose-completion respects the element boundaries.

New patch attached.
0001-Fix-choose-completion-in-CRM-with-boundaries.patch (text/x-patch, 7.3 KB)
From 4fd9ff48493f40f1e256f889bf7edf6a01559e6b Mon Sep 17 00:00:00 2001
From: "Aaron L. Zeng" <[email protected]>
Date: Thu, 13 Aug 2026 17:25:06 -0400
Subject: [PATCH] Fix choose-completion in CRM with boundaries

* lisp/emacs-lisp/crm.el (crm--choose-completion-string): Delete.
(completing-read-multiple): Set buffer-local
completion-no-auto-exit instead of using
choose-completion-string-functions.  Do not set
completion-list-insert-choice-function.

* test/lisp/emacs-lisp/crm-tests.el: New tests for completing-read-multiple.
---
 lisp/emacs-lisp/crm.el            |  27 +-------
 test/lisp/emacs-lisp/crm-tests.el | 103 ++++++++++++++++++++++++++++++
 2 files changed, 104 insertions(+), 26 deletions(-)
 create mode 100644 test/lisp/emacs-lisp/crm-tests.el

diff --git a/lisp/emacs-lisp/crm.el b/lisp/emacs-lisp/crm.el
index b68d3d55525..f94f079779b 100644
--- a/lisp/emacs-lisp/crm.el
+++ b/lisp/emacs-lisp/crm.el
@@ -217,17 +217,6 @@ crm-complete-and-exit
       (goto-char (match-end 0)))
     (if doexit (exit-minibuffer))))
 
-(defun crm--choose-completion-string (choice buffer base-position
-                                             &rest _ignored)
-  "Completion string chooser for `completing-read-multiple'.
-This is called from `choose-completion-string-functions'.
-It replaces the string that is currently being completed, without
-exiting the minibuffer."
-  (let ((completion-no-auto-exit t)
-        (choose-completion-string-functions nil))
-    (choose-completion-string choice buffer base-position)
-    t))
-
 ;; superemulates behavior of completing_read in src/minibuf.c
 ;; Use \\<crm-local-completion-map> so that help-enable-autoload can
 ;; do its thing.  Any keymap that is defined will do.
@@ -259,21 +248,7 @@ completing-read-multiple
          input)
     (minibuffer-with-setup-hook
         (lambda ()
-          (add-hook 'choose-completion-string-functions
-                    'crm--choose-completion-string nil 'local)
-          (setq-local completion-list-insert-choice-function
-                      (lambda (_start _end choice)
-                        (let* ((beg (save-excursion
-                                      (if (search-backward-regexp crm-separator
-                                                                  (field-beginning)
-                                                                  t)
-                                          (1+ (point))
-                                        (minibuffer-prompt-end))))
-                               (end (save-excursion
-                                      (if (search-forward-regexp crm-separator nil t)
-                                          (1- (point))
-                                        (point-max)))))
-                          (completion--replace beg end choice))))
+          (setq-local completion-no-auto-exit t)
           (setq-local crm-completion-table table)
           (use-local-map map))
       (setq input (completing-read
diff --git a/test/lisp/emacs-lisp/crm-tests.el b/test/lisp/emacs-lisp/crm-tests.el
new file mode 100644
index 00000000000..56b662d4a33
--- /dev/null
+++ b/test/lisp/emacs-lisp/crm-tests.el
@@ -0,0 +1,103 @@
+;;; crm-tests.el --- Tests for crm.el  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026-2026 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(require 'crm)
+(require 'ert)
+
+;; Copied from minibuffer-tests.el
+(defmacro with-minibuffer-setup (completing-read &rest body)
+  (declare (indent 1) (debug t))
+  `(catch 'result
+     (minibuffer-with-setup-hook
+         (lambda ()
+           (let ((redisplay-skip-initial-frame nil)
+                 (executing-kbd-macro nil)) ; Don't skip redisplay
+             (throw 'result (progn . ,body))))
+       (let ((executing-kbd-macro t)) ; Force the real minibuffer
+         ,completing-read))))
+
+(defmacro crm-test-with-file-name-table (initial-input &rest body)
+  (declare (indent 1) (debug t))
+  `(let* ((root (make-temp-file "crm-boundary-" 'directory))
+          (default-directory root))
+     (unwind-protect
+         (progn
+           (make-directory "dir")
+           (make-directory "dir/subdir")
+           (with-temp-file "dir/alpha" (insert ""))
+           (with-temp-file "dir/beta" (insert ""))
+           (with-temp-file "dir/subdir/gamma" (insert ""))
+           (with-minibuffer-setup
+               (completing-read-multiple "Pick: "
+                                         #'completion-file-name-table
+                                         nil t ,initial-input)
+             ,@body))
+       (delete-directory root t))))
+
+(ert-deftest crm-test-complete-uses-boundaries ()
+  (crm-test-with-file-name-table "dir/a"
+    (save-excursion (insert ",d/b"))
+    (should (equal (minibuffer-contents) "dir/a,d/b"))
+    (execute-kbd-macro (kbd "TAB"))
+    (should (equal (minibuffer-contents) "dir/alpha,d/b"))
+    ;; complete between d and /
+    (execute-kbd-macro (kbd "C-f C-f TAB"))
+    (should (equal (minibuffer-contents) "dir/alpha,dir/b"))
+    (execute-kbd-macro (kbd "C-e TAB"))
+    (should (equal (minibuffer-contents) "dir/alpha,dir/beta"))
+    (execute-kbd-macro (kbd ", d/s/g TAB"))
+    (should (equal (minibuffer-contents) "dir/alpha,dir/beta,dir/subdir/gamma"))))
+
+(ert-deftest crm-test-choose-completion-uses-boundaries ()
+  "Regression test for Bug#81411."
+  (crm-test-with-file-name-table "dir/a"
+    (save-excursion (insert ",d/b"))
+    (should (equal (minibuffer-contents) "dir/a,d/b"))
+    (execute-kbd-macro (kbd "? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha,d/b"))
+    ;; complete between d and /
+    (execute-kbd-macro (kbd "C-f C-f ? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha,dir/b"))
+    (execute-kbd-macro (kbd "C-e ? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha,dir/beta"))
+    (execute-kbd-macro (kbd ", d/s/g ? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha,dir/beta,dir/subdir/gamma"))))
+
+(ert-deftest crm-test-choose-completion-multi-char-separator ()
+  "Regression test for Bug#81411."
+  (crm-test-with-file-name-table "dir/a"
+    (save-excursion (insert " ,  d/b"))
+    (should (equal (minibuffer-contents) "dir/a ,  d/b"))
+    (execute-kbd-macro (kbd "? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha ,  d/b"))
+    ;; complete between d and /
+    (forward-char 5)
+    (execute-kbd-macro (kbd "? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha ,  dir/b"))
+    (execute-kbd-macro (kbd "C-e ? M-<down> M-RET"))
+    (should (equal (minibuffer-contents) "dir/alpha ,  dir/beta"))))
+
+(provide 'crm-tests)
+;;; crm-tests.el ends here
-- 
2.43.7
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.