On Thu, May 08 2025, Ihor Radchenko wrote: > Kristoffer Balintona <krisbalint...@gmail.com> writes: > >> Currently, `org-find-olp' assumes that it is being passed a file path + >> an outline path, but when the olp function returns nil, only a file path >> is returned, so `org-find-olp' ends up returning nil when >> `org-capture-set-target-location' expects it to return a marker. Since >> `org-find-olp' is always run when the outline-path parameter is >> provided, we run into issues when it is a function returning nil. > >> Taking a look at the code, it seems like file+olp+datetree and file+olp >> both behave like this. >> >> So my question is: which parts of the code should handle an outline-path >> function returning nil? Should we change the behavior of `org-find-olp' >> and/or `'org-capture-expand-olp', or should we change things just in >> `org-capture-set-target-location'? > > I think that we should leave `org-find-olp' alone - it is used in more > general scope (outside org-capture), and it is not at all clear where > the marker to file-only path should point. > > In contrast, `org-capture-exapnd-olp' was introduced to handle various > forms of the outline path specification in the capture template. If we > want to add a new meaning to that specification (returned or explicit > nil value), it is perfectly fine to change that function. > >> With respect to file+headline: I think a similar dilemma applies there. >> It looks like the relevant function is `org-capture-expand-headline': >> should we change that function to handle the case of its argument being >> a function that returns nil or have that handling in >> `org-capture-set-target-location'? > > `org-capture-expand-headline' serves the same purpose as > `org-capture-expand-olp' - helper function to parse possible capture > template specifications. We can extend it.
Thank you for the help. I've attached a diff for a set of proposed changes. It has `org-capture-expand-olp' and `org-capture-expand-headline' return the symbol 'file in certain cases and has `org-capture-set-target-location' treat those cases specially, inserting the capture entry at the top level. Additionally, I updated the docstring of `org-capture-templates' appropriately and added an additional test in `test-org-capture/org-capture-expand-olp'. What do you think? If these changes are okay, I can turn them into an actual patch. Although I've never contributed to org before, so I will have to read up on the conventions for patches. A separate matter: I find it unusual that we have the file+olp and file target specifications but not a file+datetree target specification. As a result, file+olp+datetree accepts a nil olp to insert the datetree on the top level but file+olp does not because that would just be equivalent to the file target specification. I feel like we should either add one or do something like removing the file specification and having file+olp cover its case. -- With appreciation, Kristoffer
diff --git a/lisp/org-capture.el b/lisp/org-capture.el index 6d395406cf..f418e9fba9 100644 --- a/lisp/org-capture.el +++ b/lisp/org-capture.el @@ -205,24 +205,36 @@ (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. If function-returning-list-of-strings or + symbol-containing-list-of-strings return the symbol + \\='file,the entry will be inserted at the end of + <file-spec> on top level. (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. If function-returning-list-of-strings or + symbol-containing-list-of-strings return the symbol + \\='file,the entry will be inserted at the end of + <file-spec> on top level. (file+regexp <file-spec> \"regexp to find location\") File to the entry 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 - :time-prompt property. To create a week-tree, use the - :tree-type property. + The tree will be inserted at the end of <file-spec> on + top level if no heading is given or if + function-returning-list-of-strings or + symbol-containing-list-of-strings return the symbol + \\='file. To prompt for date instead of using TODAY, + use the :time-prompt property. To create a week-tree, + use the :tree-type property. (file+function <file-spec> function-finding-location) A function to find the right location in the file @@ -1055,18 +1067,28 @@ (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))) + (pcase headline + ('file (goto-char (point-max))) + ((pred stringp) + (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 . ,(and outline-path (guard 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))))) + (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path)) + ;; If expanded-olp is 'file, then create the datetree + ;; at the end of the file specified by + ;; expanded-file-path + (m (if (not (eq expanded-olp 'file)) + (org-find-olp (cons expanded-file-path expanded-olp)) + (set-buffer (org-capture-target-buffer expanded-file-path)) + (save-restriction (widen) (point-max-marker))))) (set-buffer (marker-buffer m)) (org-capture-put-target-region-and-position) (widen) @@ -1086,12 +1108,16 @@ (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)) + ;; If expanded-olp is 'file, then create the datetree + ;; at the end of the file specified by + ;; expanded-file-path + (file-level-datetree-p (eq expanded-olp 'file)) + (m (if (not file-level-datetree-p) + (org-find-olp (cons expanded-file-path expanded-olp)) + (set-buffer (org-capture-target-buffer expanded-file-path)) + (save-restriction (widen) (point-max-marker))))) (set-buffer (marker-buffer m)) (org-capture-put-target-region-and-position) (widen) @@ -1152,7 +1178,7 @@ (org-today)))) ;; the following is the keep-restriction argument for ;; org-datetree-find-date-create - (when outline-path 'subtree-at-point)))) + (unless file-level-datetree-p 'subtree-at-point)))) (`(file+function ,path ,(and function (pred functionp))) (set-buffer (org-capture-target-buffer path)) (org-capture-put-target-region-and-position) @@ -1188,9 +1214,18 @@ (defun org-capture-expand-headline (headline) "Expand functions, symbols and headline names for HEADLINE. +Return a string representing a 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." +an error. + +This function may also return the symbol \\='file if HEADLINE, when it +is a function or variable, returns that symbol. Such cases will be +interpreted by `org-capture-set-target-location' as a request to target +the file directly (rather than a specific headline). This behavior +allows users to dynamically set a template\\='s target location to a +specific headline or the top-level of a file." (let* ((final-headline (cond ((stringp headline) headline) ((functionp headline) (funcall headline)) ((and (symbolp headline) (boundp headline)) @@ -1201,12 +1236,22 @@ (defun org-capture-expand-olp (file &rest olp) "Expand functions, symbols and outline paths in FILE for OLP. +Return a list of strings representing an outline path (OLP) in FILE. + 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." +value. When it is a list of strings, return it. In any other case, +signal an error. + +This function may also return the symbol \\='file if OLP, when it is a +function or variable, returns that symbol. Such cases will be +interpreted by `org-capture-set-target-location' as a request to target +the file directly (rather than a specific outline path). This behavior +allows users to dynamically set a template\\='s target location to a +specific outline path or the top-level of a file." (let* ((first (car olp)) - (final-olp (cond ((not (memq nil (mapcar #'stringp olp))) olp) + (final-olp (cond ((equal olp '(nil)) 'file) + ((not (memq nil (mapcar #'stringp olp))) olp) ((and (not (cdr olp)) (functionp first)) (with-current-buffer (find-file-noselect file) (funcall first)))