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