Thanks for this bug report!  It was very well written and led me to the
problem very quickly.  Actually fixing the problem took a while and
involved a lot of learning for me!

I've attached many patches which should fix everything and tests a
couple things.  Tests pass after each commit on Emacs 30.  Test pass on
the final commit on emacs 28, and 29.

martin <[email protected]> writes:

>  4.  Enter some query that contains one or more hyphens, for example
>  TIMESTAMP_IA="<2025-09-18>"
>
>  5.  Press C-a or otherwise place the cursor earlier in the minibuffer
>  than a hyphen, and the error message appears.

I was able to reproduce this issue and I've added this scenario to a
test.

> So I looked into the code, and it appears that both
> `org-tags-completion-function` and
> `org-agenda-filter-completion-function` have this issue.

Fixed in both places.

> In each of the respective functions, the problem *seems* to be corrected
> if I replace this line:
>
> (match-string 0 suffix)
>
> with this:
>
> (match-end 0)
>
> However, I'm not confident that I know enough about what's going on to
> submit that change as a patch.

You where very close!  We actually want `match-beginning'.  The boundry
tells emacs that the item we are completing is within the boundary.  So
when completing something like: "tag1+tag2" with point at the start, we
want to boundry to include "tag1", not "tag1+".

>From 6e76fc3d0f2bced13615b34add2425a421c15535 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Mon, 27 Oct 2025 00:09:46 -0400
Subject: [PATCH 1/6] Testing: New test `test-org/org-tags-completion-function'

* testing/lisp/test-org.el (org-test-with-minibuffer-setup): New macro
copied from Emacs minibuffer tests.
(test-org/org-tags-completion-function): New test.
---
 testing/lisp/test-org.el | 96 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 96 insertions(+)

diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 88c083def..ccfef01a5 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -9926,6 +9926,102 @@ test-org/create-math-formula
             (test-org/extract-mathml-math
              (org-create-math-formula "quote\" ; |"))))))
 
+;; Copied from Emacs source code and prepended name with "org-test-"
+;; test/lisp/minibuffer-tests.el
+(defmacro org-test-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))))
+
+(ert-deftest test-org/org-tags-completion-function ()
+  "Test completion with `org-tags-completion-function'."
+  ;; (wrong-type-argument number-or-marker-p "-")
+  :expected-result :failed
+  ;; To aid in debbugging try the following:
+  ;; (add-function :before (symbol-function 'kbd) #'message)
+  (let (messages
+        (dings 0))
+    (cl-letf* (((symbol-function 'minibuffer-message)
+                (lambda (message &rest args)
+                  (push (apply #'format-message message args) messages)))
+               ;; dinging cancels keyboard macros which is not helpful for these tests
+               ((symbol-function 'ding)
+                (lambda (&optional _arg)
+                  (setq dings (+ 1 dings))))
+               ((symbol-function 'test-messages)
+                (lambda (expected)
+                  (should (equal messages expected))
+                  (setq messages nil))))
+      (org-test-with-minibuffer-setup
+          (let ((org-last-tags-completion-table '(("test"))) org-tags-history)
+            (completing-read
+             "Match: "
+             'org-tags-completion-function nil nil nil 'org-tags-history))
+        (progn
+          (execute-kbd-macro (kbd "TIME TAB"))
+          (test-messages '("No match"))
+          (execute-kbd-macro (kbd "STAMP_IA=\"<2025- TAB"))
+          (should (equal (minibuffer-contents) "TIMESTAMP_IA=\"<2025-"))
+          (test-messages '("No match"))
+          (execute-kbd-macro (kbd "09-18>\" TAB"))
+          (test-messages '("No match"))
+          (execute-kbd-macro (kbd "C-a C-f TAB"))
+          (should (equal (minibuffer-contents) "TIMESTAMP_IA=\"<2025-09-18>\""))
+          (test-messages '("No match"))))
+      (org-test-with-minibuffer-setup
+          (let ((org-last-tags-completion-table
+                 '(("test") ("test2") ("uniq")))
+                org-tags-history)
+            (completing-read
+             "Match: "
+             'org-tags-completion-function nil nil nil 'org-tags-history))
+        (progn
+          (setq messages nil)
+          (execute-kbd-macro (kbd "un TAB"))
+          (test-messages nil)
+          (should (equal (minibuffer-contents) "uniq"))
+          (execute-kbd-macro (kbd "TAB"))
+          (test-messages '("Sole completion"))
+          (execute-kbd-macro (kbd "+tes TAB"))
+          (test-messages nil)
+          (should (equal (minibuffer-contents) "uniq+test"))
+          (execute-kbd-macro (kbd "TAB"))
+          (test-messages '("Complete, but not unique"))
+          (should (equal (minibuffer-contents) "uniq+test"))
+          ;; Test the boundaries thoroughly.  Ensure that completion
+          ;; acts the same regardless of point position within the
+          ;; boundary
+          (execute-kbd-macro (kbd "C-a TAB"))
+          (test-messages '("Sole completion"))
+          (execute-kbd-macro (kbd "C-a C-f TAB"))
+          (test-messages '("Sole completion"))
+          (execute-kbd-macro (kbd "C-a C-f C-f TAB"))
+          (test-messages '("Sole completion"))
+          (execute-kbd-macro (kbd "C-a C-f C-f C-f TAB"))
+          (test-messages '("Sole completion"))
+          (execute-kbd-macro (kbd "C-a C-f C-f C-f C-f TAB"))
+          (test-messages '("Sole completion"))
+          (should (equal (minibuffer-contents) "uniq+test"))
+          (execute-kbd-macro (kbd "C-a t| C-a TAB"))
+          (should (equal (minibuffer-contents) "test|uniq+test"))
+          (test-messages nil)
+          (execute-kbd-macro (kbd "C-a TAB"))
+          (test-messages '("Complete, but not unique"))
+          (execute-kbd-macro (kbd "C-a C-f TAB"))
+          (test-messages '("Complete, but not unique"))
+          (execute-kbd-macro (kbd "C-a C-f C-f TAB"))
+          (test-messages '("Complete, but not unique"))
+          (execute-kbd-macro (kbd "C-a C-f C-f C-f TAB"))
+          (test-messages '("Complete, but not unique"))
+          (execute-kbd-macro (kbd "C-a C-f C-f C-f C-f TAB"))
+          (test-messages '("Complete, but not unique")))))))
+
 (provide 'test-org)
 
 ;;; test-org.el ends here

base-commit: df5628041dd2317f458e2903bc61fb985849c328
-- 
2.51.0

>From 01ee76e609b8164e4b90fe7b98388091450d305f Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Fri, 31 Oct 2025 12:29:01 -0400
Subject: [PATCH 2/6] org-tags-completion-function: Return correct boundary

* lisp/org.el (org-tags-completion-function): Previously a string was
returned when the boundry should be a number.  Now it is a number.
* testing/lisp/test-org.el (org-test-with-minibuffer-setup): Update
comment as test now fails for a different reason.

Reported-by: "martin" <[email protected]>
Link: https://list.orgmode.org/caofdpfwvy6ouzrz3qkn7aqlyvpgs3exx6qa-ftgfj1c7oak...@mail.gmail.com/
---
 lisp/org.el              | 2 +-
 testing/lisp/test-org.el | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index 6b8d02b87..16cdef14a 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -12284,7 +12284,7 @@ org-tags-completion-function
       (`lambda (assoc string org-last-tags-completion-table)) ;exact match?
       (`(boundaries . ,suffix)
        (let ((end (if (string-match "[-+:&,|]" suffix)
-                      (match-string 0 suffix)
+                      (match-beginning 0)
                     (length suffix))))
          `(boundaries ,(or begin 0) . ,end)))
       (`nil
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index ccfef01a5..869c6584a 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -9941,7 +9941,8 @@ org-test-with-minibuffer-setup
 
 (ert-deftest test-org/org-tags-completion-function ()
   "Test completion with `org-tags-completion-function'."
-  ;; (wrong-type-argument number-or-marker-p "-")
+  ;; Completes in unbalanced parenthesis.
+  ;; TIMESTAMP_IA="<2025- TAB adds a tag completion.
   :expected-result :failed
   ;; To aid in debbugging try the following:
   ;; (add-function :before (symbol-function 'kbd) #'message)
-- 
2.51.0

>From 9f294eb9e8e76aa73b0e7d46c6cc9d0c7c4f3811 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Fri, 31 Oct 2025 12:33:53 -0400
Subject: [PATCH 3/6] org-tags-completion-function: Abort on unbalanced quotes

* lisp/org.el (org-tags-completion-function): Abort on unbalanced
quotes.
* testing/lisp/test-org.el (org-test-with-minibuffer-setup): Test now
passes.
---
 lisp/org.el              | 10 ++++++++++
 testing/lisp/test-org.el |  3 ---
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index 16cdef14a..78d330aa7 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -12275,6 +12275,16 @@ org-tags-completion-function
 	(confirm (lambda (x) (stringp (car x))))
 	(prefix "")
         begin)
+    ;; Abort if the string has unbalanced quotes
+    (let ((quotes 0))
+      (mapc
+       (lambda (char)
+         (when (eq char ?\")
+           (setq quotes (1+ quotes))))
+       string)
+      (when (and (< 0 quotes)
+                 (not (eq (% quotes 2) 0)))
+        (setq flag 'invalid)))
     (when (string-match "^\\(.*[-+:&,|]\\)\\([^-+:&,|]*\\)$" string)
       (setq prefix (match-string 1 string))
       (setq begin (match-beginning 2))
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 869c6584a..33140d8a8 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -9941,9 +9941,6 @@ org-test-with-minibuffer-setup
 
 (ert-deftest test-org/org-tags-completion-function ()
   "Test completion with `org-tags-completion-function'."
-  ;; Completes in unbalanced parenthesis.
-  ;; TIMESTAMP_IA="<2025- TAB adds a tag completion.
-  :expected-result :failed
   ;; To aid in debbugging try the following:
   ;; (add-function :before (symbol-function 'kbd) #'message)
   (let (messages
-- 
2.51.0

>From 8f8d5381f6619360672ac88997600565641b0350 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Fri, 31 Oct 2025 12:34:25 -0400
Subject: [PATCH 4/6] org-tags-completion-function: Link to info page in
 docstring

* lisp/org.el (org-tags-completion-function): Add link to info page.
Also add FIXME to add property support.
---
 lisp/org.el | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/lisp/org.el b/lisp/org.el
index 78d330aa7..2cc137055 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -12268,9 +12268,15 @@ org-change-tag-in-region
 
 (defun org-tags-completion-function (string _predicate &optional flag)
   "Complete tag STRING.
+
+The format for tag string is described in the
+Info node `(org) Matching tags and properties'.
+
 FLAG specifies the type of completion operation to perform.  This
 function is passed as a collection function to `completing-read',
 which see."
+  ;; FIXME: This function is used to complete a tag string which can
+  ;; include properties but does not know anything about properties
   (let ((completion-ignore-case nil)	;tags are case-sensitive
 	(confirm (lambda (x) (stringp (car x))))
 	(prefix "")
-- 
2.51.0

>From 85f92d3318101fb9a1297bb62f8c05aae8895c86 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Fri, 31 Oct 2025 13:25:13 -0400
Subject: [PATCH 5/6] org-agenda-filter-completion-function: Return correct
 boundary

* lisp/org-agenda.el (org-agenda-filter-completion-function):
Previously a string was returned when the boundry should be a number.
Now it is a number.

Reported-by: "martin" <[email protected]>
Link: https://list.orgmode.org/caofdpfwvy6ouzrz3qkn7aqlyvpgs3exx6qa-ftgfj1c7oak...@mail.gmail.com/
---
 lisp/org-agenda.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/org-agenda.el b/lisp/org-agenda.el
index 3497a9763..c7b23b8c7 100644
--- a/lisp/org-agenda.el
+++ b/lisp/org-agenda.el
@@ -8237,7 +8237,7 @@ org-agenda-filter-completion-function
       (`lambda (assoc string table)) ;exact match?
       (`(boundaries . ,suffix)
        (let ((end (if (string-match "[-+<>=]" suffix)
-                      (match-string 0 suffix)
+                      (match-beginning 0)
                     (length suffix))))
          `(boundaries ,(or begin 0) . ,end)))
       (`nil
-- 
2.51.0

>From 5424694435e61273edb55a5d5830c0c0aca62f06 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Fri, 31 Oct 2025 13:32:12 -0400
Subject: [PATCH 6/6] org-agenda-filter-completion-function: Add info page to
 docstring

* lisp/org-agenda.el (org-agenda-filter-completion-function): Add link
to info page.
---
 lisp/org-agenda.el | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lisp/org-agenda.el b/lisp/org-agenda.el
index c7b23b8c7..db3614e98 100644
--- a/lisp/org-agenda.el
+++ b/lisp/org-agenda.el
@@ -8206,6 +8206,9 @@ org-agenda-filter
 
 (defun org-agenda-filter-completion-function (string _predicate &optional flag)
   "Complete a complex filter string.
+
+See the Info Node `(org) Filtering/limiting agenda items'.
+
 FLAG specifies the type of completion operation to perform.  This
 function is passed as a collection function to `completing-read',
 which see."
-- 
2.51.0

Reply via email to