org-roam-capture.el (36884B)
1 ;;; org-roam-capture.el --- Capture functionality -*- coding: utf-8; lexical-binding: t; -*- 2 3 ;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> 4 5 ;; Author: Jethro Kuan <jethrokuan95@gmail.com> 6 ;; URL: https://github.com/org-roam/org-roam 7 ;; Keywords: org-mode, roam, convenience 8 ;; Version: 2.1.0 9 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) 10 11 ;; This file is NOT part of GNU Emacs. 12 13 ;; This program is free software; you can redistribute it and/or modify 14 ;; it under the terms of the GNU General Public License as published by 15 ;; the Free Software Foundation; either version 3, or (at your option) 16 ;; any later version. 17 ;; 18 ;; This program is distributed in the hope that it will be useful, 19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 ;; GNU General Public License for more details. 22 ;; 23 ;; You should have received a copy of the GNU General Public License 24 ;; along with GNU Emacs; see the file COPYING. If not, write to the 25 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 26 ;; Boston, MA 02110-1301, USA. 27 28 ;;; Commentary: 29 ;; 30 ;; This module provides `org-capture' functionality for Org-roam. With this 31 ;; module the user can capture new nodes or capture new content to existing 32 ;; nodes. 33 ;; 34 ;;; Code: 35 (require 'org-roam) 36 37 ;;;; Declarations 38 (defvar org-end-time-was-given) 39 40 ;;; Options 41 (defcustom org-roam-capture-templates 42 '(("d" "default" plain "%?" 43 :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" 44 "#+title: ${title}\n") 45 :unnarrowed t)) 46 "Templates for the creation of new entries within Org-roam. 47 48 Each entry is a list with the following items: 49 50 keys The keys that will select the template, as a string, characters only, for 51 example \"a\" for a template to be selected with a single key, or 52 \"bt\" for selection with two keys. When using several keys, keys 53 using the same prefix must be together in the list and preceded by a 54 2-element entry explaining the prefix key, for example: 55 56 (\"b\" \"Templates for marking stuff to buy\") 57 58 The \"C\" key is used by default for quick access to the customization of 59 the template variable. But if you want to use that key for a template, 60 you can. 61 62 description A short string describing the template, which will be shown 63 during selection. 64 65 type The type of entry. Valid types are: 66 entry an Org node, with a headline. Will be filed 67 as the child of the target entry or as a 68 top level entry. Its default template is: 69 \"* %?\n %a\" 70 item a plain list item, will be placed in the 71 first plain list at the target location. 72 Its default template is: 73 \"- %?\" 74 checkitem a checkbox item. This differs from the 75 plain list item only in so far as it uses a 76 different default template. Its default 77 template is: 78 \"- [ ] %?\" 79 table-line a new line in the first table at target location. 80 Its default template is: 81 \"| %? |\" 82 plain text to be inserted as it is. 83 84 template The template for creating the capture item. 85 If it is an empty string or nil, a default template based on 86 the entry type will be used (see the \"type\" section above). 87 Instead of a string, this may also be one of: 88 89 (file \"/path/to/template-file\") 90 (function function-returning-the-template) 91 92 in order to get a template from a file, or dynamically 93 from a function. 94 95 The template contains a compulsory :if-new property. This determines the 96 location of the new node. The :if-new property contains a list, supporting 97 the following options: 98 99 (file \"path/to/file\") 100 The file will be created, and prescribed an ID. 101 102 (file+head \"path/to/file\" \"head content\") 103 The file will be created, prescribed an ID, and head content will be 104 inserted into the file. 105 106 (file+olp \"path/to/file\" (\"h1\" \"h2\")) 107 The file will be created, prescribed an ID. The OLP (h1, h2) will be 108 created, and the point placed after. 109 110 (file+head+olp \"path/to/file\" \"head content\" (\"h1\" \"h2\")) 111 The file will be created, prescribed an ID. Head content will be 112 inserted at the start of the file. The OLP (h1, h2) will be created, 113 and the point placed after. 114 115 (file+datetree \"path/to/file\" day) 116 The file will be created, prescribed an ID. Head content will be 117 inserted at the start of the file. The datetree will be created, 118 available options are day, week, month. 119 120 (node \"title or alias or ID of an existing node\") 121 The point will be placed for an existing node, based on either, its 122 title, alias or ID. 123 124 The rest of the entry is a property list of additional options. Recognized 125 properties are: 126 127 :prepend Normally newly captured information will be appended at 128 the target location (last child, last table line, 129 last list item...). Setting this property will 130 change that. 131 132 :immediate-finish When set, do not offer to edit the information, just 133 file it away immediately. This makes sense if the 134 template only needs information that can be added 135 automatically. 136 137 :jump-to-captured When set, jump to the captured entry when finished. 138 139 :empty-lines Set this to the number of lines that should be inserted 140 before and after the new item. Default 0, only common 141 other value is 1. 142 143 :empty-lines-before Set this to the number of lines that should be inserted 144 before the new item. Overrides :empty-lines for the 145 number lines inserted before. 146 147 :empty-lines-after Set this to the number of lines that should be inserted 148 after the new item. Overrides :empty-lines for the 149 number of lines inserted after. 150 151 :clock-in Start the clock in this item. 152 153 :clock-keep Keep the clock running when filing the captured entry. 154 155 :clock-resume Start the interrupted clock when finishing the capture. 156 Note that :clock-keep has precedence over :clock-resume. 157 When setting both to t, the current clock will run and 158 the previous one will not be resumed. 159 160 :time-prompt Prompt for a date/time to be used for date/week trees 161 and when filling the template. 162 163 :tree-type When `week', make a week tree instead of the month-day 164 tree. When `month', make a month tree instead of the 165 month-day tree. 166 167 :unnarrowed Do not narrow the target buffer, simply show the 168 full buffer. Default is to narrow it so that you 169 only see the new stuff. 170 171 :table-line-pos Specification of the location in the table where the 172 new line should be inserted. It should be a string like 173 \"II-3\", meaning that the new line should become the 174 third line before the second horizontal separator line. 175 176 :kill-buffer If the target file was not yet visited by a buffer when 177 capture was invoked, kill the buffer again after capture 178 is finalized. 179 180 :no-save Do not save the target file after finishing the capture. 181 182 The template defines the text to be inserted. Often this is an 183 Org mode entry (so the first line should start with a star) that 184 will be filed as a child of the target headline. It can also be 185 freely formatted text. Furthermore, the following %-escapes will 186 be replaced with content and expanded: 187 188 %[pathname] Insert the contents of the file given by 189 `pathname'. These placeholders are expanded at the very 190 beginning of the process so they can be used to extend the 191 current template. 192 %(sexp) Evaluate elisp `(sexp)' and replace it with the results. 193 Only placeholders pre-existing within the template, or 194 introduced with %[pathname] are expanded this way. Since this 195 happens after expanding non-interactive %-escapes, those can 196 be used to fill the expression. 197 %<...> The result of `format-time-string' on the ... format specification. 198 %t Time stamp, date only. The time stamp is the current time, 199 except when called from agendas with `\\[org-agenda-capture]' or 200 with `org-capture-use-agenda-date' set. 201 %T Time stamp as above, with date and time. 202 %u, %U Like the above, but inactive time stamps. 203 %i Initial content, copied from the active region. If 204 there is text before %i on the same line, such as 205 indentation, and %i is not inside a %(sexp), that prefix 206 will be added before every line in the inserted text. 207 %a Annotation, normally the link created with `org-store-link'. 208 %A Like %a, but prompt for the description part. 209 %l Like %a, but only insert the literal link. 210 %L Like %l, but without brackets (the link content itself). 211 %c Current kill ring head. 212 %x Content of the X clipboard. 213 %k Title of currently clocked task. 214 %K Link to currently clocked task. 215 %n User name (taken from the variable `user-full-name'). 216 %f File visited by current buffer when `org-capture' was called. 217 %F Full path of the file or directory visited by current buffer. 218 %:keyword Specific information for certain link types, see below. 219 %^g Prompt for tags, with completion on tags in target file. 220 %^G Prompt for tags, with completion on all tags in all agenda files. 221 %^t Like %t, but prompt for date. Similarly %^T, %^u, %^U. 222 You may define a prompt like: %^{Please specify birthday}t. 223 The default date is that of %t, see above. 224 %^C Interactive selection of which kill or clip to use. 225 %^L Like %^C, but insert as link. 226 %^{prop}p Prompt the user for a value for property `prop'. 227 A default value can be specified like this: 228 %^{prop|default}p. 229 %^{prompt} Prompt the user for a string and replace this sequence with it. 230 A default value and a completion table can be specified like this: 231 %^{prompt|default|completion2|completion3|...}. 232 %? After completing the template, position cursor here. 233 %\\1 ... %\\N Insert the text entered at the nth %^{prompt}, where N 234 is a number, starting from 1. 235 236 Apart from these general escapes, you can access information specific to 237 the link type that is created. For example, calling `org-capture' in emails 238 or in Gnus will record the author and the subject of the message, which you 239 can access with \"%:from\" and \"%:subject\", respectively. Here is a 240 complete list of what is recorded for each link type. 241 242 Link type | Available information 243 ------------------------+------------------------------------------------------ 244 bbdb | %:type %:name %:company 245 vm, wl, mh, mew, rmail, | %:type %:subject %:message-id 246 gnus | %:from %:fromname %:fromaddress 247 | %:to %:toname %:toaddress 248 | %:fromto (either \"to NAME\" or \"from NAME\") 249 | %:date %:date-timestamp (as active timestamp) 250 | %:date-timestamp-inactive (as inactive timestamp) 251 gnus | %:group, for messages also all email fields 252 eww, w3, w3m | %:type %:url 253 info | %:type %:file %:node 254 calendar | %:type %:date 255 256 When you need to insert a literal percent sign in the template, 257 you can escape ambiguous cases with a backward slash, e.g., \\%i. 258 259 In addition to all of the above, Org-roam supports additional 260 substitutions within its templates. \"${foo}\" will look for the 261 foo property in the Org-roam node (see the `org-roam-node'). If 262 the property does not exist, the user will be prompted to fill in 263 the string value. 264 265 Org-roam templates are NOT compatible with regular Org capture: 266 they rely on additional hacks and hooks to achieve the 267 streamlined user experience in Org-roam." 268 :group 'org-roam 269 :type '(repeat 270 (choice (list :tag "Multikey description" 271 (string :tag "Keys ") 272 (string :tag "Description")) 273 (list :tag "Template entry" 274 (string :tag "Keys ") 275 (string :tag "Description ") 276 (choice :tag "Capture Type " :value entry 277 (const :tag "Org entry" entry) 278 (const :tag "Plain list item" item) 279 (const :tag "Checkbox item" checkitem) 280 (const :tag "Plain text" plain) 281 (const :tag "Table line" table-line)) 282 (choice :tag "Template " 283 (string) 284 (list :tag "File" 285 (const :format "" file) 286 (file :tag "Template file")) 287 (list :tag "Function" 288 (const :format "" function) 289 (function :tag "Template function"))) 290 (plist :inline t 291 ;; Give the most common options as checkboxes 292 :options (((const :format "%v " :if-new) 293 (choice :tag "Node location" 294 (list :tag "File" 295 (const :format "" file) 296 (string :tag " File")) 297 (list :tag "File & Head Content" 298 (const :format "" file+head) 299 (string :tag " File") 300 (string :tag " Head Content")) 301 (list :tag "File & Outline path" 302 (const :format "" file+olp) 303 (string :tag " File") 304 (list :tag "Outline path" 305 (repeat (string :tag "Headline")))) 306 (list :tag "File & Head Content & Outline path" 307 (const :format "" file+head+olp) 308 (string :tag " File") 309 (string :tag " Head Content") 310 (list :tag "Outline path" 311 (repeat (string :tag "Headline")))))) 312 ((const :format "%v " :prepend) (const t)) 313 ((const :format "%v " :immediate-finish) (const t)) 314 ((const :format "%v " :jump-to-captured) (const t)) 315 ((const :format "%v " :empty-lines) (const 1)) 316 ((const :format "%v " :empty-lines-before) (const 1)) 317 ((const :format "%v " :empty-lines-after) (const 1)) 318 ((const :format "%v " :clock-in) (const t)) 319 ((const :format "%v " :clock-keep) (const t)) 320 ((const :format "%v " :clock-resume) (const t)) 321 ((const :format "%v " :time-prompt) (const t)) 322 ((const :format "%v " :tree-type) (const week)) 323 ((const :format "%v " :unnarrowed) (const t)) 324 ((const :format "%v " :table-line-pos) (string)) 325 ((const :format "%v " :kill-buffer) (const t)))))))) 326 327 (defcustom org-roam-capture-new-node-hook nil 328 "Normal-mode hooks run when a new Org-roam node is created. 329 The current point is the point of the new node. 330 The hooks must not move the point." 331 :group 'org-roam 332 :type 'hook) 333 334 (defvar org-roam-capture-preface-hook nil 335 "Hook run when Org-roam tries to determine capture location of the node. 336 If any hook returns a value (which should be an ID), all hooks 337 after it are ignored. 338 339 With this hook you can hijack controls over the location of the 340 node for which the capture process is currently running for, or 341 use to just perform an arbitrary side effect, e.g. modify the 342 state related to the capture process. See `org-roam-protocol' and 343 `org-roam-dailies' as examples for what and how this hook is used 344 for. 345 346 If you're trying to perform the hijack, it's mandatory for you to: 347 1. Set the currently active buffer for editing operations using 348 `org-capture-target-buffer'. 349 2. Place the point in this buffer from where the location starts 350 from (e.g. if it's a file based node it should be the BOB, 351 otherwise it should be the position from where the heading 352 based node starts from). 353 3. Return the ID (as a string) of the capturing node. 354 355 If you use this hook for any other purpose, but not the hijack, 356 it's mandatory that you should return nil as the return value; so 357 the capture process would be able to setup the capture buffer. 358 359 If you need to do something when you capture new nodes, use 360 `org-roam-capture-new-node-hook' instead of this hook. 361 362 WARNING: This hook is primarily designed for the usage by the 363 extensions and packages, and requires understanding of the 364 internal capture process. If you don't understand it, you should 365 learn these internals before using this or use it at your own 366 risk breaking things.") 367 368 ;;; Variables 369 370 (defvar org-roam-capture--node nil 371 "The node passed during an Org-roam capture. 372 This variable is populated dynamically, and is only non-nil 373 during the Org-roam capture process.") 374 375 (defvar org-roam-capture--info nil 376 "A property-list of additional information passed to the Org-roam template. 377 This variable is populated dynamically, and is only non-nil 378 during the Org-roam capture process.") 379 380 (defconst org-roam-capture--template-keywords (list :if-new :id :link-description :call-location 381 :region) 382 "Keywords used in `org-roam-capture-templates' specific to Org-roam.") 383 384 ;;; Main entry point 385 ;;;###autoload 386 (cl-defun org-roam-capture- (&key goto keys node info props templates) 387 "Main entry point of `org-roam-capture' module. 388 GOTO and KEYS correspond to `org-capture' arguments. 389 INFO is a plist for filling up Org-roam's capture templates. 390 NODE is an `org-roam-node' construct containing information about the node. 391 PROPS is a plist containing additional Org-roam properties for each template. 392 TEMPLATES is a list of org-roam templates." 393 (let* ((props (plist-put props :call-location (point-marker))) 394 (org-capture-templates 395 (mapcar (lambda (template) 396 (org-roam-capture--convert-template template props)) 397 (or templates org-roam-capture-templates))) 398 (org-roam-capture--node node) 399 (org-roam-capture--info info)) 400 (when (and (not keys) 401 (= (length org-capture-templates) 1)) 402 (setq keys (caar org-capture-templates))) 403 (org-capture goto keys))) 404 405 ;;;###autoload 406 (cl-defun org-roam-capture (&optional goto keys &key filter-fn templates info) 407 "Launches an `org-capture' process for a new or existing node. 408 This uses the templates defined at `org-roam-capture-templates'. 409 Arguments GOTO and KEYS see `org-capture'. 410 FILTER-FN is a function to filter out nodes: it takes an `org-roam-node', 411 and when nil is returned the node will be filtered out. 412 The TEMPLATES, if provided, override the list of capture templates (see 413 `org-roam-capture-'.) 414 The INFO, if provided, is passed along to the underlying `org-roam-capture-'." 415 (interactive "P") 416 (let ((node (org-roam-node-read nil filter-fn))) 417 (org-roam-capture- :goto goto 418 :info info 419 :keys keys 420 :templates templates 421 :node node 422 :props '(:immediate-finish nil)))) 423 424 ;;; Capture process 425 (defun org-roam-capture-p () 426 "Return t if the current capture process is an Org-roam capture. 427 This function is to only be called when `org-capture-plist' is 428 valid for the capture (i.e. initialization, and finalization of 429 the capture)." 430 (plist-get org-capture-plist :org-roam)) 431 432 (defun org-roam-capture--get (keyword) 433 "Get the value for KEYWORD from the `org-roam-capture-template'." 434 (plist-get (plist-get org-capture-plist :org-roam) keyword)) 435 436 (defun org-roam-capture--put (&rest stuff) 437 "Put properties from STUFF into the `org-roam-capture-template'." 438 (let ((p (plist-get org-capture-plist :org-roam))) 439 (while stuff 440 (setq p (plist-put p (pop stuff) (pop stuff)))) 441 (setq org-capture-plist 442 (plist-put org-capture-plist :org-roam p)))) 443 444 ;;;; Capture target 445 (defun org-roam-capture--prepare-buffer () 446 "Prepare the capture buffer for the current Org-roam based capture template. 447 This function will initialize and setup the capture buffer, 448 create the target node (`:if-new') if it doesn't exist, and place 449 the point for further processing by `org-capture'. 450 451 Note: During the capture process this function is run by 452 `org-capture-set-target-location', as a (function ...) based 453 capture target." 454 (let ((id (cond ((run-hook-with-args-until-success 'org-roam-capture-preface-hook)) 455 ((and (org-roam-node-file org-roam-capture--node) 456 (org-roam-node-point org-roam-capture--node)) 457 (set-buffer (org-capture-target-buffer (org-roam-node-file org-roam-capture--node))) 458 (goto-char (org-roam-node-point org-roam-capture--node)) 459 (widen) 460 (org-roam-node-id org-roam-capture--node)) 461 (t 462 (org-roam-capture--setup-target-location))))) 463 (org-roam-capture--adjust-point-for-capture-type) 464 (org-capture-put :template 465 (org-roam-capture--fill-template (org-capture-get :template))) 466 (org-roam-capture--put :id id) 467 (org-roam-capture--put :finalize (or (org-capture-get :finalize) 468 (org-roam-capture--get :finalize))))) 469 470 (defun org-roam-capture--setup-target-location () 471 "Initialize the buffer, and goto the location of the new capture. 472 Return the ID of the location." 473 (let (p new-file-p) 474 (pcase (or (org-roam-capture--get :if-new) 475 (user-error "Template needs to specify `:if-new'")) 476 (`(file ,path) 477 (setq path (expand-file-name 478 (string-trim (org-roam-capture--fill-template path t)) 479 org-roam-directory)) 480 (setq new-file-p (org-roam-capture--new-file-p path)) 481 (when new-file-p (org-roam-capture--put :new-file path)) 482 (set-buffer (org-capture-target-buffer path)) 483 (widen) 484 (setq p (goto-char (point-min)))) 485 (`(file+olp ,path ,olp) 486 (setq path (expand-file-name 487 (string-trim (org-roam-capture--fill-template path t)) 488 org-roam-directory)) 489 (setq new-file-p (org-roam-capture--new-file-p path)) 490 (when new-file-p (org-roam-capture--put :new-file path)) 491 (set-buffer (org-capture-target-buffer path)) 492 (setq p (point-min)) 493 (let ((m (org-roam-capture-find-or-create-olp olp))) 494 (goto-char m)) 495 (widen)) 496 (`(file+head ,path ,head) 497 (setq path (expand-file-name 498 (string-trim (org-roam-capture--fill-template path t)) 499 org-roam-directory)) 500 (setq new-file-p (org-roam-capture--new-file-p path)) 501 (set-buffer (org-capture-target-buffer path)) 502 (when new-file-p 503 (org-roam-capture--put :new-file path) 504 (insert (org-roam-capture--fill-template head t))) 505 (widen) 506 (setq p (goto-char (point-min)))) 507 (`(file+head+olp ,path ,head ,olp) 508 (setq path (expand-file-name 509 (string-trim (org-roam-capture--fill-template path t)) 510 org-roam-directory)) 511 (setq new-file-p (org-roam-capture--new-file-p path)) 512 (set-buffer (org-capture-target-buffer path)) 513 (widen) 514 (when new-file-p 515 (org-roam-capture--put :new-file path) 516 (insert (org-roam-capture--fill-template head t))) 517 (setq p (point-min)) 518 (let ((m (org-roam-capture-find-or-create-olp olp))) 519 (goto-char m))) 520 (`(file+datetree ,path ,tree-type) 521 (setq path (expand-file-name 522 (string-trim (org-roam-capture--fill-template path t)) 523 org-roam-directory)) 524 (require 'org-datetree) 525 (widen) 526 (set-buffer (org-capture-target-buffer path)) 527 (unless (file-exists-p path) 528 (org-roam-capture--put :new-file path)) 529 (funcall 530 (pcase tree-type 531 (`week #'org-datetree-find-iso-week-create) 532 (`month #'org-datetree-find-month-create) 533 (_ #'org-datetree-find-date-create)) 534 (calendar-gregorian-from-absolute 535 (cond 536 (org-overriding-default-time 537 ;; Use the overriding default time. 538 (time-to-days org-overriding-default-time)) 539 ((org-capture-get :default-time) 540 (time-to-days (org-capture-get :default-time))) 541 ((org-capture-get :time-prompt) 542 ;; Prompt for date. Bind `org-end-time-was-given' so 543 ;; that `org-read-date-analyze' handles the time range 544 ;; case and returns `prompt-time' with the start value. 545 (let* ((org-time-was-given nil) 546 (org-end-time-was-given nil) 547 (prompt-time (org-read-date 548 nil t nil "Date for tree entry:"))) 549 (org-capture-put 550 :default-time 551 (if (or org-time-was-given 552 (= (time-to-days prompt-time) (org-today))) 553 prompt-time 554 ;; Use 00:00 when no time is given for another 555 ;; date than today? 556 (apply #'encode-time 0 0 557 org-extend-today-until 558 (cl-cdddr (decode-time prompt-time))))) 559 (time-to-days prompt-time))) 560 (t 561 ;; Current date, possibly corrected for late night 562 ;; workers. 563 (org-today))))) 564 (setq p (point))) 565 (`(node ,title-or-id) 566 ;; first try to get ID, then try to get title/alias 567 (let ((node (or (org-roam-node-from-id title-or-id) 568 (org-roam-node-from-title-or-alias title-or-id) 569 (user-error "No node with title or id \"%s\"" title-or-id)))) 570 (set-buffer (org-capture-target-buffer (org-roam-node-file node))) 571 (goto-char (org-roam-node-point node)) 572 (setq p (org-roam-node-point node))))) 573 ;; Setup `org-id' for the current capture target and return it back to the 574 ;; caller. 575 (save-excursion 576 (goto-char p) 577 (when-let* ((node org-roam-capture--node) 578 (id (org-roam-node-id node))) 579 (org-entry-put p "ID" id)) 580 (prog1 581 (org-id-get-create) 582 (run-hooks 'org-roam-capture-new-node-hook))))) 583 584 (defun org-roam-capture--new-file-p (path) 585 "Return t if PATH is for a new file with no visiting buffer." 586 (not (or (file-exists-p path) 587 (org-find-base-buffer-visiting path)))) 588 589 (defun org-roam-capture-find-or-create-olp (olp) 590 "Return a marker pointing to the entry at OLP in the current buffer. 591 If OLP does not exist, create it. If anything goes wrong, throw 592 an error, and if you need to do something based on this error, 593 you can catch it with `condition-case'." 594 (let* ((level 1) 595 (lmin 1) 596 (lmax 1) 597 (start (point-min)) 598 (end (point-max)) 599 found flevel) 600 (unless (derived-mode-p 'org-mode) 601 (error "Buffer %s needs to be in Org mode" (current-buffer))) 602 (org-with-wide-buffer 603 (goto-char start) 604 (dolist (heading olp) 605 (let ((re (format org-complex-heading-regexp-format 606 (regexp-quote heading))) 607 (cnt 0)) 608 (while (re-search-forward re end t) 609 (setq level (- (match-end 1) (match-beginning 1))) 610 (when (and (>= level lmin) (<= level lmax)) 611 (setq found (match-beginning 0) flevel level cnt (1+ cnt)))) 612 (when (> cnt 1) 613 (error "Heading not unique on level %d: %s" lmax heading)) 614 (when (= cnt 0) 615 ;; Create heading if it doesn't exist 616 (goto-char end) 617 (unless (bolp) (newline)) 618 (let (org-insert-heading-respect-content) 619 (org-insert-heading nil nil t)) 620 (unless (= lmax 1) 621 (dotimes (_ level) (org-do-demote))) 622 (insert heading) 623 (setq end (point)) 624 (goto-char start) 625 (while (re-search-forward re end t) 626 (setq level (- (match-end 1) (match-beginning 1))) 627 (when (and (>= level lmin) (<= level lmax)) 628 (setq found (match-beginning 0) flevel level cnt (1+ cnt)))))) 629 (goto-char found) 630 (setq lmin (1+ flevel) lmax (+ lmin (if org-odd-levels-only 1 0))) 631 (setq start found 632 end (save-excursion (org-end-of-subtree t t)))) 633 (point-marker)))) 634 635 (defun org-roam-capture--adjust-point-for-capture-type (&optional pos) 636 "Reposition the point for template insertion dependently on the capture type. 637 Return the newly adjusted position of `point'. 638 639 POS is the current position of point (an integer) inside the 640 currently active capture buffer, where the adjustment should 641 start to begin from. If it's nil, then it will default to 642 the current value of `point'." 643 (or pos (setq pos (point))) 644 (goto-char pos) 645 (let ((location-type (if (= pos 1) 'beginning-of-file 'heading-at-point))) 646 (and (eq location-type 'heading-at-point) 647 (cl-assert (org-at-heading-p))) 648 (pcase (org-capture-get :type) 649 (`plain 650 (cl-case location-type 651 (beginning-of-file 652 (if (org-capture-get :prepend) 653 (let ((el (org-element-at-point))) 654 (while (and (not (eobp)) 655 (memq (org-element-type el) 656 '(drawer property-drawer keyword comment comment-block horizontal-rule))) 657 (goto-char (org-element-property :end el)) 658 (setq el (org-element-at-point)))) 659 (goto-char (org-entry-end-position)))) 660 (heading-at-point 661 (if (org-capture-get :prepend) 662 (org-end-of-meta-data t) 663 (goto-char (org-entry-end-position)))))))) 664 (point)) 665 666 ;;;; Finalizers 667 (add-hook 'org-capture-prepare-finalize-hook #'org-roam-capture--install-finalize-h) 668 (defun org-roam-capture--install-finalize-h () 669 "Install `org-roam-capture--finalize' if the capture is an Org-roam capture." 670 (when (org-roam-capture-p) 671 (add-hook 'org-capture-after-finalize-hook #'org-roam-capture--finalize))) 672 673 (defun org-roam-capture--finalize () 674 "Finalize the `org-roam-capture' process." 675 (when-let ((region (org-roam-capture--get :region))) 676 (org-roam-unshield-region (car region) (cdr region))) 677 (if org-note-abort 678 (when-let ((new-file (org-roam-capture--get :new-file))) 679 (org-roam-message "Deleting file for aborted capture %s" new-file) 680 (when (find-buffer-visiting new-file) 681 (kill-buffer (find-buffer-visiting new-file))) 682 (delete-file new-file)) 683 (when-let* ((finalize (org-roam-capture--get :finalize)) 684 (org-roam-finalize-fn (intern (concat "org-roam-capture--finalize-" 685 (symbol-name finalize))))) 686 (if (functionp org-roam-finalize-fn) 687 (funcall org-roam-finalize-fn) 688 (funcall finalize)))) 689 (remove-hook 'org-capture-after-finalize-hook #'org-roam-capture--finalize)) 690 691 (defun org-roam-capture--finalize-find-file () 692 "Visit the buffer after Org-capture is done. 693 This function is to be called in the Org-capture finalization process. 694 ID is unused." 695 (switch-to-buffer (org-capture-get :buffer))) 696 697 (defun org-roam-capture--finalize-insert-link () 698 "Insert a link to ID into the buffer where Org-capture was called. 699 ID is the Org id of the newly captured content. 700 This function is to be called in the Org-capture finalization process." 701 (when-let* ((mkr (org-roam-capture--get :call-location)) 702 (buf (marker-buffer mkr))) 703 (with-current-buffer buf 704 (when-let ((region (org-roam-capture--get :region))) 705 (org-roam-unshield-region (car region) (cdr region)) 706 (delete-region (car region) (cdr region)) 707 (set-marker (car region) nil) 708 (set-marker (cdr region) nil)) 709 (org-with-point-at mkr 710 (insert (org-link-make-string (concat "id:" (org-roam-capture--get :id)) 711 (org-roam-capture--get :link-description))))))) 712 713 ;;;; Processing of the capture templates 714 (defun org-roam-capture--fill-template (template &optional org-capture-p) 715 "Expand TEMPLATE and return it. 716 It expands ${var} occurrences in TEMPLATE. When ORG-CAPTURE-P, 717 also run Org-capture's template expansion." 718 (funcall (if org-capture-p #'org-capture-fill-template #'identity) 719 (org-roam-format-template 720 template 721 (lambda (key default-val) 722 (let ((fn (intern key)) 723 (node-fn (intern (concat "org-roam-node-" key))) 724 (ksym (intern (concat ":" key)))) 725 (cond 726 ((fboundp fn) 727 (funcall fn org-roam-capture--node)) 728 ((fboundp node-fn) 729 (funcall node-fn org-roam-capture--node)) 730 ((plist-get org-roam-capture--info ksym) 731 (plist-get org-roam-capture--info ksym)) 732 (t (let ((r (completing-read (format "%s: " key) nil nil nil default-val))) 733 (plist-put org-roam-capture--info ksym r) 734 r)))))))) 735 736 (defun org-roam-capture--convert-template (template &optional props) 737 "Convert TEMPLATE from Org-roam syntax to `org-capture-templates' syntax. 738 PROPS is a plist containing additional Org-roam specific 739 properties to be added to the template." 740 (pcase template 741 (`(,_key ,_desc) 742 template) 743 ((or `(,key ,desc ,type ignore ,body . ,rest) 744 `(,key ,desc ,type (function ignore) ,body . ,rest) 745 `(,key ,desc ,type ,body . ,rest)) 746 (setq rest (append rest props)) 747 (let (org-roam-plist options) 748 (while rest 749 (let* ((key (pop rest)) 750 (val (pop rest)) 751 (custom (member key org-roam-capture--template-keywords))) 752 (when (and custom 753 (not val)) 754 (user-error "Invalid capture template format: %s\nkey %s cannot be nil" template key)) 755 (if custom 756 (setq org-roam-plist (plist-put org-roam-plist key val)) 757 (setq options (plist-put options key val))))) 758 (append `(,key ,desc ,type #'org-roam-capture--prepare-buffer ,body) 759 options 760 (list :org-roam org-roam-plist)))) 761 (_ 762 (signal 'invalid-template template)))) 763 764 765 (provide 'org-roam-capture) 766 767 ;;; org-roam-capture.el ends here