I wrote the patch that caused this, so this is my bad. We can't just change
the order of "(append valid-symbols (list 'contiguous-spaces
'leading-and-trailing-spaces)))" as you suggested since
`org-link--remove-statistics-cookies' and 'org-link--remove-pipe-chars' can
add spaces to the search string. So instead we just trim twice, before we
attempt to normalize the search string and after we finish normalizing the
search string. I added new tests to confirm that this approach works.
From 9b7064c929528f2c6a3709873a3a66a19742f36a Mon Sep 17 00:00:00 2001
From: ApollonDeParnasse <[email protected]>
Date: Thu, 9 Jul 2026 13:04:44 -0500
Subject: [PATCH] lisp/ol.el: Trim search string before attempting to normalize
 it.

* lisp/ol.el (org-link-normalize-string): Trim search string before
attempting to normalize it.
* testing/lisp/test-ol.el (test-org-link/precise-link-target):
New tests for `org-link-precise-link-target'.
---
 lisp/ol.el              |   8 +-
 testing/lisp/test-ol.el | 158 ++++++++++++++++++++++++++++++++++++----
 2 files changed, 149 insertions(+), 17 deletions(-)

diff --git a/lisp/ol.el b/lisp/ol.el
index 96edafa17..3d0e32bcc 100644
--- a/lisp/ol.el
+++ b/lisp/ol.el
@@ -989,10 +989,14 @@ subset of (statistics-cookies search-syntax pipe-chars):
 - `search-syntax' is # in #heading and enclosing () in (ref)
 - `pipe-chars' are | characters that may clash with table syntax."
   (let* ((valid-symbols (seq-intersection ignored-contents (list 'statistics-cookies 'search-syntax 'pipe-chars)))
-         (values-to-remove (append valid-symbols (list 'contiguous-spaces 'leading-and-trailing-spaces))))
+         (values-to-remove (append valid-symbols (list 'contiguous-spaces 'leading-and-trailing-spaces)))
+         ;; We trim twice: before we run our string normalizers
+         ;; and after in order to remove any spaces that were introduced
+         ;; by the normalization process
+         (trimmed-string (org-trim string)))
     (seq-reduce (lambda (str func) (funcall (map-elt org-link--string-normalizers func) str))
                 values-to-remove
-                string)))
+                trimmed-string)))
 
 (defun org-link--reveal-maybe (region _)
   "Reveal folded link in REGION when needed.
diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el
index 1487f7d38..bb0c63a8b 100644
--- a/testing/lisp/test-ol.el
+++ b/testing/lisp/test-ol.el
@@ -713,21 +713,149 @@ See https://github.com/yantar92/org/issues/4.";
 
 (ert-deftest test-org-link/precise-link-target ()
   "Test `org-link-precise-link-target` specifications."
-  (org-test-with-temp-text "* H1<point>\n* H2\n"
-    (should
-     (equal '("*H1" "H1" 1)
-            (org-link-precise-link-target))))
-  (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n"
-    (should
-     (equal '("foo" "foo" 6)
-            (org-link-precise-link-target))))
-  (org-test-with-temp-text "\nText<point>\n* H1\n"
-    (should
-     (equal '("Text" nil 2)
-            (org-link-precise-link-target))))
-  (org-test-with-temp-text "\n<point>\n* H1\n"
-    (should
-     (equal nil (org-link-precise-link-target)))))
+  (cl-macrolet ((with-text-in-regular-buffer (text &rest body)
+                  (org-with-gensyms (file inside-text buffer)
+                    `(let ((,file (make-temp-file "org-test"))
+                           (,inside-text (if (stringp ,text) ,text (eval ,text)))
+                           ,buffer)
+                       (with-temp-file ,file (insert ,inside-text))
+                       (unwind-protect
+                           (progn
+                             (setq ,buffer (find-file ,file))
+                             (when (re-search-forward "<point>" nil t)
+                               (replace-match ""))
+                             (progn ,@body))
+                         (let ((kill-buffer-query-functions nil))
+                           (org-test-kill-buffer ,buffer)
+                           (delete-file ,file)))))))
+    (org-test-with-temp-text "* H1<point>\n* H2\n"
+      (should
+       (equal '("*H1" "H1" 1)
+              (org-link-precise-link-target))))
+    (org-test-with-temp-text "* H1\n#+name: foo<point>\n#+begin_example\nhi\n#+end_example\n"
+      (should
+       (equal '("foo" "foo" 6)
+              (org-link-precise-link-target))))
+    (org-test-with-temp-text "\nText<point>\n* H1\n"
+      (should
+       (equal '("Text" nil 2)
+              (org-link-precise-link-target))))
+    (org-test-with-temp-text "\n<point>\n* H1\n"
+      (should
+       (equal nil (org-link-precise-link-target))))
+    ;; Assert search-syntax is removed
+    ;;# + no leading spaces
+    (with-text-in-regular-buffer
+     "#not-a-heading"
+     (should (equal
+              (list "not-a-heading" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "#still not a heading"
+     (should (equal
+              (list "still not a heading" nil 1)
+              (org-link-precise-link-target))))
+    ;;# +  with leading spaces
+    (with-text-in-regular-buffer
+     "            #not-a-heading"
+     (should (equal
+              (list "not-a-heading" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "                     #still not a heading"
+     (should (equal
+              (list "still not a heading" nil 1)
+              (org-link-precise-link-target))))
+    ;;single set of parens + no leading spaces
+    (with-text-in-regular-buffer
+     "(let ((element (org-element-at-point)))"
+     (should (equal
+              (list "let ((element (org-element-at-point))" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "(let ((element (org-element-at-point)))"
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "let ((element (org-element-at-point))" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "(cond (line (org-goto-line line)"
+     (should (equal
+              (list "cond (line (org-goto-line line" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "(cond (line (org-goto-line line)"
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "cond (line (org-goto-line line" nil 1)
+              (org-link-precise-link-target))))
+    ;;single set of paren + leading spaces
+    (with-text-in-regular-buffer
+     "                     (let ((element (org-element-at-point)))"
+     (should (equal
+              (list "let ((element (org-element-at-point))" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "                (let ((element (org-element-at-point)))"
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "let ((element (org-element-at-point))" nil 1)
+              (org-link-precise-link-target))))
+    ;; multiple parens + no leading spaces
+    (with-text-in-regular-buffer
+     "((and (listp org-log-done) (memq 'done org-log-done))"
+     (should (equal
+              (list "and (listp org-log-done) (memq 'done org-log-done" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "and (listp org-log-done) (memq 'done org-log-done"
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "and (listp org-log-done) (memq 'done org-log-done" nil 1)
+              (org-link-precise-link-target))))
+    ;; multiple parens + leading spaces
+    (with-text-in-regular-buffer
+     "	  ((eq var 'prefix) (set-default-toplevel-value var nil))"
+     (should (equal
+              (list "eq var 'prefix) (set-default-toplevel-value var nil" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "	  ((eq var 'prefix) (set-default-toplevel-value var nil))"
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "eq var 'prefix) (set-default-toplevel-value var nil" nil 1)
+              (org-link-precise-link-target))))
+    ;; nothing but parens
+    (with-text-in-regular-buffer
+     "("
+     (should (equal
+              (list "(" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "("
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "(" nil 1)
+              (org-link-precise-link-target))))
+    ;; nothing but parens + leading spaces
+    (with-text-in-regular-buffer
+     "      ((("
+     (should (equal
+              (list "(((" nil 1)
+              (org-link-precise-link-target))))
+    (with-text-in-regular-buffer
+     "         (((("
+     (progn (set-mark (point))
+            (goto-char (line-end-position)))
+     (should (equal
+              (list "((((" nil 1)
+              (org-link-precise-link-target))))))
 
 (defmacro test-ol-stored-link-with-text (text &rest body)
   "Return :link and :description from link stored in body."
-- 
2.54.0

Reply via email to