0

I would like to create headings and subheadings with org-capture if they don't exist with file+olp in my templates. The problem is that I get this message Heading not found on level 1.

I'm not sure this is the best approach but I thought about 2 solutions add-advice and condition-case. Not sure about org-capture-put, org-capture-set, and org-capture-set-target-location.

With add-advice I'm not sure how to insert my code at the start of function to add the headings if they are not found, unless I somehow used cl-letf on error. Note, that I'm not in the target file.

(advice-add #'org-find-olp  :around
  (lambda (f &rest args)
    (progn (message "I'm not in the target file I am in: %s" (buffer-name))
           (apply f args))))

With condition-case, I have the same problem as with add-advice, I'm not in the target buffer. With this solution I would have to figure out a way to integrate this with org-capture, add-advice might be better because of this.

(defun my/org-find-olp ()
  "Create headings if they do not exist."
  (condition-case err
    (org-find-olp
      '("~/somfile.org" "Heading1" "Sub1" "Sub2"))
      (error
        (if (string-match-p "^Heading not found" (car (cdr err)))
          (message "Adding headings in %s" (current-buffer))
          (error (error err))))))
(my/org-find-olp )

My last resort would be to:

  1. Edit the org source code directly.
  2. Create the headings manually.
2
  • 1
    Given that the capture template is long lived (presumably you don't change the list of headings you specify in it often - and most probably not at all), I would recommend that you create the headings manually in the target file when you create the template and not worry about it. IMO, this is a task that is not worth automating.
    – NickD
    Commented May 2, 2023 at 18:46
  • I completely agree with you, I thought about that before posting this because it makes me ask if I really want to add that subheading or heading. Even though more than likely I won't implement it, I'm curious to know how to do it. The only thing that comes to mind is cl-letf on error.
    – Rick
    Commented May 2, 2023 at 18:53

2 Answers 2

0

[Not a complete answer (but too long for a comment), just a sketch of a possible implementation - untested and possibly wrong. The main assumption is that I can advise a function and change its argument list by adding an extra optional argument: I think that's possible but I'm not sure.]

If I had to do it, I would advise org-find-olp to take an extra optional boolean argument, create-if-necessary. If the arg is true, the new function would use its first argument (which is a list consisting of the file path and the list of headings) to check if the file exists and, if not, create it; also it would check that the heading hierarchy exists within that file and, if not, create it or add any missing components; then it would call the existing org-find-olp without the extra argument with the assurance that the pieces it needs exist already.

The problem is that the call sites of org-find-olp would have to change to pass the new argument when needed.

There are three places in Org mode that org-find-olp is called, although there may be more in packages that are added through ELPA/MELPA (I don't have any such, but my package list is rather Spartan). Two of the three places are the file+olp and file+olp+datetree targets in org-capture; the third is in org-mobile. You'd have to change (at least) the file+olp target to call org-find-olp with extra args. Instead of:

    ;; line 1032 or thereabouts in org-capture.el
    ...
    (`(file+olp ,path . ,outline-path)
     (let ((m (org-find-olp (cons (org-capture-expand-file path)
                                  outline-path))))
       (set-buffer (marker-buffer m))
       ... 

you'd write:

    ...
    (`(file+olp ,path . ,outline-path)
     (let ((m (org-find-olp (cons (org-capture-expand-file path)
                                  outline-path) nil t)))
       (set-buffer (marker-buffer m))
       ...

You'd leave the org-mobile call (as well as any other calls from other modules) unmodified, unless you investigate them and you want them to have the same behavior as the capture calls.

The advice would be something like this (Caution: pseudocode ahead):

(defun org-find-olp-create (orig-func path &optional this-buffer create-if-necessary)
   (when create-if-necessary
      (let ((filepath (car path))
            (headings (cdr path))
        (when <filepath does not exist>
           <create it>)
        <check headings in filepath and create/add missing>))
   ;; call the original function without the last argument
   (apply orig-func (list path this-buffer)))

(advice-add 'org-find-olp :around #'org-find-olp-create)

Note that if you remove the advice, you will also have to revert the changes in org-capture (and any other changes you might have made in call sites from other modules). The fact that this method requires changes to code other than just advising the function is its major disadvantage. Maybe somebody might be able to suggest a better method that does not require such changes, but I can't help but think that it would require more complicated tricks to accomplish that.

Note also that the actual work that org-find-olp-create would have to do to create the file and the headings if necessary, would need to be implemented. The pseudocode above is just placeholders for the actual implementation.

To reiterate what I said in my comment: some things are not worth automating. This is one of them.

3
  • I think you're talking about editing the source code when you mentioned You'd have to change (at least) the file+olp target. I guess this is where developers draw the line and edit the source code directly but yea I agree with you, some things are not worth automating. YAGNI
    – Rick
    Commented May 3, 2023 at 13:27
  • Yes, all that the above does is to minimize the changes that you'd have to maintain in your fork. But it is a fork.
    – NickD
    Commented May 3, 2023 at 17:06
  • That looks complex but I understand what you mean. I think I'm better off just manually adding them. However, there are a few things that I tried that didn't work. I posted an answer, not sure why my last two functions with cl-letf don't work?
    – Rick
    Commented May 3, 2023 at 19:55
0

To answer the question, you're better off adding the headings manually but there are some possibilities.

UPDATE I created this function it's an improvement on the one below. I did not want to use file+function but it mimics outline path like file+olp, but this function creates the headings.

(defun my/org-find-or-create-hl (&rest hls)
"For use with `org-capture-templates'.

Find headings or create them if they do not exist.
Headings are searched in descending order.

Example template:

(add-to-list \\='org-capture-templates
                 \\=`(\"j\"
                   \"Testing\"
                   entry
                   (file+function \"~/notes.org\" (lambda ()(my/org-find-or-create-hl  \"H1\" \"H2\" \"H3\" )))
                   \"* %?\" :kill-buffer t :unnarrowed t ))
⇒
* H1
** H2
*** H3
**** Your Entry "
  (goto-char (point-min))
  (let ((headings (reverse hls))
      (found nil))
  (catch 'break-my-loop
    (dolist (hl  headings)
      (let ((pos (org-find-exact-headline-in-buffer hl)))
        (when pos (setq found pos)(throw 'break-my-loop t)))))
   
  (if found (goto-char found)
    ;; NS why point-max works but it works with prepend
    (progn
      (goto-char (point-max))
      (dotimes (i (length headings))
        (insert (format "%s %s\n" (make-string (+ i 1) ?*)(nth i (reverse headings)))))
      (org-backward-heading-same-level 2)))))

Although I did not want to use file+function, this is a solution. With some tweaking you can make it behave like file+olp by using apply then creating the headings as needed (e.g., my/find-some-hl "* Not Found" "Sub1" "Sub 2"). It would require more work.

(defun my/org--create-heading (pattern)
  "Find a heading, create it if it does not exist."
 (let ((pat pattern))
    (beginning-of-buffer) 
    (if (re-search-forward pat nil t)
        nil
        (progn (beginning-of-buffer)(insert pat "\n")(my/org--create-heading pat)))))
        
(add-to-list 'org-capture-templates
                 `("j"
                   "Testing"
                   entry
                   (file+function "~/somefile.org" (lambda () (my/org--create-heading  "* Not Found" )) )
                   "* %?" :kill-buffer t :unnarrowed t :prepend t))

As Nick mentioned you can edit org-find-olp to take an extra optional argument. Adding hooks is a possibility as well.

This was a situation in which I was trying to edit the header of the org capture. Which is a similar situation in which you're wanting to run a function in the scope of another function.

I wanted to edit the org-mks to display different text via text properties when entering keys in org capture.

Without editing the code directly in org-mks this is what I came up with. However, this does not work. Markdown mode is only an example, I was planning on changing the text properties of the header "Select a capture template."

(defun my/save--window-excursion (&rest body)
  "Override save-window-excursion inside org-mks to use our functions."
 (save-window-excursion
  (progn
   (markdown-mode)   
   body (markdown-mode)))) 

(defun my/org-mks ()
    "Change save-window-excursion to my/save--window-excursion in function org-mks"
    (cl-letf (((symbol-function #'save-window-excursion ) #'my/save--window-excursion))
      (org-mks org-capture-templates
           "Select a capture template\\n========================="
           "Template key: "
           '(("C" "Customize org-capture-templates")
         ("q" "Abort")))))
(my/org-mks)

This one I think is recursive not sure why.

(defun my/org-switch-to-buffer-other-window (&rest args)
 "Override org-switch-to-buffer-other-window in the scope of my/org-mks."
;; Run text properties code here to change the heading "Select a capture template to
;; Current key description or put all the description in the key.
;; EG: wbpn Work Boss Projects Next
 (org-switch-to-buffer-other-window  "*HA HA*")
)
(defun my/org-mks ()
    "Change org-switch-to-buffer-other-window to my/org-switch-to-buffer-other-window in function org-mks"
    (interactive)
    (cl-letf (((symbol-function #'org-switch-to-buffer-other-window) #'my/org-switch-to-buffer-other-window))
    (org-mks org-capture-templates
           "Select a capture F template\n========================="
           "Template key: "
           '(("C" "Customize org-capture-templates")
         ("q" "Abort")))
      ))
(my/org-mks)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.