On Sat, Jan 31 2026, Ihor Radchenko wrote: > Kristoffer Balintona <[email protected]> writes: > >>> - (`(file+olp ,path . ,(and outline-path (guard outline-path))) >>> + ((or `(file+olp ,path) >>> + `(file+olp ,path . ,outline-path)) > > Isn't simply `(file+olp ,path . ,outline-path)' good enough?
Fixed (for file+olp and file+olp+datetree). I think I did that because the :type of 'org-capture-templates' did not match that 'pcase' pattern. But now I see it does, so I was probably doing something wrong or was confused at the time for whatever reason. >>> (let* ((expanded-file-path (org-capture-expand-file path)) >>> - (m (org-find-olp (cons expanded-file-path >>> - (apply #'org-capture-expand-olp >>> expanded-file-path outline-path))))) >>> + (expanded-olp (apply #'org-capture-expand-olp >>> expanded-file-path outline-path)) >>> + ;; Vary behavior depending on whether expanded-olp is >>> + ;; nil or non-nil. If expanded-olp is non-nil, then >>> + ;; create the entry at that outline path. If >>> + ;; expanded-olp is nil (no olp is provided), then >>> + ;; create the entry in the expanded-file-path file >>> + (m (if expanded-olp >>> + (org-find-olp (cons expanded-file-path >>> expanded-olp)) >>> + (set-buffer (org-capture-target-buffer >>> expanded-file-path)) >>> + ;; If expanded-olp is nil or omitted, then the >>> + ;; user is essentially using the form (file >>> + ;; <file-spec>), so behave as such by setting >>> + ;; `target-entry-p' to nil >>> + (setq target-entry-p nil) >>> + ;; Return a marker pointing to the end of the >>> + ;; file to cause the new entry to be created >>> + ;; there. We widen first to consider the >>> + ;; possibility of the buffer already being open >>> + ;; and narrowed by the user >>> + (save-restriction (widen) (point-max-marker))))) > > That will not obey :prepend if we force adding to the end of buffer. > I think that what needs to be done here is simply deferring to > (file "path"), which see. > > Same for file+olp+datetree and file+headline. Thanks for the hint. I agree. See the usage of 'set-target-to-file' in the new patch set. I factored out what the file spec so that file+olp and file+headline can use it. (Note: I wanted to avoid using e.g. 'cl-flet' since that would mean wrapping the existing let form, making the diff quite large.) That leaves file+olp+datetree: I wasn't sure how that should behave with :prepend. With the attached patch set: 1. With :prepend nil and NO existing datetree: A new datetree will be created at the end of the file (if no or nil olp) or end of subtree (when non-nil olp) with a new entry. 2. With :prepend t and NO existing datetree: A new datetree will be created at the end of the file (if no or nil olp) or end of subtree (when non-nil olp) with a new entry. 3. With :prepend nil and an existing datetree: A new entry will be created at the END of associated date in the datetree. 4. With :prepend t and an existing datetree: A new entry will be created at the BEGINNING of associated date in the datetree. This means that :prepend only affects where the entry is inserted in the datetree (either at the end or beginning of the associated date in the datetree), not where the datetree itself is created. Do you have a problem with any of the above behaviors? > Also, since we are adding new features, can you add an ORG-NEWS entry? Done. WDYT? > (I think manual is ok, but you can double-check). I've inserted brief mentions of the new target specification forms in the Template Elements node of the manual. -- Kind regards, Kristoffer
From 56ce6ce51df419a11a9a0d7069de7d19cc23f278 Mon Sep 17 00:00:00 2001 From: Kristoffer Balintona <[email protected]> Date: Fri, 9 May 2025 11:40:13 -0500 Subject: [PATCH 1/2] lisp/org-capture.el: Accept omitted or nil olp for file+olp+datetree and file+olp * doc/org-manual.org (Template elements): Mention new accepted file+olp and file+olp+datetree specification forms. * etc/ORG-NEWS: (The ~file+olp~ and ~file+olp+datetree~ capture target specifications now accept an omitted or nil outline path): Document new behavior for file+olp and file+olp+datetree. * lisp/org-capture.el (org-capture-templates): Update docstring and its :type to reflect new behavior. (org-capture-set-target-location): The file+olp+datetree and file+olp target specifications now accept a nil or omitted olp. Update the docstring. (org-capture-expand-olp): Return nil when no olp is supplied or when olp is nil. * testing/lisp/test-org-capture.el (test-org-capture/org-capture-expand-olp): Add test cases for nil and omitted olp. --- doc/org-manual.org | 4 + etc/ORG-NEWS | 9 ++ lisp/org-capture.el | 139 +++++++++++++++++++------------ testing/lisp/test-org-capture.el | 14 ++++ 4 files changed, 113 insertions(+), 53 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 50359ef5b..ac6b7ab40 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -8117,6 +8117,8 @@ Now lets look at the elements of a template definition. Each entry in Fast configuration if the target heading is unique in the file. + - =(file+olp <file-spec>)= :: + - =(file+olp <file-spec> "Level 1 heading" "Level 2" ...)= :: - =(file+olp <file-spec> function-returning-list-of-strings)= :: @@ -8129,6 +8131,8 @@ Now lets look at the elements of a template definition. Each entry in Use a regular expression to position point. + - =(file+olp+datetree <file-spec>)= :: + - =(file+olp+datetree <file-spec> [ "Level 1 heading" ...])= :: - =(file+olp+datetree <file-spec> function-returning-list-of-strings)= :: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 9113e68ea..3236c39cc 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -243,6 +243,15 @@ take the date as an argument, and generate a list of pairs for types of datetrees (e.g. for lunar calendars, academic calendars, retail 4-4-5 calendars, etc). +*** The ~file+olp~ and ~file+olp+datetree~ capture target specifications now accept an omitted or nil outline path + +The ~file+olp~ and ~file+olp+datetree~ are ~org-capture-templates~ +target specifications. Previously, the ~file+olp~ and +~file+olp+datetree~ required an outline path. Now, the specified +outline path may be omitted or set to ~nil~, in which case the entry +is inserted at the top level of the target file, respecting +~:prepend~. + *** Source blocks fall back to Fundamental mode Org now falls back to Fundamental mode for source blocks when the diff --git a/lisp/org-capture.el b/lisp/org-capture.el index 3c8040854..c1676dca3 100644 --- a/lisp/org-capture.el +++ b/lisp/org-capture.el @@ -207,20 +207,31 @@ target Specification of where the captured item should be placed. (file+headline <file-spec> symbol-containing-string) Fast configuration if the target heading is unique in the file + (file+olp <file-spec>) (file+olp <file-spec> \"Level 1 heading\" \"Level 2\" ...) (file+olp <file-spec> function-returning-list-of-strings) (file+olp <file-spec> symbol-containing-list-of-strings) - For non-unique headings, the full outline path is safer + For non-unique headings, the full outline path is + safer. The entry is created at the outline path (a + list of strings denoting headlines). If no outline + path is specified or if the outline path specification + is nil, then insert the entry at the top level of + <file-spec>. (file+regexp <file-spec> \"regexp to find location\") File to the entry containing matching regexp + (file+olp+datetree <file-spec>) (file+olp+datetree <file-spec> \"Level 1 heading\" ...) (file+olp+datetree <file-spec> function-returning-list-of-strings) (file+olp+datetree <file-spec> symbol-containing-list-of-strings) - Will create a heading in a date tree for today's date. - If no heading is given, the tree will be on top level. - To prompt for date instead of using TODAY, use the + Will create an entry in a datetree under the specified + outline path for today's date. If no outline path is + given or if the outline path specification is nil, then + insert the entry into the first existing top level + datetree in <file-spec> or, if no top level datetree + exists, a newly created datetree at the end of + <file-spec>. To get prompted for a date, use the :time-prompt property. To create a week-tree, use the :tree-type property. @@ -451,11 +462,10 @@ you can escape ambiguous cases with a backward slash, e.g., \\%i." (file :tag "Literal") (function :tag "Function") (variable :tag "Variable"))) - (olp-variants '(choice :tag "Outline path" - (repeat :tag "Outline path" :inline t - (string :tag "Headline")) - (function :tag "Function") - (variable :tag "Variable")))) + (olp-variants-choices '((function :tag "Function") + (variable :tag "Variable") + (repeat :tag "Outline path" :inline t + (string :tag "Headline"))))) `(repeat (choice :value ("" "" entry (file "~/org/notes.org") "") (list :tag "Multikey description" @@ -484,20 +494,22 @@ you can escape ambiguous cases with a backward slash, e.g., \\%i." (string :tag "Headline") (function :tag "Function") (variable :tag "Variable"))) - (list :tag "File & Outline path" - (const :format "" file+olp) - ,file-variants - ,olp-variants) + (list :tag "File [ & Outline path ]" + (const :format "" file+olp) + ,file-variants + (choice :tag "Outline path" + (const :tag "Top level" nil) + ,@olp-variants-choices)) (list :tag "File & Regexp" (const :format "" file+regexp) ,file-variants (regexp :tag " Regexp")) - (list :tag "File [ & Outline path ] & Date tree" - (const :format "" file+olp+datetree) - ,file-variants - ,(append - olp-variants - '((const :tag "Date tree at top level" nil)))) + (list :tag "File [ & Outline path ] & Date tree" + (const :format "" file+olp+datetree) + ,file-variants + (choice :tag "Outline path" + (const :tag "Date tree at top level" nil) + ,@olp-variants-choices)) (list :tag "File & function" (const :format "" file+function) ,file-variants @@ -1052,17 +1064,20 @@ for `entry'-type templates")) (defun org-capture-set-target-location (&optional target) "Find TARGET buffer and position. Store them in the capture property list." - (let ((target-entry-p t)) + (let* ((target-entry-p t) + (set-target-to-file + (lambda (path) + (set-buffer (org-capture-target-buffer path)) + (org-capture-put-target-region-and-position) + (widen) + (setq target-entry-p nil)))) (save-excursion (pcase (or target (org-capture-get :target)) ((or `here `(here)) (org-capture-put :exact-position (point) :insert-here t)) (`(file ,path) - (set-buffer (org-capture-target-buffer path)) - (org-capture-put-target-region-and-position) - (widen) - (setq target-entry-p nil)) + (funcall set-target-to-file path)) (`(id ,(and id (or (pred stringp) (pred symbolp)))) (pcase (org-id-find id) (`(,path . ,position) @@ -1094,15 +1109,24 @@ Store them in the capture property list." (unless (bolp) (insert "\n")) (insert "* " headline "\n") (forward-line -1))) - (`(file+olp ,path . ,(and outline-path (guard outline-path))) + (`(file+olp ,path . ,outline-path) (let* ((expanded-file-path (org-capture-expand-file path)) - (m (org-find-olp (cons expanded-file-path - (apply #'org-capture-expand-olp expanded-file-path outline-path))))) - (set-buffer (marker-buffer m)) - (org-capture-put-target-region-and-position) - (widen) - (goto-char m) - (set-marker m nil))) + (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path))) + ;; Vary behavior depending on whether EXPANDED-OLP is nil + ;; or non-nil. If EXPANDED-OLP is non-nil, then get a + ;; marker at that olp. If expanded-olp is nil (i.e., no + ;; olp is provided), then get a marker at the current + ;; position in the target file. + (if expanded-olp + (let ((m (org-find-olp (cons expanded-file-path expanded-olp)))) + (set-buffer (marker-buffer m)) + (org-capture-put-target-region-and-position) + (widen) + (goto-char m) + (set-marker m nil)) + ;; No olp provided, so behave as the (file ...) target + ;; specification does + (funcall set-target-to-file expanded-file-path)))) (`(file+regexp ,path ,(and regexp (pred stringp))) (set-buffer (org-capture-target-buffer path)) (org-capture-put-target-region-and-position) @@ -1117,12 +1141,17 @@ Store them in the capture property list." (setq target-entry-p (and (derived-mode-p 'org-mode) (org-at-heading-p))))) (`(file+olp+datetree ,path . ,outline-path) - (let ((m (if outline-path - (let ((expanded-file-path (org-capture-expand-file path))) - (org-find-olp (cons expanded-file-path - (apply #'org-capture-expand-olp expanded-file-path outline-path)))) - (set-buffer (org-capture-target-buffer path)) - (point-marker)))) + (let* ((expanded-file-path (org-capture-expand-file path)) + (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path)) + ;; Vary behavior depending on whether EXPANDED-OLP is + ;; nil or non-nil. If EXPANDED-OLP is non-nil, then + ;; get a marker at that olp. If EXPANDED-OLP is nil + ;; (i.e., no olp is provided), then get a marker at + ;; the current position in the target file. + (m (if expanded-olp + (org-find-olp (cons expanded-file-path expanded-olp)) + (set-buffer (org-capture-target-buffer expanded-file-path)) + (point-marker)))) (set-buffer (marker-buffer m)) (org-capture-put-target-region-and-position) (widen) @@ -1183,7 +1212,7 @@ Store them in the capture property list." (org-today)))) ;; the following is the keep-restriction argument for ;; org-datetree-find-date-create - (when outline-path 'subtree-at-point)))) + (when expanded-olp 'subtree-at-point)))) (`(file+function ,path ,(and function (pred functionp))) (set-buffer (org-capture-target-buffer path)) (org-capture-put-target-region-and-position) @@ -1232,20 +1261,24 @@ an error." (defun org-capture-expand-olp (file &rest olp) "Expand functions, symbols and outline paths in FILE for OLP. -When OLP is a function, call it with no arguments while the current -buffer is the FILE-visiting buffer. When it is a variable, return its -value. When it is a list of string, return it. In any other case, -signal an error." - (let* ((first (car olp)) - (final-olp (cond ((not (memq nil (mapcar #'stringp olp))) olp) - ((and (not (cdr olp)) (functionp first)) - (with-current-buffer (find-file-noselect file) - (funcall first))) - ((and (not (cdr olp)) (symbolp first) (boundp first)) - (symbol-value first)) - (t nil)))) - (or final-olp - (error "org-capture: Invalid outline path target: %S" olp)))) +Return a list of strings representing an outline path (OLP) in FILE. + +The behavior of this function is as follows: +- When OLP is a function, call it with no arguments while the current + buffer is the FILE-visiting buffer. +- When it is a variable, return its value. +- When it is a list of strings, return that list. +- When OLP is not provided or is nil, return nil. +In any other case, signal an error." + (let* ((first (car olp))) + (cond ((and (= 1 (length olp)) (null first)) nil) + ((not (memq nil (mapcar #'stringp olp))) olp) + ((and (not (cdr olp)) (functionp first)) + (with-current-buffer (find-file-noselect file) + (funcall first))) + ((and (not (cdr olp)) (symbolp first) (boundp first)) + (symbol-value first)) + (t (error "org-capture: Invalid outline path target: %S" olp))))) (defun org-capture-expand-file (file) "Expand functions, symbols and file names for FILE. diff --git a/testing/lisp/test-org-capture.el b/testing/lisp/test-org-capture.el index 6b49a2df7..089bd5989 100644 --- a/testing/lisp/test-org-capture.el +++ b/testing/lisp/test-org-capture.el @@ -1091,6 +1091,20 @@ before\nglobal-before\nafter\nglobal-after" '("A" "B" "C") (org-test-with-temp-text-in-file "" (org-capture-expand-olp buffer-file-name "A" "B" "C")))) + ;; `org-capture-expand-olp' should return nil if the outline path is + ;; nil + (should + (equal + nil + (org-test-with-temp-text-in-file "" + (org-capture-expand-olp buffer-file-name nil)))) + ;; `org-capture-expand-olp' should return nil if the outline path is + ;; omitted + (should + (equal + nil + (org-test-with-temp-text-in-file "" + (org-capture-expand-olp buffer-file-name)))) ;; The current buffer during the funcall of the lambda is the temporary ;; test file. (should -- 2.53.0
From 1d41bd1150b4de0a5355445fb42dc9dbd780f30f Mon Sep 17 00:00:00 2001 From: Kristoffer Balintona <[email protected]> Date: Fri, 9 May 2025 11:40:13 -0500 Subject: [PATCH 2/2] lisp/org-capture.el: Accept omitted or nil headline for the file+headline * doc/org-manual.org (Template elements): Mention new accepted file+headline specification form. * etc/ORG-NEWS: (The ~file+headline~ capture target specification now accepts omitted or nil headline): Document new behavior for file+headline target. * lisp/org-capture.el (org-capture-templates): Update docstring. (org-capture-set-target-location): The file+headline specification now accepts a nil or omitted headline. (org-capture-expand-headline): Return nil when the supplied headline is nil. * testing/lisp/test-org-capture.el (test-org-capture/org-capture-expand-headline): Add new test for new behavior as well as existing ones. --- doc/org-manual.org | 2 + etc/ORG-NEWS | 7 +++ lisp/org-capture.el | 81 ++++++++++++++++++-------------- testing/lisp/test-org-capture.el | 28 +++++++++++ 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index ac6b7ab40..37267f018 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -8109,6 +8109,8 @@ Now lets look at the elements of a template definition. Each entry in Filing as child of this entry, or in the body of the entry. + - =(file+headline <file-spec>)= :: + - =(file+headline <file-spec> "node headline")= :: - =(file+headline <file-spec> function-returning-string)= :: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 3236c39cc..5d7b43a3a 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -252,6 +252,13 @@ outline path may be omitted or set to ~nil~, in which case the entry is inserted at the top level of the target file, respecting ~:prepend~. +*** The ~file+headline~ capture target specification now accepts omitted or nil headline + +~file+headline~ is a target specification in ~org-capture-templates~. +Previously, ~file+headline~ required a headline. Now, the headline +may be omitted or set to ~nil~, in which case the entry is inserted at +the top level of the target file, respecting ~:prepend~. + *** Source blocks fall back to Fundamental mode Org now falls back to Fundamental mode for source blocks when the diff --git a/lisp/org-capture.el b/lisp/org-capture.el index c1676dca3..7f79bedd7 100644 --- a/lisp/org-capture.el +++ b/lisp/org-capture.el @@ -202,10 +202,16 @@ target Specification of where the captured item should be placed. (id \"id of existing Org entry\") File as child of this entry, or in the body of the entry + (file+headline <file-spec>) (file+headline <file-spec> \"node headline\") (file+headline <file-spec> function-returning-string) (file+headline <file-spec> symbol-containing-string) - Fast configuration if the target heading is unique in the file + Fast configuration if the target heading is unique in + the file. The entry is created under the headline + specified by a string, symbol, or function. If no + headline is provided or if the headline specification + is nil, the entry will be inserted at the top level of + <file-spec>. (file+olp <file-spec>) (file+olp <file-spec> \"Level 1 heading\" \"Level 2\" ...) @@ -491,6 +497,7 @@ you can escape ambiguous cases with a backward slash, e.g., \\%i." (const :format "" file+headline) ,file-variants (choice :tag "Headline" + (const :tag "Top level" nil) (string :tag "Headline") (function :tag "Function") (variable :tag "Variable"))) @@ -1086,29 +1093,31 @@ Store them in the capture property list." (org-capture-put-target-region-and-position) (goto-char position)) (_ (error "Cannot find target ID \"%s\"" id)))) - (`(file+headline ,path ,headline) - (set-buffer (org-capture-target-buffer path)) - ;; Org expects the target file to be in Org mode, otherwise - ;; it throws an error. However, the default notes files - ;; should work out of the box. In this case, we switch it to - ;; Org mode. - (unless (derived-mode-p 'org-mode) - (org-display-warning - (format "Capture requirement: switching buffer %S to Org mode" - (current-buffer))) - (org-mode)) - (org-capture-put-target-region-and-position) - (widen) - (goto-char (point-min)) - (setq headline (org-capture-expand-headline headline)) - (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))) + (`(file+headline ,path . ,headline-spec) + (let ((headline (org-capture-expand-headline (car headline-spec)))) + (if (null headline) + (funcall set-target-to-file path) + (set-buffer (org-capture-target-buffer path)) + ;; Org expects the target file to be in Org mode, + ;; otherwise it throws an error. However, the default + ;; notes files should work out of the box. In this case, + ;; we switch it to Org mode. + (unless (derived-mode-p 'org-mode) + (org-display-warning + (format "Capture requirement: switching buffer %S to Org mode" + (current-buffer))) + (org-mode)) + (org-capture-put-target-region-and-position) + (widen) + (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))))) (`(file+olp ,path . ,outline-path) (let* ((expanded-file-path (org-capture-expand-file path)) (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path))) @@ -1248,16 +1257,20 @@ Store them in the capture property list." (defun org-capture-expand-headline (headline) "Expand functions, symbols and headline names for HEADLINE. -When HEADLINE is a function, call it. When it is a variable, return -its value. When it is a string, return it. In any other case, signal -an error." - (let* ((final-headline (cond ((stringp headline) headline) - ((functionp headline) (funcall headline)) - ((and (symbolp headline) (boundp headline)) - (symbol-value headline)) - (t nil)))) - (or final-headline - (error "org-capture: Invalid headline target: %S" headline)))) +Return a string representing a headline. + +The behavior of this function is as follows: +- When HEADLINE is a function, call it. +- When it is a variable, return its value. +- When it is a string, return that string. +- When HEADLINE is nil, return nil. +In any other case, signal an error." + (cond ((null headline) nil) + ((stringp headline) headline) + ((functionp headline) (funcall headline)) + ((and (symbolp headline) (boundp headline)) + (symbol-value headline)) + (t (error "org-capture: Invalid headline target: %S" headline)))) (defun org-capture-expand-olp (file &rest olp) "Expand functions, symbols and outline paths in FILE for OLP. diff --git a/testing/lisp/test-org-capture.el b/testing/lisp/test-org-capture.el index 089bd5989..c5964aead 100644 --- a/testing/lisp/test-org-capture.el +++ b/testing/lisp/test-org-capture.el @@ -1083,6 +1083,34 @@ before\nglobal-before\nafter\nglobal-after" (org-capture nil "t") (buffer-string)))))) +(ert-deftest test-org-capture/org-capture-expand-headline () + "Test `org-capture-expand-headline'." + ;; `org-capture-expand-headline' should return nil when headline is + ;; nil + (should + (equal + nil + (org-capture-expand-headline nil))) + ;; `org-capture-expand-headline' should return headline if it is a + ;; string + (should + (equal + "A" + (org-capture-expand-headline "A"))) + ;; `org-capture-expand-headline' should evaluate headline if it is a + ;; function and return its value + (should + (equal + "A" + (org-capture-expand-headline (lambda () "A")))) + ;; `org-capture-expand-headline' should return the value of headline + ;; if it is a symbol + (should + (equal + "A" + (org-dlet ((temp "A")) + (org-capture-expand-headline 'temp))))) + (ert-deftest test-org-capture/org-capture-expand-olp () "Test org-capture-expand-olp." ;; `org-capture-expand-olp' accepts inlined outline path. -- 2.53.0
