replacing cl concatenate with concat
[org-mode.git] / contrib / lisp / org-choose.el
blob3513fe9475ce10ff1ca8345d597a42aa544fbfef
1 ;;;_ org-choose.el --- decision management for org-mode
3 ;;;_. Headers
4 ;;;_ , License
5 ;; Copyright (C) 2009-2012 Tom Breton (Tehom)
7 ;; This file is not part of GNU Emacs.
9 ;; Author: Tom Breton (Tehom)
10 ;; Keywords: outlines, convenience
12 ;; This file is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; any later version.
17 ;; This file is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs; see the file COPYING. If not, write to
24 ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
25 ;; Boston, MA 02111-1307, USA.
27 ;;;_ , Commentary:
29 ; This is code to support decision management. It lets you treat a
30 ; group of sibling items in org-mode as alternatives in a decision.
32 ; There are no user commands in this file. You use it by:
33 ; * Loading it (manually or by M-x customize-apropos org-modules)
35 ;; * Setting up at least one set of TODO keywords with the
36 ;; interpretation "choose" by either:
38 ;; * Using the file directive #+CHOOSE_TODO:
40 ;; * For instance, "#+CHOOSE_TODO: NO(,-) MAYBE(,0) YES"
42 ;; * Or by M-x customize-apropos org-todo-keywords
44 ;; * Operating on single items with the TODO commands.
46 ;; * Use C-S-right to change the keyword set. Use this to change to
47 ;; the "choose" keyword set that you just defined.
49 ;; * Use S-right to advance the TODO mark to the next setting.
51 ;; For "choose", that means you like this alternative more than
52 ;; before. Other alternatives will be automatically demoted to
53 ;; keep your settings consistent.
55 ;; * Use S-left to demote TODO to the previous setting.
57 ;; For "choose", that means you don't like this alternative as much
58 ;; as before. Other alternatives will be automatically promoted,
59 ;; if this item was all that was keeping them down.
61 ;; * All the other TODO commands are available and behave essentially
62 ;; the normal way.
65 ;;;_ , Requires
67 (require 'org)
68 ;(eval-when-compile
69 ; (require 'cl))
70 (require 'cl)
72 ;;;_. Body
73 ;;;_ , The variables
75 (defstruct (org-choose-mark-data. (:type list))
76 "The format of an entry in org-choose-mark-data.
77 Indexes are 0-based or `nil'.
79 keyword
80 bot-lower-range
81 top-upper-range
82 range-length
83 static-default
84 all-keywords)
86 (defvar org-choose-mark-data
88 "Alist of information for choose marks.
90 Each entry is an `org-choose-mark-data.'" )
91 (make-variable-buffer-local 'org-choose-mark-data)
92 ;;;_ , For setup
93 ;;;_ . org-choose-filter-one
95 (defun org-choose-filter-one (i)
96 "Return a list of
97 * a canonized version of the string
98 * optionally one symbol"
101 (not
102 (string-match "(.*)" i))
103 (list i i)
104 (let*
106 (end-text (match-beginning 0))
107 (vanilla-text (substring i 0 end-text))
108 ;;Get the parenthesized part.
109 (match (match-string 0 i))
110 ;;Remove the parentheses.
111 (args (substring match 1 -1))
112 ;;Split it
113 (arglist
114 (let
115 ((arglist-x (org-split-string args ",")))
116 ;;When string starts with "," `split-string' doesn't
117 ;;make a first arg, so in that case make one
118 ;;manually.
120 (string-match "^," args)
121 (cons nil arglist-x)
122 arglist-x)))
123 (decision-arg (second arglist))
124 (type
125 (cond
126 ((string= decision-arg "0")
127 'default-mark)
128 ((string= decision-arg "+")
129 'top-upper-range)
130 ((string= decision-arg "-")
131 'bot-lower-range)
132 (t nil)))
133 (vanilla-arg (first arglist))
134 (vanilla-mark
135 (if vanilla-arg
136 (concat vanilla-text "("vanilla-arg")")
137 vanilla-text)))
138 (if type
139 (list vanilla-text vanilla-mark type)
140 (list vanilla-text vanilla-mark)))))
142 ;;;_ . org-choose-setup-vars
143 (defun org-choose-setup-vars (bot-lower-range top-upper-range
144 static-default num-items all-mark-texts)
145 "Add to org-choose-mark-data according to arguments"
147 (let*
149 (tail
150 ;;If there's no bot-lower-range or no default, we don't
151 ;;have ranges.
152 (cdr
153 (if (and static-default bot-lower-range)
154 (let*
156 ;;If there's no top-upper-range, use the last
157 ;;item.
158 (top-upper-range
159 (or top-upper-range (1- num-items)))
160 (lower-range-length
161 (1+ (- static-default bot-lower-range)))
162 (upper-range-length
163 (- top-upper-range static-default))
164 (range-length
165 (min upper-range-length lower-range-length)))
168 (make-org-choose-mark-data.
169 :keyword nil
170 :bot-lower-range bot-lower-range
171 :top-upper-range top-upper-range
172 :range-length range-length
173 :static-default static-default
174 :all-keywords all-mark-texts))
176 (make-org-choose-mark-data.
177 :keyword nil
178 :bot-lower-range nil
179 :top-upper-range nil
180 :range-length nil
181 :static-default (or static-default 0)
182 :all-keywords all-mark-texts)))))
184 (dolist (text all-mark-texts)
185 (pushnew (cons text tail)
186 org-choose-mark-data
187 :test
188 #'(lambda (a b)
189 (equal (car a) (car b)))))))
194 ;;;_ . org-choose-filter-tail
195 (defun org-choose-filter-tail (raw)
196 "Return a translation of RAW to vanilla and set appropriate
197 buffer-local variables.
199 RAW is a list of strings representing the input text of a choose
200 interpretation."
201 (let
202 ((vanilla-list nil)
203 (all-mark-texts nil)
204 (index 0)
205 bot-lower-range top-upper-range range-length static-default)
206 (dolist (i raw)
207 (destructuring-bind
208 (vanilla-text vanilla-mark &optional type)
209 (org-choose-filter-one i)
210 (cond
211 ((eq type 'bot-lower-range)
212 (setq bot-lower-range index))
213 ((eq type 'top-upper-range)
214 (setq top-upper-range index))
215 ((eq type 'default-mark)
216 (setq static-default index)))
217 (incf index)
218 (push vanilla-text all-mark-texts)
219 (push vanilla-mark vanilla-list)))
221 (org-choose-setup-vars bot-lower-range top-upper-range
222 static-default index (reverse all-mark-texts))
223 (nreverse vanilla-list)))
225 ;;;_ . org-choose-setup-filter
227 (defun org-choose-setup-filter (raw)
228 "A setup filter for choose interpretations."
229 (when (eq (car raw) 'choose)
230 (cons
231 'choose
232 (org-choose-filter-tail (cdr raw)))))
234 ;;;_ . org-choose-conform-after-promotion
235 (defun org-choose-conform-after-promotion (entry-pos keywords highest-ok-ix)
236 "Conform the current item after another item was promoted"
238 (unless
239 ;;Skip the entry that triggered this by skipping any entry with
240 ;;the same starting position. plist uses the start of the
241 ;;header line as the position, but map no longer does, so we
242 ;;have to go back to the heading.
244 (save-excursion
245 (org-back-to-heading)
246 (point))
247 entry-pos)
248 (let
249 ((ix
250 (org-choose-get-entry-index keywords)))
251 ;;If the index of the entry exceeds the highest allowable
252 ;;index, change it to that.
253 (when (and ix
254 (> ix highest-ok-ix))
255 (org-todo
256 (nth highest-ok-ix keywords))))))
257 ;;;_ . org-choose-conform-after-demotion
258 (defun org-choose-conform-after-demotion (entry-pos keywords
259 raise-to-ix
260 old-highest-ok-ix)
261 "Conform the current item after another item was demoted."
263 (unless
264 ;;Skip the entry that triggered this.
266 (save-excursion
267 (org-back-to-heading)
268 (point))
269 entry-pos)
270 (let
271 ((ix
272 (org-choose-get-entry-index keywords)))
273 ;;If the index of the entry was at or above the old allowable
274 ;;position, change it to the new mirror position if there is
275 ;;one.
276 (when (and
278 raise-to-ix
279 (>= ix old-highest-ok-ix))
280 (org-todo
281 (nth raise-to-ix keywords))))))
283 ;;;_ , org-choose-keep-sensible (the org-trigger-hook function)
284 (defun org-choose-keep-sensible (change-plist)
285 "Bring the other items back into a sensible state after an item's
286 setting was changed."
287 (let*
288 ( (from (plist-get change-plist :from))
289 (to (plist-get change-plist :to))
290 (entry-pos
291 (set-marker
292 (make-marker)
293 (plist-get change-plist :position)))
294 (kwd-data
295 (assoc to org-todo-kwd-alist)))
296 (when
297 (eq (nth 1 kwd-data) 'choose)
298 (let*
300 (data
301 (assoc to org-choose-mark-data))
302 (keywords
303 (org-choose-mark-data.-all-keywords data))
304 (old-index
305 (org-choose-get-index-in-keywords
306 from
307 keywords))
308 (new-index
309 (org-choose-get-index-in-keywords
311 keywords))
312 (highest-ok-ix
313 (org-choose-highest-other-ok
314 new-index
315 data))
316 (funcdata
317 (cond
318 ;;The entry doesn't participate in conformance,
319 ;;so give `nil' which does nothing.
320 ((not highest-ok-ix) nil)
321 ;;The entry was created or promoted
322 ((or
323 (not old-index)
324 (> new-index old-index))
325 (list
326 #'org-choose-conform-after-promotion
327 entry-pos keywords
328 highest-ok-ix))
329 (t ;;Otherwise the entry was demoted.
330 (let
332 (raise-to-ix
333 (min
334 highest-ok-ix
335 (org-choose-mark-data.-static-default
336 data)))
337 (old-highest-ok-ix
338 (org-choose-highest-other-ok
339 old-index
340 data)))
342 (list
343 #'org-choose-conform-after-demotion
344 entry-pos
345 keywords
346 raise-to-ix
347 old-highest-ok-ix))))))
349 (if funcdata
350 ;;The funny-looking names are to make variable capture
351 ;;unlikely. (Poor-man's lexical bindings).
352 (destructuring-bind (func-d473 . args-46k) funcdata
353 (let
354 ((map-over-entries
355 (org-choose-get-fn-map-group))
356 ;;We may call `org-todo', so let various hooks
357 ;;`nil' so we don't cause loops.
358 org-after-todo-state-change-hook
359 org-trigger-hook
360 org-blocker-hook
361 org-todo-get-default-hook
362 ;;Also let this alist `nil' so we don't log
363 ;;secondary transitions.
364 org-todo-log-states)
365 ;;Map over group
366 (funcall map-over-entries
367 #'(lambda ()
368 (apply func-d473 args-46k))))))))
370 ;;Remove the marker
371 (set-marker entry-pos nil)))
375 ;;;_ , Getting the default mark
376 ;;;_ . org-choose-get-index-in-keywords
377 (defun org-choose-get-index-in-keywords (ix all-keywords)
378 "Return the index of the current entry."
380 (if ix
381 (position ix all-keywords
382 :test #'equal)))
384 ;;;_ . org-choose-get-entry-index
385 (defun org-choose-get-entry-index (all-keywords)
386 "Return index of current entry."
388 (let*
389 ((state (org-entry-get (point) "TODO")))
390 (org-choose-get-index-in-keywords state all-keywords)))
392 ;;;_ . org-choose-get-fn-map-group
394 (defun org-choose-get-fn-map-group ()
395 "Return a function to map over the group"
397 #'(lambda (fn)
398 (require 'org-agenda) ;; `org-map-entries' seems to need it.
399 (save-excursion
400 (unless (org-up-heading-safe)
401 (error "Choosing is only supported between siblings in a tree, not on top level"))
402 (let
403 ((level (org-reduced-level (org-outline-level))))
404 (save-restriction
405 (org-map-entries
407 (format "LEVEL=%d" level)
408 'tree))))))
410 ;;;_ . org-choose-get-highest-mark-index
412 (defun org-choose-get-highest-mark-index (keywords)
413 "Get the index of the highest current mark in the group.
414 If there is none, return 0"
416 (let*
418 ;;Func maps over applicable entries.
419 (map-over-entries
420 (org-choose-get-fn-map-group))
422 (indexes-list
423 (remove nil
424 (funcall map-over-entries
425 #'(lambda ()
426 (org-choose-get-entry-index keywords))))))
428 indexes-list
429 (apply #'max indexes-list)
430 0)))
433 ;;;_ . org-choose-highest-ok
435 (defun org-choose-highest-other-ok (ix data)
436 "Return the highest index that any choose mark can sensibly have,
437 given that another mark has index IX.
438 DATA must be a `org-choose-mark-data.'."
440 (let
442 (bot-lower-range
443 (org-choose-mark-data.-bot-lower-range data))
444 (top-upper-range
445 (org-choose-mark-data.-top-upper-range data))
446 (range-length
447 (org-choose-mark-data.-range-length data)))
448 (when (and ix bot-lower-range)
449 (let*
450 ((delta
451 (- top-upper-range ix)))
452 (unless
453 (< range-length delta)
454 (+ bot-lower-range delta))))))
456 ;;;_ . org-choose-get-default-mark-index
458 (defun org-choose-get-default-mark-index (data)
459 "Return the index of the default mark in a choose interpretation.
461 DATA must be a `org-choose-mark-data.'."
465 (let
466 ((highest-mark-index
467 (org-choose-get-highest-mark-index
468 (org-choose-mark-data.-all-keywords data))))
469 (org-choose-highest-other-ok
470 highest-mark-index data))
471 (org-choose-mark-data.-static-default data)))
475 ;;;_ . org-choose-get-mark-N
476 (defun org-choose-get-mark-N (n data)
477 "Get the text of the nth mark in a choose interpretation."
479 (let*
480 ((l (org-choose-mark-data.-all-keywords data)))
481 (nth n l)))
483 ;;;_ . org-choose-get-default-mark
485 (defun org-choose-get-default-mark (new-mark old-mark)
486 "Get the default mark IFF in a choose interpretation.
487 NEW-MARK and OLD-MARK are the text of the new and old marks."
489 (let*
491 (old-kwd-data
492 (assoc old-mark org-todo-kwd-alist))
493 (new-kwd-data
494 (assoc new-mark org-todo-kwd-alist))
495 (becomes-choose
496 (and
498 (not old-kwd-data)
499 (not
500 (eq (nth 1 old-kwd-data) 'choose)))
501 (eq (nth 1 new-kwd-data) 'choose))))
502 (when
503 becomes-choose
504 (let
505 ((new-mark-data
506 (assoc new-mark org-choose-mark-data)))
508 new-mark
509 (org-choose-get-mark-N
510 (org-choose-get-default-mark-index
511 new-mark-data)
512 new-mark-data)
513 (error "Somehow got an unrecognizable mark"))))))
515 ;;;_ , Setting it all up
517 (eval-after-load "org"
518 '(progn
519 (add-to-list 'org-todo-setup-filter-hook
520 #'org-choose-setup-filter)
521 (add-to-list 'org-todo-get-default-hook
522 #'org-choose-get-default-mark)
523 (add-to-list 'org-trigger-hook
524 #'org-choose-keep-sensible)
525 (add-to-list 'org-todo-interpretation-widgets
526 '(:tag "Choose (to record decisions)" choose)
527 'append)
531 ;;;_. Footers
532 ;;;_ , Provides
534 (provide 'org-choose)
536 ;;;_ * Local emacs vars.
537 ;;;_ + Local variables:
538 ;;;_ + End:
540 ;;;_ , End
542 ;;; org-choose.el ends here