From 93728e90f996c34eb70e51b94eff71a81571740f Mon Sep 17 00:00:00 2001
From: Daanturo <daanturo@gmail.com>
Date: Sat, 15 Mar 2025 12:46:45 +0000
Subject: [PATCH] oc-basic.el: new option
 org-cite-basic-complete-key-crm-separator

* lisp/oc-basic.el: Define org-cite-basic-complete-key-crm-separator
and
`org-cite-basic--complete-key-dynamic-crm-separator'. (org-cite-basic--complete-key):
Conditionally use the new option.

* etc/ORG-NEWS: (New option ~org-cite-basic-complete-key-crm-separator~):
Document the new feature.

Link: https://list.orgmode.org/D77467CB-7E40-40A4-B5A2-88C73889032E@getmailspring.com/
---
 etc/ORG-NEWS     |  7 +++++
 lisp/oc-basic.el | 76 ++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 74 insertions(+), 9 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 332586f4f..e11a94ef2 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -320,6 +320,13 @@ behaviour of other exporters. In this case, to exclude a section from
 the table of contents, mark it as =:UNNUMBERED: notoc= in its
 properties.
 
+*** New option ~org-cite-basic-complete-key-crm-separator~
+
+This option makes ~org-cite~'s ~basic~ insert processor use
+~completing-read-multiple~ instead of the default consecutive prompts.
+It can also be set to dynamically compute ~crm-separator~ so that the
+separator does not appear in completion candidates.
+
 ** New functions and changes in function arguments
 
 # This also includes changes in function behavior from Elisp perspective.
diff --git a/lisp/oc-basic.el b/lisp/oc-basic.el
index 32a99e987..0ab62adf7 100644
--- a/lisp/oc-basic.el
+++ b/lisp/oc-basic.el
@@ -140,6 +140,26 @@
   :type 'face
   :safe #'facep)
 
+(defcustom org-cite-basic-complete-key-crm-separator nil
+  "When non-nil, use `completing-read-multiple' with this as the separator.
+When nil, use multiple `completing-read' prompts.  When set to a string,
+it should be a regexp to be used as `crm-separator' (which see).  When
+set to symbol `dynamic', use \";;...\" as a separator with the number of
+\";\" sufficient so that none of the completion candidates contain the
+separator."
+  :group 'org-cite
+  :package-version '(Org . "9.8")
+  :type
+  '(choice
+    (const :tag "Use \";;\" as the separator." "[ \t]*;;[ \t]*")
+    (const
+     :tag
+     "Dynamically compute \";\"+ with the needed length."
+     dynamic)
+    (string :tag "Custom regexp for the separator.")
+    (const :tag "Prompt multiple times." nil))
+  :safe (lambda (obj) (or (string-or-null-p obj) (member obj '(dynamic)))))
+
 
 ;;; Internal variables
 (defvar org-cite-basic--bibliography-cache nil
@@ -884,6 +904,27 @@ Return nil if there are no bibliography files or no entries."
         (puthash entries t org-cite-basic--completion-cache)
         org-cite-basic--completion-cache)))))
 
+(defun org-cite-basic--complete-key-dynamic-crm-separator
+    (completion-candidates separator)
+  "Return a repeated version of SEPARATOR as needed.
+The number of appeared SEPARATORs in the returned string is sufficient
+so that none of COMPLETION-CANDIDATES contains it.  SEPARATOR should be
+a literal string."
+  (let* ((dyn-sep separator)
+         (consecutive-sep-regexp
+          (format "%s+" (regexp-opt (list separator)))))
+    (with-temp-buffer
+      (dolist (cand completion-candidates)
+        (when (stringp cand)
+          (insert cand "\n")))
+      (goto-char (point-min))
+      (while (re-search-forward consecutive-sep-regexp nil t)
+        (when (<= (length dyn-sep) (length (match-string 0)))
+          (setq dyn-sep (concat dyn-sep separator)))))
+    (format "[ \t]*%s[ \t]*" (regexp-quote dyn-sep))))
+
+(defvar crm-separator) ; defined in crm.el
+
 (defun org-cite-basic--complete-key (&optional multiple)
   "Prompt for a reference key and return a citation reference string.
 
@@ -895,12 +936,28 @@ Raise an error when no bibliography is set in the buffer."
   (let* ((table
           (or (org-cite-basic--key-completion-table)
               (user-error "No bibliography set")))
-         (prompt
-          (lambda (text)
-            (completing-read text table nil t))))
-    (if (null multiple)
-        (let ((key (gethash (funcall prompt "Key: ") table)))
-          (org-string-nw-p key))
+         (prompt-single
+          (lambda (text) (completing-read text table nil t)))
+         (choice-to-citation
+          (lambda (choice)
+            (let ((key (gethash choice table)))
+              (org-string-nw-p key)))))
+    (cond
+     ((null multiple)
+      (funcall choice-to-citation
+               (completing-read "Key: " table nil t)))
+     (org-cite-basic-complete-key-crm-separator
+      (let ((crm-separator
+             (pcase org-cite-basic-complete-key-crm-separator
+               ((pred stringp)
+                org-cite-basic-complete-key-crm-separator)
+               (`dynamic
+                (org-cite-basic--complete-key-dynamic-crm-separator
+                 (hash-table-keys table) ";")))))
+        (seq-keep
+         choice-to-citation
+         (completing-read-multiple "Keys: " table nil t))))
+     (t
       (let* ((keys nil)
              (build-prompt
               (lambda ()
@@ -908,11 +965,12 @@ Raise an error when no bibliography is set in the buffer."
                     (format "Key (empty input exits) %s: "
                             (mapconcat #'identity (reverse keys) ";"))
                   "Key (empty input exits): "))))
-        (let ((key (funcall prompt (funcall build-prompt))))
+        (let ((key (funcall prompt-single (funcall build-prompt))))
           (while (org-string-nw-p key)
             (push (gethash key table) keys)
-            (setq key (funcall prompt (funcall build-prompt)))))
-        keys))))
+            (setq key
+                  (funcall prompt-single (funcall build-prompt)))))
+        keys)))))
 
 
 ;;; Register processor
-- 
2.49.0

