org-depend.el 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. ;;; org-depend.el --- TODO dependencies for Org-mode
  2. ;; Copyright (C) 2008-2021 Free Software Foundation, Inc.
  3. ;;
  4. ;; Author: Carsten Dominik <carsten at orgmode dot org>
  5. ;; Keywords: outlines, hypermedia, calendar, wp
  6. ;; Homepage: https://orgmode.org
  7. ;; Version: 0.08
  8. ;;
  9. ;; This file is not part of GNU Emacs.
  10. ;;
  11. ;; This file is free software; you can redistribute it and/or modify
  12. ;; it under the terms of the GNU General Public License as published by
  13. ;; the Free Software Foundation; either version 3, or (at your option)
  14. ;; any later version.
  15. ;; This program is distributed in the hope that it will be useful,
  16. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;; GNU General Public License for more details.
  19. ;; You should have received a copy of the GNU General Public License
  20. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  21. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  22. ;;
  23. ;;; Commentary:
  24. ;;
  25. ;; WARNING: This file is just a PROOF OF CONCEPT, not a supported part
  26. ;; of Org-mode.
  27. ;;
  28. ;; This is an example implementation of TODO dependencies in Org-mode.
  29. ;; It uses the new hooks in version 5.13 of Org-mode,
  30. ;; `org-trigger-hook' and `org-blocker-hook'.
  31. ;;
  32. ;; It implements the following:
  33. ;;
  34. ;; Triggering
  35. ;; ----------
  36. ;;
  37. ;; 1) If an entry contains a TRIGGER property that contains the string
  38. ;; "chain-siblings(KEYWORD)", then switching that entry to DONE does
  39. ;; do the following:
  40. ;; - The sibling following this entry switched to todo-state KEYWORD.
  41. ;; - The sibling also gets a TRIGGER property "chain-sibling(KEYWORD)",
  42. ;; property, to make sure that, when *it* is DONE, the chain will
  43. ;; continue.
  44. ;;
  45. ;; 2) If an entry contains a TRIGGER property that contains the string
  46. ;; "chain-siblings-scheduled", then switching that entry to DONE does
  47. ;; the following actions, similarly to "chain-siblings(KEYWORD)":
  48. ;; - The sibling receives the same scheduled time as the entry
  49. ;; marked as DONE (or, in the case, in which there is no scheduled
  50. ;; time, the sibling does not get any either).
  51. ;; - The sibling also gets the same TRIGGER property
  52. ;; "chain-siblings-scheduled", so the chain can continue.
  53. ;;
  54. ;; 3) If the TRIGGER property contains the string
  55. ;; "chain-find-next(KEYWORD[,OPTIONS])", then switching that entry
  56. ;; to DONE do the following:
  57. ;; - All siblings are of the entry are collected into a temporary
  58. ;; list and then filtered and sorted according to OPTIONS
  59. ;; - The first sibling on the list is changed into KEYWORD state
  60. ;; - The sibling also gets the same TRIGGER property
  61. ;; "chain-find-next", so the chain can continue.
  62. ;;
  63. ;; OPTIONS should be a comma separated string without spaces, and
  64. ;; can contain following options:
  65. ;;
  66. ;; - from-top the candidate list is all of the siblings in
  67. ;; the current subtree
  68. ;;
  69. ;; - from-bottom candidate list are all siblings from bottom up
  70. ;;
  71. ;; - from-current candidate list are all siblings from current item
  72. ;; until end of subtree, then wrapped around from
  73. ;; first sibling
  74. ;;
  75. ;; - no-wrap candidate list are siblings from current one down
  76. ;;
  77. ;; - todo-only Only consider siblings that have a todo keyword
  78. ;; -
  79. ;; - todo-and-done-only
  80. ;; Same as above but also include done items.
  81. ;;
  82. ;; - priority-up sort by highest priority
  83. ;; - priority-down sort by lowest priority
  84. ;; - effort-up sort by highest effort
  85. ;; - effort-down sort by lowest effort
  86. ;;
  87. ;; Default OPTIONS are from-top
  88. ;;
  89. ;;
  90. ;; 4) If the TRIGGER property contains any other words like
  91. ;; XYZ(KEYWORD), these are treated as entry id's with keywords. That
  92. ;; means Org-mode will search for an entry with the ID property XYZ
  93. ;; and switch that entry to KEYWORD as well.
  94. ;;
  95. ;; Blocking
  96. ;; --------
  97. ;;
  98. ;; 1) If an entry contains a BLOCKER property that contains the word
  99. ;; "previous-sibling", the sibling above the current entry is
  100. ;; checked when you try to mark it DONE. If it is still in a TODO
  101. ;; state, the current state change is blocked.
  102. ;;
  103. ;; 2) If the BLOCKER property contains any other words, these are
  104. ;; treated as entry id's. That means Org-mode will search for an
  105. ;; entry with the ID property exactly equal to this word. If any
  106. ;; of these entries is not yet marked DONE, the current state change
  107. ;; will be blocked.
  108. ;;
  109. ;; 3) Whenever a state change is blocked, an org-mark is pushed, so that
  110. ;; you can find the offending entry with `C-c &'.
  111. ;;
  112. ;;; Example:
  113. ;;
  114. ;; When trying this example, make sure that the settings for TODO keywords
  115. ;; have been activated, i.e. include the following line and press C-c C-c
  116. ;; on the line before working with the example:
  117. ;;
  118. ;; #+TYP_TODO: TODO NEXT | DONE
  119. ;;
  120. ;; * TODO Win a million in Las Vegas
  121. ;; The "third" TODO (see above) cannot become a TODO without this money.
  122. ;;
  123. ;; :PROPERTIES:
  124. ;; :ID: I-cannot-do-it-without-money
  125. ;; :END:
  126. ;;
  127. ;; * Do this by doing a chain of TODO's
  128. ;; ** NEXT This is the first in this chain
  129. ;; :PROPERTIES:
  130. ;; :TRIGGER: chain-siblings(NEXT)
  131. ;; :END:
  132. ;;
  133. ;; ** This is the second in this chain
  134. ;;
  135. ;; ** This is the third in this chain
  136. ;; :PROPERTIES:
  137. ;; :BLOCKER: I-cannot-do-it-without-money
  138. ;; :END:
  139. ;;
  140. ;; ** This is the forth in this chain
  141. ;; When this is DONE, we will also trigger entry XYZ-is-my-id
  142. ;; :PROPERTIES:
  143. ;; :TRIGGER: XYZ-is-my-id(TODO)
  144. ;; :END:
  145. ;;
  146. ;; ** This is the fifth in this chain
  147. ;;
  148. ;; * Start writing report
  149. ;; :PROPERTIES:
  150. ;; :ID: XYZ-is-my-id
  151. ;; :END:
  152. ;;
  153. ;;
  154. (require 'org)
  155. (eval-when-compile
  156. (require 'cl))
  157. (defcustom org-depend-tag-blocked t
  158. "Whether to indicate blocked TODO items by a special tag."
  159. :group 'org
  160. :type 'boolean)
  161. (defcustom org-depend-find-next-options
  162. "from-current,todo-only,priority-up"
  163. "Default options for chain-find-next trigger"
  164. :group 'org
  165. :type 'string)
  166. (defmacro org-depend-act-on-sibling (trigger-val &rest rest)
  167. "Perform a set of actions on the next sibling, if it exists,
  168. copying the sibling spec TRIGGER-VAL to the next sibling."
  169. `(catch 'exit
  170. (save-excursion
  171. (goto-char pos)
  172. ;; find the sibling, exit if no more siblings
  173. (condition-case nil
  174. (outline-forward-same-level 1)
  175. (error (throw 'exit t)))
  176. ;; mark the sibling TODO
  177. ,@rest
  178. ;; make sure the sibling will continue the chain
  179. (org-entry-add-to-multivalued-property
  180. nil "TRIGGER" ,trigger-val))))
  181. (defvar org-depend-doing-chain-find-next nil)
  182. (defun org-depend-trigger-todo (change-plist)
  183. "Trigger new TODO entries after the current is switched to DONE.
  184. This does two different kinds of triggers:
  185. - If the current entry contains a TRIGGER property that contains
  186. \"chain-siblings(KEYWORD)\", it goes to the next sibling, marks it
  187. KEYWORD and also installs the \"chain-sibling\" trigger to continue
  188. the chain.
  189. - If the current entry contains a TRIGGER property that contains
  190. \"chain-siblings-scheduled\", we go to the next sibling and copy
  191. the scheduled time from the current task, also installing the property
  192. in the sibling.
  193. - Any other word (space-separated) like XYZ(KEYWORD) in the TRIGGER
  194. property is seen as an entry id. Org-mode finds the entry with the
  195. corresponding ID property and switches it to the state TODO as well."
  196. ;; Refresh the effort text properties
  197. (org-refresh-properties org-effort-property 'org-effort)
  198. ;; Get information from the plist
  199. (let* ((type (plist-get change-plist :type))
  200. (pos (plist-get change-plist :position))
  201. (from (plist-get change-plist :from))
  202. (to (plist-get change-plist :to))
  203. (org-log-done nil) ; IMPORTANT!: no logging during automatic trigger!
  204. trigger triggers tr p1 p2 kwd id)
  205. (catch 'return
  206. (unless (eq type 'todo-state-change)
  207. ;; We are only handling todo-state-change....
  208. (throw 'return t))
  209. (unless (and (member from org-not-done-keywords)
  210. (member to org-done-keywords))
  211. ;; This is not a change from TODO to DONE, ignore it
  212. (throw 'return t))
  213. ;; OK, we just switched from a TODO state to a DONE state
  214. ;; Lets see if this entry has a TRIGGER property.
  215. ;; If yes, split it up on whitespace.
  216. (setq trigger (org-entry-get pos "TRIGGER")
  217. triggers (and trigger (split-string trigger)))
  218. ;; Go through all the triggers
  219. (while (setq tr (pop triggers))
  220. (cond
  221. ((and (not org-depend-doing-chain-find-next)
  222. (string-match "\\`chain-find-next(\\b\\(.+?\\)\\b\\(.*\\))\\'" tr))
  223. ;; smarter sibling selection
  224. (let* ((org-depend-doing-chain-find-next t)
  225. (kwd (match-string 1 tr))
  226. (options (match-string 2 tr))
  227. (options (if (or (null options)
  228. (equal options ""))
  229. org-depend-find-next-options
  230. options))
  231. (todo-only (string-match "todo-only" options))
  232. (todo-and-done-only (string-match "todo-and-done-only"
  233. options))
  234. (from-top (string-match "from-top" options))
  235. (from-bottom (string-match "from-bottom" options))
  236. (from-current (string-match "from-current" options))
  237. (no-wrap (string-match "no-wrap" options))
  238. (priority-up (string-match "priority-up" options))
  239. (priority-down (string-match "priority-down" options))
  240. (effort-up (string-match "effort-up" options))
  241. (effort-down (string-match "effort-down" options)))
  242. (save-excursion
  243. (org-back-to-heading t)
  244. (let ((this-item (point)))
  245. ;; go up to the parent headline, then advance to next child
  246. (org-up-heading-safe)
  247. (let ((end (save-excursion (org-end-of-subtree t)
  248. (point)))
  249. (done nil)
  250. (items '()))
  251. (outline-next-heading)
  252. (while (not done)
  253. (if (not (looking-at org-complex-heading-regexp))
  254. (setq done t)
  255. (let ((todo-kwd (match-string 2))
  256. (tags (match-string 5))
  257. (priority (org-get-priority (or (match-string 3) "")))
  258. (effort (when (or effort-up effort-down)
  259. (let ((effort (get-text-property (point) 'org-effort)))
  260. (when effort
  261. (org-duration-to-minutes effort))))))
  262. (push (list (point) todo-kwd priority tags effort)
  263. items))
  264. (unless (org-goto-sibling)
  265. (setq done t))))
  266. ;; massage the list according to options
  267. (setq items
  268. (cond (from-top (nreverse items))
  269. (from-bottom items)
  270. ((or from-current no-wrap)
  271. (let* ((items (nreverse items))
  272. (pos (position this-item items :key #'first))
  273. (items-before (subseq items 0 pos))
  274. (items-after (subseq items pos)))
  275. (if no-wrap items-after
  276. (append items-after items-before))))
  277. (t (nreverse items))))
  278. (setq items (remove-if
  279. (lambda (item)
  280. (or (equal (first item) this-item)
  281. (and (not todo-and-done-only)
  282. (member (second item) org-done-keywords))
  283. (and (or todo-only
  284. todo-and-done-only)
  285. (null (second item)))))
  286. items))
  287. (setq items
  288. (sort
  289. items
  290. (lambda (item1 item2)
  291. (let* ((p1 (third item1))
  292. (p2 (third item2))
  293. (e1 (fifth item1))
  294. (e2 (fifth item2))
  295. (p1-lt (< p1 p2))
  296. (p1-gt (> p1 p2))
  297. (e1-lt (and e1 (or (not e2) (< e1 e2))))
  298. (e2-gt (and e2 (or (not e1) (> e1 e2)))))
  299. (cond (priority-up
  300. (or p1-gt
  301. (and (equal p1 p2)
  302. (or (and effort-up e1-lt)
  303. (and effort-down e2-gt)))))
  304. (priority-down
  305. (or p1-lt
  306. (and (equal p1 p2)
  307. (or (and effort-up e1-lt)
  308. (and effort-down e2-gt)))))
  309. (effort-up
  310. (or e2-gt (and (equal e1 e2) p1-gt)))
  311. (effort-down
  312. (or e1-lt (and (equal e1 e2) p1-gt))))))))
  313. (when items
  314. (goto-char (first (first items)))
  315. (org-entry-add-to-multivalued-property nil "TRIGGER" tr)
  316. (org-todo kwd)))))))
  317. ((string-match "\\`chain-siblings(\\(.*?\\))\\'" tr)
  318. ;; This is a TODO chain of siblings
  319. (setq kwd (match-string 1 tr))
  320. (org-depend-act-on-sibling (format "chain-siblings(%s)" kwd)
  321. (org-todo kwd)))
  322. ((string-match "\\`\\(\\S-+\\)(\\(.*?\\))\\'" tr)
  323. ;; This seems to be ENTRY_ID(KEYWORD)
  324. (setq id (match-string 1 tr)
  325. kwd (match-string 2 tr)
  326. p1 (org-find-entry-with-id id))
  327. ;; First check current buffer, then all files.
  328. (if p1
  329. ;; There is an entry with this ID, mark it TODO.
  330. (save-excursion
  331. (goto-char p1)
  332. (org-todo kwd))
  333. (when (setq p2 (org-id-find id))
  334. (save-excursion
  335. (with-current-buffer (find-file-noselect (car p2))
  336. (goto-char (cdr p2))
  337. (org-todo kwd))))))
  338. ((string-match "\\`chain-siblings-scheduled\\'" tr)
  339. (let ((time (org-get-scheduled-time pos)))
  340. (when time
  341. (org-depend-act-on-sibling
  342. "chain-siblings-scheduled"
  343. (org-schedule nil time))))))))))
  344. (defun org-depend-block-todo (change-plist)
  345. "Block turning an entry into a TODO.
  346. This checks for a BLOCKER property in an entry and checks
  347. all the entries listed there. If any of them is not done,
  348. block changing the current entry into a TODO entry. If the property contains
  349. the word \"previous-sibling\", the sibling above the current entry is checked.
  350. Any other words are treated as entry id's. If an entry exists with the
  351. this ID property, that entry is also checked."
  352. ;; Get information from the plist
  353. (let* ((type (plist-get change-plist :type))
  354. (pos (plist-get change-plist :position))
  355. (from (plist-get change-plist :from))
  356. (to (plist-get change-plist :to))
  357. (org-log-done nil) ; IMPORTANT!: no logging during automatic trigger
  358. blocker blockers bl p1 p2
  359. (proceed-p
  360. (catch 'return
  361. ;; If this is not a todo state change, or if this entry is
  362. ;; DONE, do not block
  363. (when (or (not (eq type 'todo-state-change))
  364. (member from (cons 'done org-done-keywords))
  365. (member to (cons 'todo org-not-done-keywords))
  366. (not to))
  367. (throw 'return t))
  368. ;; OK, the plan is to switch from nothing to TODO
  369. ;; Lets see if we will allow it. Find the BLOCKER property
  370. ;; and split it on whitespace.
  371. (setq blocker (org-entry-get pos "BLOCKER")
  372. blockers (and blocker (split-string blocker)))
  373. ;; go through all the blockers
  374. (while (setq bl (pop blockers))
  375. (cond
  376. ((equal bl "previous-sibling")
  377. ;; the sibling is required to be DONE.
  378. (catch 'ignore
  379. (save-excursion
  380. (goto-char pos)
  381. ;; find the older sibling, exit if no more siblings
  382. (unless (org-get-last-sibling)
  383. (throw 'ignore t))
  384. ;; Check if this entry is not yet done and block
  385. (unless (org-entry-is-done-p)
  386. ;; return nil, to indicate that we block the change!
  387. (org-mark-ring-push)
  388. (throw 'return nil)))))
  389. ((setq p1 (org-find-entry-with-id bl))
  390. ;; there is an entry with this ID, check it out
  391. (save-excursion
  392. (goto-char p1)
  393. (unless (org-entry-is-done-p)
  394. ;; return nil, to indicate that we block the change!
  395. (org-mark-ring-push)
  396. (throw 'return nil))))
  397. ((setq p2 (org-id-find bl))
  398. (save-excursion
  399. (with-current-buffer (find-file-noselect (car p2))
  400. (goto-char (cdr p2))
  401. (unless (org-entry-is-done-p)
  402. (org-mark-ring-push)
  403. (throw 'return nil)))))))
  404. ;; Return t to indicate that we are not blocking.
  405. t)))
  406. (when org-depend-tag-blocked
  407. (org-toggle-tag "blocked" (if proceed-p 'off 'on)))
  408. proceed-p))
  409. (add-hook 'org-trigger-hook 'org-depend-trigger-todo)
  410. (add-hook 'org-blocker-hook 'org-depend-block-todo)
  411. (provide 'org-depend)
  412. ;;; org-depend.el ends here