aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-20 11:51:12 -0400
committerCraig Jennings <c@cjennings.net>2026-06-20 11:51:12 -0400
commitd96a162db3be25cb812d08a10c831baa2790c492 (patch)
tree6b633916138901a37321fd57a3f851624cc534f9
parent3a0fa280183d426935956979ad713fb6e7284488 (diff)
downloaddotemacs-d96a162db3be25cb812d08a10c831baa2790c492.tar.gz
dotemacs-d96a162db3be25cb812d08a10c831baa2790c492.zip
refactor(org-capture): extract the find-or-create-top-heading block
cj/org-capture--goto-file-headline, cj/--org-capture-goto-open-work, and cj/--org-capture-goto-exact-headline each repeated the same positioning block: search from point-min, jump to the heading on a match, else append it at end of buffer and back up. Extract cj/--org-find-or-create-top-heading taking the search regexp and the heading line; the three sites delegate. Behavior unchanged; adds direct coverage of the helper with a plain regexp.
-rw-r--r--modules/org-capture-config.el49
-rw-r--r--tests/test-org-capture-config--find-or-create-top-heading.el45
2 files changed, 69 insertions, 25 deletions
diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el
index 18e130dc6..2f245185f 100644
--- a/modules/org-capture-config.el
+++ b/modules/org-capture-config.el
@@ -76,6 +76,21 @@
"Return the cache key for PATH and HEADLINE."
(list (org-capture-expand-file path) headline))
+(defun cj/--org-find-or-create-top-heading (search-regexp heading-line)
+ "Move point to the top-level heading matched by SEARCH-REGEXP in this buffer.
+Search from the start of the buffer; on a match leave point at the start of
+that heading line. With no match, append HEADING-LINE (a full \"* ...\" line,
+without a trailing newline) at the end of the buffer and leave point on it.
+Returns point."
+ (goto-char (point-min))
+ (if (re-search-forward search-regexp nil t)
+ (forward-line 0)
+ (goto-char (point-max))
+ (unless (bolp) (insert "\n"))
+ (insert heading-line "\n")
+ (forward-line -1))
+ (point))
+
(defun cj/org-capture--goto-file-headline (path headline)
"Move to capture target PATH and HEADLINE, using a cached marker when valid.
This implements Org's `file+headline' target positioning behavior, but avoids
@@ -94,15 +109,9 @@ re-scanning large target files after the first successful lookup."
(marker (gethash key cj/org-capture--file-headline-target-cache)))
(if (cj/org-capture--headline-marker-valid-p marker headline)
(goto-char marker)
- (goto-char (point-min))
- (if (re-search-forward (format org-complex-heading-regexp-format
- (regexp-quote headline))
- nil t)
- (forward-line 0)
- (goto-char (point-max))
- (unless (bolp) (insert "\n"))
- (insert "* " headline "\n")
- (forward-line -1))
+ (cj/--org-find-or-create-top-heading
+ (format org-complex-heading-regexp-format (regexp-quote headline))
+ (concat "* " headline))
(puthash key (copy-marker (point))
cj/org-capture--file-headline-target-cache))))
@@ -177,27 +186,17 @@ file path. Return a plist (:file F :open-work BOOL :project NAME :warn MSG):
"Move point to a top-level \"... Open Work\" heading in the current buffer.
Create \"* PROJECT-NAME Open Work\" at end of buffer when none exists.
Leave point at the start of the heading line."
- (goto-char (point-min))
- (if (re-search-forward cj/--org-open-work-heading-regexp nil t)
- (forward-line 0)
- (goto-char (point-max))
- (unless (bolp) (insert "\n"))
- (insert (format "* %s Open Work\n" project-name))
- (forward-line -1)))
+ (cj/--org-find-or-create-top-heading
+ cj/--org-open-work-heading-regexp
+ (format "* %s Open Work" project-name)))
(defun cj/--org-capture-goto-exact-headline (headline)
"Move point to the top-level HEADLINE in the current buffer.
Create \"* HEADLINE\" at end of buffer when absent. Leave point at the
start of the heading line."
- (goto-char (point-min))
- (if (re-search-forward (format org-complex-heading-regexp-format
- (regexp-quote headline))
- nil t)
- (forward-line 0)
- (goto-char (point-max))
- (unless (bolp) (insert "\n"))
- (insert "* " headline "\n")
- (forward-line -1)))
+ (cj/--org-find-or-create-top-heading
+ (format org-complex-heading-regexp-format (regexp-quote headline))
+ (concat "* " headline)))
(defun cj/--org-capture-project-location ()
"Org-capture `function' target for project-aware Task/Bug capture.
diff --git a/tests/test-org-capture-config--find-or-create-top-heading.el b/tests/test-org-capture-config--find-or-create-top-heading.el
new file mode 100644
index 000000000..236c87c87
--- /dev/null
+++ b/tests/test-org-capture-config--find-or-create-top-heading.el
@@ -0,0 +1,45 @@
+;;; test-org-capture-config--find-or-create-top-heading.el --- Tests for the shared find-or-create helper -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; cj/--org-find-or-create-top-heading is the search-or-append positioning block
+;; extracted from cj/org-capture--goto-file-headline, cj/--org-capture-goto-open-work,
+;; and cj/--org-capture-goto-exact-headline. The three call sites stay covered by
+;; test-org-capture-config-project-target.el (open-work, exact-headline) and the
+;; target-cache test; these cover the generic helper directly with a plain regexp
+;; (so the test doesn't depend on org's complex-heading format).
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-capture-config)
+
+(ert-deftest test-org-find-or-create-top-heading-finds-existing ()
+ "Normal: an existing heading is found; point lands at its line start and the
+buffer is unchanged."
+ (with-temp-buffer
+ (insert "* Alpha\nbody\n* Target\nmore\n")
+ (cj/--org-find-or-create-top-heading "^\\* Target$" "* Target")
+ (should (looking-at-p "\\* Target$"))
+ (should (equal (buffer-string) "* Alpha\nbody\n* Target\nmore\n"))))
+
+(ert-deftest test-org-find-or-create-top-heading-creates-when-absent ()
+ "Boundary: with no match, the heading line is appended (a separating newline
+added because the buffer doesn't end in one) and point lands on it."
+ (with-temp-buffer
+ (insert "some text") ; no trailing newline
+ (cj/--org-find-or-create-top-heading "^\\* Missing$" "* Missing")
+ (should (equal (buffer-string) "some text\n* Missing\n"))
+ (should (looking-at-p "\\* Missing$"))))
+
+(ert-deftest test-org-find-or-create-top-heading-empty-buffer ()
+ "Boundary: in an empty buffer the heading is inserted at the top, no extra
+leading newline."
+ (with-temp-buffer
+ (cj/--org-find-or-create-top-heading "^\\* X$" "* X")
+ (should (equal (buffer-string) "* X\n"))
+ (should (looking-at-p "\\* X$"))))
+
+(provide 'test-org-capture-config--find-or-create-top-heading)
+;;; test-org-capture-config--find-or-create-top-heading.el ends here