From ed436e704bac557311fc0a26b885f2493976e51c Mon Sep 17 00:00:00 2001
From: Daniel M German <dmg@turingmachine.org>
Date: Mon, 4 Aug 2025 18:03:13 -0700
Subject: [PATCH] org.el: make `org-map-region' properly set point at first
 heading

* lisp/org.el (org-map-region): Add a call to `goto-char' to go
to the beginning of the match after the first search.

* testing/lisp/test-org.el (test-org/map-region): Add tests
for `org-map-region'.

The current logic of `org-map-region' uses two searches.

In the first, the point is left after the asterisks of the heading.
In the subsequent calls, the point is correctly set to the first
asterisk of the headline.

This change makes sure the first header is processed the same way
as the rest.

TINYCHANGE
---
 lisp/org.el              |  5 ++-
 testing/lisp/test-org.el | 77 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/lisp/org.el b/lisp/org.el
index 65abfbe1a..626e44585 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -7078,13 +7078,16 @@ After top level, it switches back to sibling level."
 	(funcall fun)))))
 
 (defun org-map-region (fun beg end)
-  "Call FUN for every heading between BEG and END."
+  "Call FUN for every heading between BEG and END.
+The point is placed at the beginning of each heading
+(including any *) before FUN is called."
   (let ((org-ignore-region t))
     (save-excursion
       (setq end (copy-marker end))
       (goto-char beg)
       (when (and (re-search-forward org-outline-regexp-bol nil t)
 		 (< (point) end))
+        (goto-char (match-beginning 0))
 	(funcall fun))
       (while (and (progn
 		    (outline-next-heading)
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 36dea35b7..e9f56efdd 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -3161,6 +3161,83 @@ Let’s stop here
       (should (equal '("two")
                      (org-element-cache-map (lambda (el) (org-element-property :title el)) :next-re "TODO"))))))
 
+(ert-deftest test-org/map-region ()
+  "Test `org-map-region'."
+
+  (cl-flet
+      ;; return the text from point to the end of line
+      ((extract-text-to-end-of-line ()
+         (buffer-substring-no-properties (point) (line-end-position)))
+       ;; org-map-region does not return anything so we need to
+       ;; wrap it in a function that saves the return value
+       ;; of the function applied to each header, and returns
+       ;; the results of applying such function as a list
+       (org-map-region-with-results  (fn &optional beg end)
+         (let (results)
+           (org-map-region
+            (lambda ()
+              (let ((result (funcall fn)))
+                (push result results)))
+            (or beg (point-min))
+            (or end (point-max)))
+           (nreverse results))))
+    ;; each test returns the line of the entry
+    (dolist (org-element-use-cache '(t nil))
+      (should
+       (equal (list "* Level 1" "** Level 2")
+	      (org-test-with-temp-text "* Level 1\n** Level 2"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line))))
+      (should
+       ;; add some text before first heading
+       (equal (list "* Level 1" "** Level 2")
+	      (org-test-with-temp-text "#+filetags: :Todo:weekly:\n\n\n\n* Level 1\n** Level 2"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line))))
+      (should
+      ;; do not start from the beginning of the entry, thus skip it
+       (equal (list "** Level 2" "** Another Level 2")
+	      (org-test-with-temp-text "* Level 1\n** Level 2\n** Another Level 2"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line 5))))
+      (should
+       ;; test skipping BEG END parameters
+       (equal (list "* 345" "** 012")
+	      (org-test-with-temp-text "* 345\n** 012\n 2\n** Another Level 2"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line 1 11))))
+      (should
+       ;; between levels (after *, but before text), should be empty 
+       (equal (list )
+	      (org-test-with-temp-text "* 345\n** 012\n \n** Another Level 2"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line 2 10))))
+      (should
+       ;; test miminal region BEG END that will process an entry
+       ;; the minimal region that can be processed begins at the line and contains one character after
+       ;; the asterisk
+       ;; note that the callback sees all the text
+       (equal (list "* 4567890")
+	      (org-test-with-temp-text "\n* 4567890\nThis is a level \n* Another Level\n\nsome text"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line 2 5))))
+      (should
+       ;; a more complex test
+       (equal (list "* Level 1" "** Level 2" "* Level 1 again" "** Level 2 again")
+	      (org-test-with-temp-text "
+:PROPERTIES:
+:ID:       some-id
+:END:
+* Level 1
+
+Some text
+
+** Level 2
+
+More text
+
+* Level 1 again
+
+** Level 2 again
+
+"
+	                               (org-map-region-with-results #'extract-text-to-end-of-line))))
+      )))
+
 (ert-deftest test-org/edit-headline ()
   "Test `org-edit-headline' specifications."
   (should
-- 
2.43.0

