org-depend.el 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. ;;; org-depend.el --- TODO dependencies for Org-mode
  2. ;; This file is not part of GNU Emacs.
  3. ;;
  4. ;; This file is free software; you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation; either version 3, or (at your option)
  7. ;; any later version.
  8. ;; GNU Emacs is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;; You should have received a copy of the GNU General Public License
  13. ;; along with GNU Emacs; see the file COPYING. If not, write to the
  14. ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  15. ;; Boston, MA 02110-1301, USA.
  16. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  17. ;;
  18. ;;; Commentary:
  19. ;;
  20. ;; WARNING: This file is just a PROOF OF CONCEPT, not a supported part
  21. ;; of Org-mode.
  22. ;;
  23. ;; This is an example implementation of TODO dependencies in Org-mode.
  24. ;; It uses the new hooks in version 5.13 of Org-mode,
  25. ;; `org-trigger-hook' and `org-blocker-hook'.
  26. ;;
  27. ;; It implements the following:
  28. ;;
  29. ;; Triggering
  30. ;; ----------
  31. ;;
  32. ;; 1) If an entry contains a TRIGGER property that contains the string
  33. ;; "chain-siblings(KEYWORD)", then switching that entry to DONE does
  34. ;; do the following:
  35. ;; - The sibling following this entry switched to todo-state KEYWORD.
  36. ;; - The sibling also gets a TRIGGER property "chain-sibling(KEYWORD)",
  37. ;; property, to make sure that, when *it* is DONE, the chain will
  38. ;; continue.
  39. ;;
  40. ;; 2) If the TRIGGER property contains any other words like
  41. ;; XYZ(KEYWORD), these are treated as entry id's with keywords. That
  42. ;; means, Org-mode will search for an entry with the ID property XYZ
  43. ;; and switch that entry to KEYWORD as well.
  44. ;;
  45. ;; Blocking
  46. ;; --------
  47. ;;
  48. ;; 1) If an entry contains a BLOCKER property that contains the word
  49. ;; "previous-sibling", the sibling above the current entry is
  50. ;; checked when you try to mark it DONE. If it is still in a TODO
  51. ;; state, the current state change is blocked.
  52. ;;
  53. ;; 2) If the BLOCKER property contains any other words, these are
  54. ;; treated as entry id's. That means, Org-mode will search for an
  55. ;; entry with the ID property exactly equal to this word. If any
  56. ;; of these entries is not yet marked DONE, the current state change
  57. ;; will be blocked.
  58. ;;
  59. ;; 3) Whenever a state change is blocked, an org-mark is pushed, so that
  60. ;; you can find the offending entry with `C-c &'.
  61. ;;
  62. ;;; Example:
  63. ;;
  64. ;; When trying this example, make sure that the settings for TODO keywords
  65. ;; have been activated, i.e. include the following line and press C-c C-c
  66. ;; on the line before working with the example:
  67. ;;
  68. ;; #+TYP_TODO: TODO NEXT | DONE
  69. ;;
  70. ;; * TODO Win a million in Las Vegas
  71. ;; The "third" TODO (see above) cannot become a TODO without this money.
  72. ;;
  73. ;; :PROPERTIES:
  74. ;; :ID: I-cannot-do-it-without-money
  75. ;; :END:
  76. ;;
  77. ;; * Do this by doing a chain of TODO's
  78. ;; ** NEXT This is the first in this chain
  79. ;; :PROPERTIES:
  80. ;; :TRIGGER: chain-siblings(NEXT)
  81. ;; :END:
  82. ;;
  83. ;; ** This is the second in this chain
  84. ;;
  85. ;; ** This is the third in this chain
  86. ;; :PROPERTIES:
  87. ;; :BLOCKER: I-cannot-do-it-without-money
  88. ;; :END:
  89. ;;
  90. ;; ** This is the forth in this chain
  91. ;; When this is DONE, we will also trigger entry XYZ-is-my-id
  92. ;; :PROPERTIES:
  93. ;; :TRIGGER: XYZ-is-my-id(TODO)
  94. ;; :END:
  95. ;;
  96. ;; ** This is the fifth in this chain
  97. ;;
  98. ;; * Start writing report
  99. ;; :PROPERTIES:
  100. ;; :ID: XYZ-is-my-id
  101. ;; :END:
  102. ;;
  103. ;;
  104. (require 'org)
  105. (defun org-depend-trigger-todo (change-plist)
  106. "Trigger new TODO entries after the current is switched to DONE.
  107. This does two different kinds of triggers:
  108. - If the current entry contains a TRIGGER property that contains
  109. \"chain-siblings(KEYWORD)\", it goes to the next sibling, marks it
  110. KEYWORD and also installs the \"chain-sibling\" trigger to continue
  111. the chain.
  112. - Any other word (space-separated) like XYZ(KEYWORD) in the TRIGGER
  113. property is seen as an entry id. Org-mode finds the entry with the
  114. corresponding ID property and switches it to the state TODO as well."
  115. ;; Get information from the plist
  116. (let* ((type (plist-get change-plist :type))
  117. (pos (plist-get change-plist :position))
  118. (from (plist-get change-plist :from))
  119. (to (plist-get change-plist :to))
  120. (org-log-done nil) ; IMPROTANT!: no logging during automatic trigger!
  121. trigger triggers tr p1 kwd)
  122. (catch 'return
  123. (unless (eq type 'todo-state-change)
  124. ;; We are only handling todo-state-change....
  125. (throw 'return t))
  126. (unless (and (member from org-not-done-keywords)
  127. (member to org-done-keywords))
  128. ;; This is not a change from TODO to DONE, ignore it
  129. (throw 'return t))
  130. ;; OK, we just switched from a TODO state to a DONE state
  131. ;; Lets see if this entry has a TRIGGER property.
  132. ;; If yes, split it up on whitespace.
  133. (setq trigger (org-entry-get pos "TRIGGER")
  134. triggers (and trigger (org-split-string trigger "[ \t]+")))
  135. ;; Go through all the triggers
  136. (while (setq tr (pop triggers))
  137. (cond
  138. ((string-match "\\`chain-siblings(\\(.*?\\))\\'" tr)
  139. ;; This is a TODO chain of siblings
  140. (setq kwd (match-string 1 tr))
  141. (catch 'exit
  142. (save-excursion
  143. (goto-char pos)
  144. ;; find the sibling, exit if no more siblings
  145. (condition-case nil
  146. (outline-forward-same-level 1)
  147. (error (throw 'exit t)))
  148. ;; mark the sibling TODO
  149. (org-todo kwd)
  150. ;; make sure the sibling will continue the chain
  151. (org-entry-add-to-multivalued-property
  152. nil "TRIGGER" (format "chain-siblings(%s)" kwd)))))
  153. ((string-match "\\`\\(\\S-+\\)(\\(.*?\\))\\'" tr)
  154. ;; This seems to be ENTRY_ID(KEYWORD)
  155. (setq id (match-string 1 tr)
  156. kwd (match-string 2 tr)
  157. p1 (org-find-entry-with-id id))
  158. (when p1
  159. ;; there is an entry with this ID, mark it TODO
  160. (save-excursion
  161. (goto-char p1)
  162. (org-todo kwd)))))))))
  163. (defun org-depend-block-todo (change-plist)
  164. "Block turning an entry into a TODO.
  165. This checks for a BLOCKER property in an entry and checks
  166. all the entries listed there. If any of them is not done,
  167. block changing the current entry into a TODO entry. If the property contains
  168. the word \"previous-sibling\", the sibling above the current entry is checked.
  169. Any other words are treated as entry id's. If an entry exists with the
  170. this ID property, that entry is also checked."
  171. ;; Get information from the plist
  172. (let* ((type (plist-get change-plist :type))
  173. (pos (plist-get change-plist :position))
  174. (from (plist-get change-plist :from))
  175. (to (plist-get change-plist :to))
  176. (org-log-done nil) ; IMPROTANT!: no logging during automatic trigger
  177. blocker blockers bl p1)
  178. (catch 'return
  179. (unless (eq type 'todo-state-change)
  180. ;; We are not handling this kind of change
  181. (throw 'return t))
  182. (unless (and (not from) (member to org-not-done-keywords))
  183. ;; This is not a change from nothing to TODO, ignore it
  184. (throw 'return t))
  185. ;; OK, the plan is to switch from nothing to TODO
  186. ;; Lets see if we will allow it. Find the BLOCKER property
  187. ;; and split it on whitespace.
  188. (setq blocker (org-entry-get pos "BLOCKER")
  189. blockers (and blocker (org-split-string blocker "[ \t]+")))
  190. ;; go through all the blockers
  191. (while (setq bl (pop blockers))
  192. (cond
  193. ((equal bl "previous-sibling")
  194. ;; the sibling is required to be DONE.
  195. (catch 'ignore
  196. (save-excursion
  197. (goto-char pos)
  198. ;; find the older sibling, exit if no more siblings
  199. (condition-case nil
  200. (outline-backward-same-level 1)
  201. (error (throw 'ignore t)))
  202. ;; Check if this entry is not yet done and block
  203. (unless (org-entry-is-done-p)
  204. ;; return nil, to indicate that we block the change!
  205. (org-mark-ring-push)
  206. (throw 'return nil)))))
  207. ((setq p1 (org-find-entry-with-id bl))
  208. ;; there is an entry with this ID, check it out
  209. (save-excursion
  210. (goto-char p1)
  211. (unless (org-entry-is-done-p)
  212. ;; return nil, to indicate that we block the change!
  213. (org-mark-ring-push)
  214. (throw 'return nil))))))
  215. t ; return t to indicate that we are not blocking
  216. )))
  217. (add-hook 'org-trigger-hook 'org-depend-trigger-todo)
  218. (add-hook 'org-blocker-hook 'org-depend-block-todo)
  219. ;;; org-depend.el ends here