Subject: bug#76028: 31; completing-read-multiple: Add prompt indicator

From: Daniel Mendler <[email-masked]>
Date: Mon, 03 Feb 2025 16:51:15 +0100

Daniel Mendler <[email-masked]> writes:

> Stefan Kangas <[email-masked]> writes:
>
>> Daniel Mendler via "Bug reports for GNU Emacs, the Swiss army knife of
>> text editors" <[email-masked]> writes:
>>
>>> I propose to add such an indicator to CRM by default. Above I have
>>> chosen the tag "CRM" for its shortness, but a more user friendly
>>> solution might require a longer more descriptive name. Furthermore the
>>> code above extracts the separator from the regular expression
>>> `crm-separator', which is not robust for all separators.
>>
>> How about " [comma-separated list] "?
>>
>> I'd also propose we should have a defcustom for it, allowing users to
>> set it to something shorter if they want.
>
> Yes, this would look good. Note that `crm-separator' can be set to an
> arbitrary regular expression. How can we turn `crm-separator' into a
> string like "comma-separated list"? Maybe `crm-separator' should be
> generalized? One could set it to a cons ("," . "comma-separated list")
> with the regexp in the car and the description in the cdr. In cases
> where `crm-separator' is a string, we could use a generic string like
> "[multi list]".

I have attached a patch to this mail which implements this proposal.

Daniel

>From 72f3b3890d138c7a5cd1b4da9f1918d5cf170a74 Mon Sep 17 00:00:00 2001
From: Daniel Mendler <[email-masked]>
Date: Mon, 3 Feb 2025 16:39:49 +0100
Subject: [PATCH] completing-read-multiple: CRM indication and prompt
 customization

The `completing-read-multiple' prompt indicates multi
completion.  The customization option `crm-prompt' configures
the formatting of the prompt.  The variable can be set for
example to "%p" in order to only display the original prompt, to
"[%d] %p" to display the separator description and the prompt,
or to "[CRM %s] %p" to display a shorter indicator of only the
separator string and the prompt.

* lisp/emacs-lisp/crm.el (crm-prompt): New user option.
(crm-separator): Make customizable, update value and docstring.
(crm--regexp): New helper function.
(crm--collection-fn, crm--current-element, crm-complete-and-exit)
(completing-read-multiple): Use it.
---
 lisp/emacs-lisp/crm.el | 50 ++++++++++++++++++++++++++++++++----------
 1 file changed, 39 insertions(+), 11 deletions(-)

diff --git a/lisp/emacs-lisp/crm.el b/lisp/emacs-lisp/crm.el
index 454c3e85074..fcaef6cc476 100644
--- a/lisp/emacs-lisp/crm.el
+++ b/lisp/emacs-lisp/crm.el
@@ -79,9 +79,23 @@
 
 (define-obsolete-variable-alias 'crm-default-separator 'crm-separator "29.1")
 
-(defvar crm-separator "[ \t]*,[ \t]*"
-  "Separator regexp used for separating strings in `completing-read-multiple'.
-It should be a regexp that does not match the list of completion candidates.")
+(defcustom crm-separator '("[ \t]*,[ \t]*" "," "comma-separated list")
+  "Separator used for separating strings in `completing-read-multiple'.
+It should be a regexp or a list of three strings of the form (regexp
+separator description).  The regexp must not match the list of
+completion candidates, the separator string and the description can be
+shown in the prompt, see `crm-prompt'."
+  :type '(choice regexp (list regexp string string))
+  :group 'minibuffer)
+
+(defcustom crm-prompt "[%d] %p"
+  "Prompt format for `completing-read-multiple'.
+The prompt is formatted by `format-spec' with the keys %d, %s and %p
+which stand for the separator description, the separator string and the
+original prompt respectively."
+  :type 'string
+  :group 'minibuffer
+  :version "31.1")
 
 (defvar-keymap crm-local-completion-map
   :doc "Local keymap for minibuffer multiple input with completion.
@@ -106,6 +120,10 @@ crm-completion-table
 This is a table used for completion by `completing-read-multiple' and its
 supporting functions.")
 
+(defun crm--regexp ()
+  "Separator regexp."
+  (or (car-safe crm-separator) crm-separator))
+
 ;; this function evolved from a posting by Stefan Monnier
 (defun crm--collection-fn (string predicate flag)
   "Function used by `completing-read-multiple' to compute completion values.
@@ -122,7 +140,7 @@ crm--collection-fn
 Reference sections on “Programmed Completion” and “Basic Completion
 Functions”."
   (let ((beg 0))
-    (while (string-match crm-separator string beg)
+    (while (string-match (crm--regexp) string beg)
       (setq beg (match-end 0)))
     (completion-table-with-context (substring string 0 beg)
                                    crm-completion-table
@@ -135,11 +153,11 @@ crm--current-element
 Return the element's boundaries as (START . END)."
   (let ((bob (minibuffer-prompt-end)))
     (cons (save-excursion
-                  (if (re-search-backward crm-separator bob t)
+                  (if (re-search-backward (crm--regexp) bob t)
                       (match-end 0)
               bob))
           (save-excursion
-                (if (re-search-forward crm-separator nil t)
+                (if (re-search-forward (crm--regexp) nil t)
                     (match-beginning 0)
               (point-max))))))
 
@@ -195,7 +213,7 @@ crm-complete-and-exit
                                                (lambda () (setq doexit t)))
                  (goto-char end)
                  (not (eobp))))
-             (looking-at crm-separator))
+             (looking-at (crm--regexp)))
       ;; Skip to the next element.
       (goto-char (match-end 0)))
     (if doexit (exit-minibuffer))))
@@ -256,14 +274,16 @@ completing-read-multiple
                             (let* ((beg (save-excursion
                                           (goto-char (minibuffer-prompt-end))
                                           (or (search-forward start nil t)
-                                              (search-forward-regexp crm-separator nil t)
+                                              (search-forward-regexp
+                                               (crm--regexp) nil t)
                                               (minibuffer-prompt-end))))
                                    (end (save-excursion
                                           (goto-char (point-max))
                                           (or (search-backward end nil t)
                                               (progn
                                                 (goto-char beg)
-                                                (search-forward-regexp crm-separator nil t))
+                                                (search-forward-regexp
+                                                 (crm--regexp) nil t))
                                               (point-max)))))
                               (completion--replace beg end choice))
                           (completion--replace start end choice))))
@@ -272,14 +292,22 @@ completing-read-multiple
                       (unless (eq require-match t) require-match))
           (setq-local crm-completion-table table))
       (setq input (read-from-minibuffer
-                   prompt initial-input map
+                   (format-spec crm-prompt
+                                (if (consp crm-separator)
+                                    `((?s . ,(nth 1 crm-separator))
+                                      (?d . ,(nth 2 crm-separator))
+                                      (?p . ,prompt))
+                                  `((?s . ,crm-separator)
+                                    (?d . "multi list")
+                                    (?p . ,prompt))))
+                   initial-input map
                    nil hist def inherit-input-method)))
     ;; If the user enters empty input, `read-from-minibuffer'
     ;; returns the empty string, not DEF.
     (when (and def (string-equal input ""))
       (setq input (if (consp def) (car def) def)))
     ;; Remove empty strings in the list of read strings.
-    (split-string input crm-separator t)))
+    (split-string input (crm--regexp) t)))
 
 ;; testing and debugging
 ;; (defun crm-init-test-environ ()
-- 
2.47.2