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