feat!(capture): add option to create entry ids

This un-breaks a breaking change where we addressed an issue that
prevented ids from being generated for 'entry' capture types.  While
this was a much-asked feature, the change also disrupted user workflows
that depend on the old behavior of entries creating org headings and
nothing more.

Users can now opt in to generating a heading id by passing
`:entry-node t` in their org-roam capture templates definition.

Amend: ed94524964
This commit is contained in:
Dustin Farris
2025-09-29 16:20:57 -07:00
parent 41f9a10be5
commit 0799985296
4 changed files with 99 additions and 41 deletions

View File

@@ -847,7 +847,9 @@ of the template are similar to ~org-capture~ templates.
automatically chooses this template for you. automatically chooses this template for you.
2. The template is given a description of ~"default"~. 2. The template is given a description of ~"default"~.
3. ~plain~ text is inserted. Other options include Org headings via 3. ~plain~ text is inserted. Other options include Org headings via
~entry~. ~entry~. Note that ~entry~ type captures create regular Org headings
without IDs by default. To create an Org-roam node with an ID, use
the ~:entry-node t~ option.
4. Notice that the ~target~ that's usually in Org-capture templates is missing 4. Notice that the ~target~ that's usually in Org-capture templates is missing
here. here.
5. ~"%?"~ is the template inserted on each call to ~org-roam-capture-~. 5. ~"%?"~ is the template inserted on each call to ~org-roam-capture-~.
@@ -1221,6 +1223,10 @@ Here is a sane default configuration:
"#+title: %<%Y-%m-%d>\n")))) "#+title: %<%Y-%m-%d>\n"))))
#+end_src #+end_src
Note: The ~entry~ type capture above creates a regular Org heading without an
ID. If you want each daily entry to be an Org-roam node with its own ID, add
~:entry-node t~ to the template.
See [[*The Templating System][The Templating System]] for creating new templates. See [[*The Templating System][The Templating System]] for creating new templates.
*** Usage *** Usage

View File

@@ -1219,7 +1219,9 @@ automatically chooses this template for you.
The template is given a description of @code{"default"}. The template is given a description of @code{"default"}.
@item @item
@code{plain} text is inserted. Other options include Org headings via @code{plain} text is inserted. Other options include Org headings via
@code{entry}. @code{entry}. Note that @code{entry} type captures create regular Org headings
without IDs by default. To create an Org-roam node with an ID, use
the @code{:entry-node t} option.
@item @item
Notice that the @code{target} that's usually in Org-capture templates is missing Notice that the @code{target} that's usually in Org-capture templates is missing
here. here.
@@ -1703,6 +1705,10 @@ Here is a sane default configuration:
"#+title: %<%Y-%m-%d>\n")))) "#+title: %<%Y-%m-%d>\n"))))
@end lisp @end lisp
Note: The @code{entry} type capture above creates a regular Org heading without an
ID@. If you want each daily entry to be an Org-roam node with its own ID, add
@code{:entry-node t} to the template.
See @ref{The Templating System} for creating new templates. See @ref{The Templating System} for creating new templates.
@node Usage @node Usage
@@ -2421,5 +2427,5 @@ When GOTO is non-nil, go the note without creating an entry."
@printindex vr @printindex vr
Emacs 30.1 (Org mode 9.7.29) Emacs 30.2 (Org mode 9.7.34)
@bye @bye

View File

@@ -63,9 +63,11 @@ description A short string describing the template, which will be shown
during selection. during selection.
type The type of entry. Valid types are: type The type of entry. Valid types are:
entry an Org node, with a headline. Will be filed entry an Org heading. Will be filed as the child
as the child of the target entry or as a of the target entry or as a top level entry.
top level entry. Its default template is: Use :entry-node t to create an ID for this
heading, making it an org-roam node.
Its default template is:
\"* %?\n %a\" \"* %?\n %a\"
item a plain list item, will be placed in the item a plain list item, will be placed in the
first plain list at the target location. first plain list at the target location.
@@ -135,6 +137,11 @@ The following options are supported for the :target property:
The rest of the entry is a property list of additional options. Recognized The rest of the entry is a property list of additional options. Recognized
properties are: properties are:
:entry-node When set to t for entry-type captures, creates an ID for
the captured entry heading. When nil or not specified,
no ID is created for the entry. Only applies to templates
with type 'entry'.
:prepend Normally newly captured information will be appended at :prepend Normally newly captured information will be appended at
the target location (last child, last table line, the target location (last child, last table line,
last list item...). Setting this property will last list item...). Setting this property will
@@ -390,7 +397,7 @@ This variable is populated dynamically, and is only non-nil
during the Org-roam capture process.") during the Org-roam capture process.")
(defconst org-roam-capture--template-keywords (list :target :id :link-description :call-location (defconst org-roam-capture--template-keywords (list :target :id :link-description :call-location
:region) :region :entry-node)
"Keywords used in `org-roam-capture-templates' specific to Org-roam.") "Keywords used in `org-roam-capture-templates' specific to Org-roam.")
;;; Main entry point ;;; Main entry point
@@ -578,12 +585,13 @@ capture target."
target-entry-p (and (derived-mode-p 'org-mode) (org-at-heading-p)))))) target-entry-p (and (derived-mode-p 'org-mode) (org-at-heading-p))))))
;; Setup `org-id' for the current capture target and return it back to the ;; Setup `org-id' for the current capture target and return it back to the
;; caller. ;; caller.
;; Unless it's an entry type, then we want to create an ID for the entry instead ;; For entry type: only create ID if :entry-node is t
(pcase (org-capture-get :type) (pcase (org-capture-get :type)
('entry ('entry
(when (org-roam-capture--get :entry-node)
(advice-add #'org-capture-place-entry :after #'org-roam-capture--create-id-for-entry) (advice-add #'org-capture-place-entry :after #'org-roam-capture--create-id-for-entry)
(org-roam-capture--put :new-node-p t) (org-roam-capture--put :new-node-p t)
(setq id (org-roam-node-id org-roam-capture--node))) (setq id (org-roam-node-id org-roam-capture--node))))
(_ (_
(save-excursion (save-excursion
(goto-char p) (goto-char p)
@@ -610,7 +618,7 @@ capture target."
(advice-remove #'org-capture-place-template #'org-roam-capture-run-new-node-hook-a)) (advice-remove #'org-capture-place-template #'org-roam-capture-run-new-node-hook-a))
(defun org-roam-capture--create-id-for-entry () (defun org-roam-capture--create-id-for-entry ()
"Create the ID for the new entry." "Create the ID for the new entry heading."
(org-entry-put (point) "ID" (org-roam-capture--get :id)) (org-entry-put (point) "ID" (org-roam-capture--get :id))
(advice-remove #'org-capture-place-entry #'org-roam-capture--create-id-for-entry)) (advice-remove #'org-capture-place-entry #'org-roam-capture--create-id-for-entry))

View File

@@ -58,38 +58,76 @@
(org-roam-capture--fill-template (lambda () "foo")) (org-roam-capture--fill-template (lambda () "foo"))
:to-equal "foo"))) :to-equal "foo")))
(describe "org-roam-capture entry-type ID creation" (describe "org-roam-capture :entry-node option"
(it "creates ID for entry-type captures" :var ((temp-dir) (org-roam-directory) (org-roam-db-location))
(let* ((temp-dir (make-temp-file "org-roam-test" t))
(test-file (expand-file-name "test.org" temp-dir)) (before-each
(org-roam-directory temp-dir) (setq temp-dir (make-temp-file "org-roam-test" t))
(org-roam-capture--node (org-roam-node-create :id (org-id-new))) (setq org-roam-directory temp-dir)
(org-roam-capture--info (make-hash-table :test 'equal)) (setq org-roam-db-location (expand-file-name "org-roam.db" temp-dir))
capture-id) (org-roam-db-sync))
(unwind-protect
(progn (after-each
;; Create initial file content (delete-directory temp-dir t))
(it "does not create ID for entry-type capture without :entry-node option"
(let* ((test-file (expand-file-name "test-parent.org" temp-dir))
(org-roam-capture-templates
'(("t" "test" entry "* ${title}"
:target (file "test-parent.org")
:unnarrowed t))))
;; Create parent file with an existing heading
(with-temp-file test-file (with-temp-file test-file
(insert "#+TITLE: Test File\n\n* Parent Heading\n:PROPERTIES:\n:ID: parent-id\n:END:\n")) (insert "#+TITLE: Parent File\n\n* Existing Heading\n"))
;; Mock org-capture context and get-target ;; Mock the node selection to return a new node
(cl-letf* (((symbol-function 'org-capture-get) (cl-letf (((symbol-function 'org-roam-node-read)
(lambda (prop) (lambda (&rest _)
(pcase prop (org-roam-node-create :title "New Entry Without ID"))))
(:type 'entry)
(_ nil))))
((symbol-function 'org-roam-capture--get-target)
(lambda () `(file ,test-file))))
;; Call the setup function (org-roam-capture)
(with-current-buffer (find-file-noselect test-file)
(org-roam-capture--setup-target-location)
(setq capture-id (org-roam-capture--get :id)))
;; Verify ID was created and stored for entry type ;; Finalize the capture
(expect capture-id :not :to-be nil) (org-capture-finalize))
(expect (org-roam-capture--get :new-node-p) :to-be t)))
(delete-directory temp-dir t)))) ;; Verify the captured entry exists but has no ID
(with-temp-buffer
(insert-file-contents test-file)
(goto-char (point-min))
(re-search-forward "^\\* New Entry Without ID$")
(let ((has-id (org-entry-get (point) "ID")))
(expect has-id :to-be nil)))))
(it "creates ID for entry-type capture with :entry-node t option"
(let* ((test-file (expand-file-name "test-parent-with-id.org" temp-dir))
(org-roam-capture-templates
'(("t" "test" entry "* ${title}"
:target (file "test-parent-with-id.org")
:entry-node t
:unnarrowed t))))
;; Create parent file with an existing heading
(with-temp-file test-file
(insert "#+TITLE: Parent File\n\n* Existing Heading\n"))
;; Mock the node selection to return a new node
(cl-letf (((symbol-function 'org-roam-node-read)
(lambda (&rest _)
(org-roam-node-create :title "New Entry With ID"))))
(org-roam-capture)
;; Finalize the capture
(org-capture-finalize))
;; Verify the captured entry has an ID
(with-temp-buffer
(insert-file-contents test-file)
(goto-char (point-min))
(re-search-forward "^\\* New Entry With ID$")
(let ((has-id (org-entry-get (point) "ID")))
(expect has-id :not :to-be nil)))))
(it "creates ID at target for non-entry-type captures" (it "creates ID at target for non-entry-type captures"
(let* ((temp-dir (make-temp-file "org-roam-test" t)) (let* ((temp-dir (make-temp-file "org-roam-test" t))