org-drill.el: fix a few wrong :type spec.
[org-mode.git] / contrib / lisp / org-drill.el
blobb0223597ab93412ff97a6f5f3e13b2958e480bca
1 ;; -*- coding: utf-8-unix -*-
2 ;; org-drill.el - Self-testing using spaced repetition
3 ;;
4 ;; Author: Paul Sexton <eeeickythump@gmail.com>
5 ;; Version: 2.3.5
6 ;; Repository at http://bitbucket.org/eeeickythump/org-drill/
7 ;;
8 ;; This file is not part of GNU Emacs.
9 ;;
10 ;; Synopsis
11 ;; ========
13 ;; Uses the SuperMemo spaced repetition algorithms to conduct interactive
14 ;; "drill sessions", where the material to be remembered is presented to the
15 ;; student in random order. The student rates his or her recall of each item,
16 ;; and this information is used to schedule the item for later revision.
18 ;; Each drill session can be restricted to topics in the current buffer
19 ;; (default), one or several files, all agenda files, or a subtree. A single
20 ;; topic can also be drilled.
22 ;; Different "card types" can be defined, which present their information to
23 ;; the student in different ways.
25 ;; See the file README.org in the repository for more detailed documentation.
27 (eval-when-compile (require 'cl))
28 (eval-when-compile (require 'hi-lock))
29 (require 'org)
30 (require 'org-id)
31 (require 'org-learn)
34 (defgroup org-drill nil
35 "Options concerning interactive drill sessions in Org mode (org-drill)."
36 :tag "Org-Drill"
37 :group 'org-link)
40 (defcustom org-drill-question-tag
41 "drill"
42 "Tag which topics must possess in order to be identified as review topics
43 by `org-drill'."
44 :group 'org-drill
45 :type 'string)
48 (defcustom org-drill-maximum-items-per-session
50 "Each drill session will present at most this many topics for review.
51 Nil means unlimited."
52 :group 'org-drill
53 :type '(choice integer (const nil)))
56 (defcustom org-drill-maximum-duration
58 "Maximum duration of a drill session, in minutes.
59 Nil means unlimited."
60 :group 'org-drill
61 :type '(choice integer (const nil)))
64 (defcustom org-drill-failure-quality
66 "If the quality of recall for an item is this number or lower,
67 it is regarded as an unambiguous failure, and the repetition
68 interval for the card is reset to 0 days. If the quality is higher
69 than this number, it is regarded as successfully recalled, but the
70 time interval to the next repetition will be lowered if the quality
71 was near to a fail.
73 By default this is 2, for SuperMemo-like behaviour. For
74 Mnemosyne-like behaviour, set it to 1. Other values are not
75 really sensible."
76 :group 'org-drill
77 :type '(choice (const 2) (const 1)))
80 (defcustom org-drill-forgetting-index
82 "What percentage of items do you consider it is 'acceptable' to
83 forget each drill session? The default is 10%. A warning message
84 is displayed at the end of the session if the percentage forgotten
85 climbs above this number."
86 :group 'org-drill
87 :type 'integer)
90 (defcustom org-drill-leech-failure-threshold
92 "If an item is forgotten more than this many times, it is tagged
93 as a 'leech' item."
94 :group 'org-drill
95 :type '(choice integer (const nil)))
98 (defcustom org-drill-leech-method
99 'skip
100 "How should 'leech items' be handled during drill sessions?
101 Possible values:
102 - nil :: Leech items are treated the same as normal items.
103 - skip :: Leech items are not included in drill sessions.
104 - warn :: Leech items are still included in drill sessions,
105 but a warning message is printed when each leech item is
106 presented."
107 :group 'org-drill
108 :type '(choice (const warn) (const skip) (const nil)))
111 (defface org-drill-visible-cloze-face
112 '((t (:foreground "darkseagreen")))
113 "The face used to hide the contents of cloze phrases."
114 :group 'org-drill)
117 (defface org-drill-visible-cloze-hint-face
118 '((t (:foreground "dark slate blue")))
119 "The face used to hide the contents of cloze phrases."
120 :group 'org-drill)
123 (defface org-drill-hidden-cloze-face
124 '((t (:foreground "deep sky blue" :background "blue")))
125 "The face used to hide the contents of cloze phrases."
126 :group 'org-drill)
129 (defcustom org-drill-use-visible-cloze-face-p
131 "Use a special face to highlight cloze-deleted text in org mode
132 buffers?"
133 :group 'org-drill
134 :type 'boolean)
137 (defcustom org-drill-hide-item-headings-p
139 "Conceal the contents of the main heading of each item during drill
140 sessions? You may want to enable this behaviour if item headings or tags
141 contain information that could 'give away' the answer."
142 :group 'org-drill
143 :type 'boolean)
146 (defcustom org-drill-new-count-color
147 "royal blue"
148 "Foreground colour used to display the count of remaining new items
149 during a drill session."
150 :group 'org-drill
151 :type 'color)
153 (defcustom org-drill-mature-count-color
154 "green"
155 "Foreground colour used to display the count of remaining mature items
156 during a drill session. Mature items are due for review, but are not new."
157 :group 'org-drill
158 :type 'color)
160 (defcustom org-drill-failed-count-color
161 "red"
162 "Foreground colour used to display the count of remaining failed items
163 during a drill session."
164 :group 'org-drill
165 :type 'color)
167 (defcustom org-drill-done-count-color
168 "sienna"
169 "Foreground colour used to display the count of reviewed items
170 during a drill session."
171 :group 'org-drill
172 :type 'color)
175 (setplist 'org-drill-cloze-overlay-defaults
176 '(display "[...]"
177 face org-drill-hidden-cloze-face
178 window t))
180 (setplist 'org-drill-hidden-text-overlay
181 '(invisible t))
183 (setplist 'org-drill-replaced-text-overlay
184 '(display "Replaced text"
185 face default
186 window t))
189 (defvar org-drill-cloze-regexp
190 ;; ver 1 "[^][]\\(\\[[^][][^]]*\\]\\)"
191 ;; ver 2 "\\(\\[.*?\\]\\|^[^[[:cntrl:]]*?\\]\\|\\[.*?$\\)"
192 ;; ver 3! "\\(\\[.*?\\]\\|\\[.*?[[:cntrl:]]+.*?\\]\\)"
193 "\\(\\[[[:cntrl:][:graph:][:space:]]*?\\)\\(\\||.+?\\)\\(\\]\\)")
196 (defvar org-drill-cloze-keywords
197 `((,org-drill-cloze-regexp
198 (1 'org-drill-visible-cloze-face nil)
199 (2 'org-drill-visible-cloze-hint-face t)
200 (3 'org-drill-visible-cloze-face nil)
204 (defcustom org-drill-card-type-alist
205 '((nil . org-drill-present-simple-card)
206 ("simple" . org-drill-present-simple-card)
207 ("twosided" . org-drill-present-two-sided-card)
208 ("multisided" . org-drill-present-multi-sided-card)
209 ("hide1cloze" . org-drill-present-multicloze-hide1)
210 ("hide2cloze" . org-drill-present-multicloze-hide2)
211 ("show1cloze" . org-drill-present-multicloze-show1)
212 ("show2cloze" . org-drill-present-multicloze-show2)
213 ("multicloze" . org-drill-present-multicloze-hide1)
214 ("hidefirst" . org-drill-present-multicloze-hide-first)
215 ("hidelast" . org-drill-present-multicloze-hide-last)
216 ("hide1_firstmore" . org-drill-present-multicloze-hide1-firstmore)
217 ("show1_lastmore" . org-drill-present-multicloze-show1-lastmore)
218 ("show1_firstless" . org-drill-present-multicloze-show1-firstless)
219 ("conjugate" org-drill-present-verb-conjugation
220 org-drill-show-answer-verb-conjugation)
221 ("spanish_verb" . org-drill-present-spanish-verb)
222 ("translate_number" org-drill-present-translate-number
223 org-drill-show-answer-translate-number))
224 "Alist associating card types with presentation functions. Each entry in the
225 alist takes one of two forms:
226 1. (CARDTYPE . QUESTION-FN), where CARDTYPE is a string or nil (for default),
227 and QUESTION-FN is a function which takes no arguments and returns a boolean
228 value.
229 2. (CARDTYPE QUESTION-FN ANSWER-FN), where ANSWER-FN is a function that takes
230 one argument -- the argument is a function that itself takes no arguments.
231 ANSWER-FN is called with the point on the active item's
232 heading, just prior to displaying the item's 'answer'. It can therefore be
233 used to modify the appearance of the answer. ANSWER-FN must call its argument
234 before returning. (Its argument is a function that prompts the user and
235 performs rescheduling)."
236 :group 'org-drill
237 :type '(alist :key-type (choice string (const nil)) :value-type function))
240 (defcustom org-drill-scope
241 'file
242 "The scope in which to search for drill items when conducting a
243 drill session. This can be any of:
245 file The current buffer, respecting the restriction if any.
246 This is the default.
247 tree The subtree started with the entry at point
248 file-no-restriction The current buffer, without restriction
249 file-with-archives The current buffer, and any archives associated with it.
250 agenda All agenda files
251 agenda-with-archives All agenda files with any archive files associated
252 with them.
253 directory All files with the extension '.org' in the same
254 directory as the current file (includes the current
255 file if it is an .org file.)
256 (FILE1 FILE2 ...) If this is a list, all files in the list will be scanned.
258 ;; Note -- meanings differ slightly from the argument to org-map-entries:
259 ;; 'file' means current file/buffer, respecting any restriction
260 ;; 'file-no-restriction' means current file/buffer, ignoring restrictions
261 ;; 'directory' means all *.org files in current directory
262 :group 'org-drill
263 :type '(choice (const file) (const tree) (const file-no-restriction)
264 (const file-with-archives) (const agenda)
265 (const agenda-with-archives) (const directory)
266 list))
269 (defcustom org-drill-save-buffers-after-drill-sessions-p
271 "If non-nil, prompt to save all modified buffers after a drill session
272 finishes."
273 :group 'org-drill
274 :type 'boolean)
277 (defcustom org-drill-spaced-repetition-algorithm
278 'sm5
279 "Which SuperMemo spaced repetition algorithm to use for scheduling items.
280 Available choices are:
281 - SM2 :: the SM2 algorithm, used in SuperMemo 2.0
282 - SM5 :: the SM5 algorithm, used in SuperMemo 5.0
283 - Simple8 :: a modified version of the SM8 algorithm. SM8 is used in
284 SuperMemo 98. The version implemented here is simplified in that while it
285 'learns' the difficulty of each item using quality grades and number of
286 failures, it does not modify the matrix of values that
287 governs how fast the inter-repetition intervals increase. A method for
288 adjusting intervals when items are reviewed early or late has been taken
289 from SM11, a later version of the algorithm, and included in Simple8."
290 :group 'org-drill
291 :type '(choice (const sm2) (const sm5) (const simple8)))
294 (defcustom org-drill-optimal-factor-matrix
296 "DO NOT CHANGE THE VALUE OF THIS VARIABLE.
298 Persistent matrix of optimal factors, used by the SuperMemo SM5 algorithm.
299 The matrix is saved (using the 'customize' facility) at the end of each
300 drill session.
302 Over time, values in the matrix will adapt to the individual user's
303 pace of learning."
304 :group 'org-drill
305 :type 'sexp)
308 (defcustom org-drill-sm5-initial-interval
310 "In the SM5 algorithm, the initial interval after the first
311 successful presentation of an item is always 4 days. If you wish to change
312 this, you can do so here."
313 :group 'org-drill
314 :type 'float)
317 (defcustom org-drill-add-random-noise-to-intervals-p
319 "If true, the number of days until an item's next repetition
320 will vary slightly from the interval calculated by the SM2
321 algorithm. The variation is very small when the interval is
322 small, but scales up with the interval."
323 :group 'org-drill
324 :type 'boolean)
327 (defcustom org-drill-adjust-intervals-for-early-and-late-repetitions-p
329 "If true, when the student successfully reviews an item 1 or more days
330 before or after the scheduled review date, this will affect that date of
331 the item's next scheduled review, according to the algorithm presented at
332 [[http://www.supermemo.com/english/algsm11.htm#Advanced%20repetitions]].
334 Items that were reviewed early will have their next review date brought
335 forward. Those that were reviewed late will have their next review
336 date postponed further.
338 Note that this option currently has no effect if the SM2 algorithm
339 is used."
340 :group 'org-drill
341 :type 'boolean)
344 (defcustom org-drill-cloze-text-weight
346 "For card types 'hide1_firstmore', 'show1_lastmore' and 'show1_firstless',
347 this number determines how often the 'less favoured' situation
348 should arise. It will occur 1 in every N trials, where N is the
349 value of the variable.
351 For example, with the hide1_firstmore card type, the first piece
352 of clozed text should be hidden more often than the other
353 pieces. If this variable is set to 4 (default), the first item
354 will only be shown 25% of the time (1 in 4 trials). Similarly for
355 show1_lastmore, the last item will be shown 75% of the time, and
356 for show1_firstless, the first item would only be shown 25% of the
357 time.
359 If the value of this variable is NIL, then weighting is disabled, and
360 all weighted card types are treated as their unweighted equivalents."
361 :group 'org-drill
362 :type '(choice integer (const nil)))
365 (defcustom org-drill-cram-hours
367 "When in cram mode, items are considered due for review if
368 they were reviewed at least this many hours ago."
369 :group 'org-drill
370 :type 'integer)
373 ;;; NEW items have never been presented in a drill session before.
374 ;;; MATURE items HAVE been presented at least once before.
375 ;;; - YOUNG mature items were scheduled no more than
376 ;;; ORG-DRILL-DAYS-BEFORE-OLD days after their last
377 ;;; repetition. These items will have been learned 'recently' and will have a
378 ;;; low repetition count.
379 ;;; - OLD mature items have intervals greater than
380 ;;; ORG-DRILL-DAYS-BEFORE-OLD.
381 ;;; - OVERDUE items are past their scheduled review date by more than
382 ;;; LAST-INTERVAL * (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) days,
383 ;;; regardless of young/old status.
386 (defcustom org-drill-days-before-old
388 "When an item's inter-repetition interval rises above this value in days,
389 it is no longer considered a 'young' (recently learned) item."
390 :group 'org-drill
391 :type 'integer)
394 (defcustom org-drill-overdue-interval-factor
396 "An item is considered overdue if its scheduled review date is
397 more than (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) * LAST-INTERVAL
398 days in the past. For example, a value of 1.2 means an additional
399 20% of the last scheduled interval is allowed to elapse before
400 the item is overdue. A value of 1.0 means no extra time is
401 allowed at all - items are immediately considered overdue if
402 there is even one day's delay in reviewing them. This variable
403 should never be less than 1.0."
404 :group 'org-drill
405 :type 'float)
408 (defcustom org-drill-learn-fraction
410 "Fraction between 0 and 1 that governs how quickly the spaces
411 between successive repetitions increase, for all items. The
412 default value is 0.5. Higher values make spaces increase more
413 quickly with each successful repetition. You should only change
414 this in small increments (for example 0.05-0.1) as it has an
415 exponential effect on inter-repetition spacing."
416 :group 'org-drill
417 :type 'float)
420 (defvar *org-drill-session-qualities* nil)
421 (defvar *org-drill-start-time* 0)
422 (defvar *org-drill-new-entries* nil)
423 (defvar *org-drill-dormant-entry-count* 0)
424 (defvar *org-drill-due-entry-count* 0)
425 (defvar *org-drill-overdue-entry-count* 0)
426 (defvar *org-drill-due-tomorrow-count* 0)
427 (defvar *org-drill-overdue-entries* nil
428 "List of markers for items that are considered 'overdue', based on
429 the value of ORG-DRILL-OVERDUE-INTERVAL-FACTOR.")
430 (defvar *org-drill-young-mature-entries* nil
431 "List of markers for mature entries whose last inter-repetition
432 interval was <= ORG-DRILL-DAYS-BEFORE-OLD days.")
433 (defvar *org-drill-old-mature-entries* nil
434 "List of markers for mature entries whose last inter-repetition
435 interval was greater than ORG-DRILL-DAYS-BEFORE-OLD days.")
436 (defvar *org-drill-failed-entries* nil)
437 (defvar *org-drill-again-entries* nil)
438 (defvar *org-drill-done-entries* nil)
439 (defvar *org-drill-current-item* nil
440 "Set to the marker for the item currently being tested.")
441 (defvar *org-drill-cram-mode* nil
442 "Are we in 'cram mode', where all items are considered due
443 for review unless they were already reviewed in the recent past?")
444 (defvar org-drill-scheduling-properties
445 '("LEARN_DATA" "DRILL_LAST_INTERVAL" "DRILL_REPEATS_SINCE_FAIL"
446 "DRILL_TOTAL_REPEATS" "DRILL_FAILURE_COUNT" "DRILL_AVERAGE_QUALITY"
447 "DRILL_EASE" "DRILL_LAST_QUALITY" "DRILL_LAST_REVIEWED"))
450 ;;; Make the above settings safe as file-local variables.
453 (put 'org-drill-question-tag 'safe-local-variable 'stringp)
454 (put 'org-drill-maximum-items-per-session 'safe-local-variable
455 '(lambda (val) (or (integerp val) (null val))))
456 (put 'org-drill-maximum-duration 'safe-local-variable
457 '(lambda (val) (or (integerp val) (null val))))
458 (put 'org-drill-failure-quality 'safe-local-variable 'integerp)
459 (put 'org-drill-forgetting-index 'safe-local-variable 'integerp)
460 (put 'org-drill-leech-failure-threshold 'safe-local-variable 'integerp)
461 (put 'org-drill-leech-method 'safe-local-variable
462 '(lambda (val) (memq val '(nil skip warn))))
463 (put 'org-drill-use-visible-cloze-face-p 'safe-local-variable 'booleanp)
464 (put 'org-drill-hide-item-headings-p 'safe-local-variable 'booleanp)
465 (put 'org-drill-spaced-repetition-algorithm 'safe-local-variable
466 '(lambda (val) (memq val '(simple8 sm5 sm2))))
467 (put 'org-drill-sm5-initial-interval 'safe-local-variable 'floatp)
468 (put 'org-drill-add-random-noise-to-intervals-p 'safe-local-variable 'booleanp)
469 (put 'org-drill-adjust-intervals-for-early-and-late-repetitions-p
470 'safe-local-variable 'booleanp)
471 (put 'org-drill-cram-hours 'safe-local-variable 'integerp)
472 (put 'org-drill-learn-fraction 'safe-local-variable 'floatp)
473 (put 'org-drill-days-before-old 'safe-local-variable 'integerp)
474 (put 'org-drill-overdue-interval-factor 'safe-local-variable 'floatp)
475 (put 'org-drill-scope 'safe-local-variable
476 '(lambda (val) (or (symbolp val) (listp val))))
477 (put 'org-drill-save-buffers-after-drill-sessions-p 'safe-local-variable 'booleanp)
478 (put 'org-drill-cloze-text-weight 'safe-local-variable
479 '(lambda (val) (or (null val) (integerp val))))
482 ;;;; Utilities ================================================================
485 (defun free-marker (m)
486 (set-marker m nil))
489 (defmacro pop-random (place)
490 (let ((idx (gensym)))
491 `(if (null ,place)
493 (let ((,idx (random* (length ,place))))
494 (prog1 (nth ,idx ,place)
495 (setq ,place (append (subseq ,place 0 ,idx)
496 (subseq ,place (1+ ,idx)))))))))
499 (defmacro push-end (val place)
500 "Add VAL to the end of the sequence stored in PLACE. Return the new
501 value."
502 `(setq ,place (append ,place (list ,val))))
505 (defun shuffle-list (list)
506 "Randomly permute the elements of LIST (all permutations equally likely)."
507 ;; Adapted from 'shuffle-vector' in cookie1.el
508 (let ((i 0)
510 temp
511 (len (length list)))
512 (while (< i len)
513 (setq j (+ i (random* (- len i))))
514 (setq temp (nth i list))
515 (setf (nth i list) (nth j list))
516 (setf (nth j list) temp)
517 (setq i (1+ i))))
518 list)
521 (defun round-float (floatnum fix)
522 "Round the floating point number FLOATNUM to FIX decimal places.
523 Example: (round-float 3.56755765 3) -> 3.568"
524 (let ((n (expt 10 fix)))
525 (/ (float (round (* floatnum n))) n)))
528 (defun command-keybinding-to-string (cmd)
529 "Return a human-readable description of the key/keys to which the command
530 CMD is bound, or nil if it is not bound to a key."
531 (let ((key (where-is-internal cmd overriding-local-map t)))
532 (if key (key-description key))))
535 (defun time-to-inactive-org-timestamp (time)
536 (format-time-string
537 (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")
538 time))
541 (defun org-map-drill-entries (func &optional scope &rest skip)
542 "Like `org-map-entries', but only drill entries are processed."
543 (let ((org-drill-scope (or scope org-drill-scope)))
544 (apply 'org-map-entries func
545 (concat "+" org-drill-question-tag)
546 (case org-drill-scope
547 (file nil)
548 (file-no-restriction 'file)
549 (directory
550 (directory-files (file-name-directory (buffer-file-name))
551 t "\\.org$"))
552 (t org-drill-scope))
553 skip)))
556 (defmacro with-hidden-cloze-text (&rest body)
557 `(progn
558 (org-drill-hide-clozed-text)
559 (unwind-protect
560 (progn
561 ,@body)
562 (org-drill-unhide-clozed-text))))
565 (defmacro with-hidden-cloze-hints (&rest body)
566 `(progn
567 (org-drill-hide-cloze-hints)
568 (unwind-protect
569 (progn
570 ,@body)
571 (org-drill-unhide-text))))
574 (defmacro with-hidden-comments (&rest body)
575 `(progn
576 (if org-drill-hide-item-headings-p
577 (org-drill-hide-heading-at-point))
578 (org-drill-hide-comments)
579 (unwind-protect
580 (progn
581 ,@body)
582 (org-drill-unhide-text))))
585 (defun org-drill-days-since-last-review ()
586 "Nil means a last review date has not yet been stored for
587 the item.
588 Zero means it was reviewed today.
589 A positive number means it was reviewed that many days ago.
590 A negative number means the date of last review is in the future --
591 this should never happen."
592 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
593 (when datestr
594 (- (time-to-days (current-time))
595 (time-to-days (apply 'encode-time
596 (org-parse-time-string datestr)))))))
599 (defun org-drill-hours-since-last-review ()
600 "Like `org-drill-days-since-last-review', but return value is
601 in hours rather than days."
602 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
603 (when datestr
604 (floor
605 (/ (- (time-to-seconds (current-time))
606 (time-to-seconds (apply 'encode-time
607 (org-parse-time-string datestr))))
608 (* 60 60))))))
611 (defun org-drill-entry-p (&optional marker)
612 "Is MARKER, or the point, in a 'drill item'? This will return nil if
613 the point is inside a subheading of a drill item -- to handle that
614 situation use `org-part-of-drill-entry-p'."
615 (save-excursion
616 (when marker
617 (org-drill-goto-entry marker))
618 (member org-drill-question-tag (org-get-local-tags))))
621 (defun org-drill-goto-entry (marker)
622 (org-pop-to-buffer-same-window (marker-buffer marker))
623 (goto-char marker))
626 (defun org-part-of-drill-entry-p ()
627 "Is the current entry either the main heading of a 'drill item',
628 or a subheading within a drill item?"
629 (or (org-drill-entry-p)
630 ;; Does this heading INHERIT the drill tag
631 (member org-drill-question-tag (org-get-tags-at))))
634 (defun org-drill-goto-drill-entry-heading ()
635 "Move the point to the heading which holds the :drill: tag for this
636 drill entry."
637 (unless (org-at-heading-p)
638 (org-back-to-heading))
639 (unless (org-part-of-drill-entry-p)
640 (error "Point is not inside a drill entry"))
641 (while (not (org-drill-entry-p))
642 (unless (org-up-heading-safe)
643 (error "Cannot find a parent heading that is marked as a drill entry"))))
647 (defun org-drill-entry-leech-p ()
648 "Is the current entry a 'leech item'?"
649 (and (org-drill-entry-p)
650 (member "leech" (org-get-local-tags))))
653 ;; (defun org-drill-entry-due-p ()
654 ;; (cond
655 ;; (*org-drill-cram-mode*
656 ;; (let ((hours (org-drill-hours-since-last-review)))
657 ;; (and (org-drill-entry-p)
658 ;; (or (null hours)
659 ;; (>= hours org-drill-cram-hours)))))
660 ;; (t
661 ;; (let ((item-time (org-get-scheduled-time (point))))
662 ;; (and (org-drill-entry-p)
663 ;; (or (not (eql 'skip org-drill-leech-method))
664 ;; (not (org-drill-entry-leech-p)))
665 ;; (or (null item-time) ; not scheduled
666 ;; (not (minusp ; scheduled for today/in past
667 ;; (- (time-to-days (current-time))
668 ;; (time-to-days item-time))))))))))
671 (defun org-drill-entry-days-overdue ()
672 "Returns:
673 - NIL if the item is not to be regarded as scheduled for review at all.
674 This is the case if it is not a drill item, or if it is a leech item
675 that we wish to skip, or if we are in cram mode and have already reviewed
676 the item within the last few hours.
677 - 0 if the item is new, or if it scheduled for review today.
678 - A negative integer - item is scheduled that many days in the future.
679 - A positive integer - item is scheduled that many days in the past."
680 (cond
681 (*org-drill-cram-mode*
682 (let ((hours (org-drill-hours-since-last-review)))
683 (and (org-drill-entry-p)
684 (or (null hours)
685 (>= hours org-drill-cram-hours))
686 0)))
688 (let ((item-time (org-get-scheduled-time (point))))
689 (cond
690 ((or (not (org-drill-entry-p))
691 (and (eql 'skip org-drill-leech-method)
692 (org-drill-entry-leech-p)))
693 nil)
694 ((null item-time) ; not scheduled -> due now
697 (- (time-to-days (current-time))
698 (time-to-days item-time))))))))
701 (defun org-drill-entry-overdue-p (&optional days-overdue last-interval)
702 "Returns true if entry that is scheduled DAYS-OVERDUE dasy in the past,
703 and whose last inter-repetition interval was LAST-INTERVAL, should be
704 considered 'overdue'. If the arguments are not given they are extracted
705 from the entry at point."
706 (unless days-overdue
707 (setq days-overdue (org-drill-entry-days-overdue)))
708 (unless last-interval
709 (setq last-interval (org-drill-entry-last-interval 1)))
710 (and (numberp days-overdue)
711 (> days-overdue 1) ; enforce a sane minimum 'overdue' gap
712 ;;(> due org-drill-days-before-overdue)
713 (> (/ (+ days-overdue last-interval 1.0) last-interval)
714 org-drill-overdue-interval-factor)))
718 (defun org-drill-entry-due-p ()
719 (let ((due (org-drill-entry-days-overdue)))
720 (and (not (null due))
721 (not (minusp due)))))
724 (defun org-drill-entry-new-p ()
725 (and (org-drill-entry-p)
726 (let ((item-time (org-get-scheduled-time (point))))
727 (null item-time))))
730 (defun org-drill-entry-last-quality (&optional default)
731 (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
732 (if quality
733 (string-to-number quality)
734 default)))
737 (defun org-drill-entry-failure-count ()
738 (let ((quality (org-entry-get (point) "DRILL_FAILURE_COUNT")))
739 (if quality
740 (string-to-number quality)
741 0)))
744 (defun org-drill-entry-average-quality (&optional default)
745 (let ((val (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
746 (if val
747 (string-to-number val)
748 (or default nil))))
750 (defun org-drill-entry-last-interval (&optional default)
751 (let ((val (org-entry-get (point) "DRILL_LAST_INTERVAL")))
752 (if val
753 (string-to-number val)
754 (or default 0))))
756 (defun org-drill-entry-repeats-since-fail (&optional default)
757 (let ((val (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
758 (if val
759 (string-to-number val)
760 (or default 0))))
762 (defun org-drill-entry-total-repeats (&optional default)
763 (let ((val (org-entry-get (point) "DRILL_TOTAL_REPEATS")))
764 (if val
765 (string-to-number val)
766 (or default 0))))
768 (defun org-drill-entry-ease (&optional default)
769 (let ((val (org-entry-get (point) "DRILL_EASE")))
770 (if val
771 (string-to-number val)
772 default)))
775 ;;; From http://www.supermemo.com/english/ol/sm5.htm
776 (defun org-drill-random-dispersal-factor ()
777 "Returns a random number between 0.5 and 1.5."
778 (let ((a 0.047)
779 (b 0.092)
780 (p (- (random* 1.0) 0.5)))
781 (flet ((sign (n)
782 (cond ((zerop n) 0)
783 ((plusp n) 1)
784 (t -1))))
785 (/ (+ 100 (* (* (/ -1 b) (log (- 1 (* (/ b a ) (abs p)))))
786 (sign p)))
787 100.0))))
789 (defun pseudonormal (mean variation)
790 "Random numbers in a pseudo-normal distribution with mean MEAN, range
791 MEAN-VARIATION to MEAN+VARIATION"
792 (+ (random* variation)
793 (random* variation)
794 (- variation)
795 mean))
798 (defun org-drill-early-interval-factor (optimal-factor
799 optimal-interval
800 days-ahead)
801 "Arguments:
802 - OPTIMAL-FACTOR: interval-factor if the item had been tested
803 exactly when it was supposed to be.
804 - OPTIMAL-INTERVAL: interval for next repetition (days) if the item had been
805 tested exactly when it was supposed to be.
806 - DAYS-AHEAD: how many days ahead of time the item was reviewed.
808 Returns an adjusted optimal factor which should be used to
809 calculate the next interval, instead of the optimal factor found
810 in the matrix."
811 (let ((delta-ofmax (* (1- optimal-factor)
812 (/ (+ optimal-interval
813 (* 0.6 optimal-interval) -1) (1- optimal-interval)))))
814 (- optimal-factor
815 (* delta-ofmax (/ days-ahead (+ days-ahead (* 0.6 optimal-interval)))))))
818 (defun org-drill-get-item-data ()
819 "Returns a list of 6 items, containing all the stored recall
820 data for the item at point:
821 - LAST-INTERVAL is the interval in days that was used to schedule the item's
822 current review date.
823 - REPEATS is the number of items the item has been successfully recalled without
824 without any failures. It is reset to 0 upon failure to recall the item.
825 - FAILURES is the total number of times the user has failed to recall the item.
826 - TOTAL-REPEATS includes both successful and unsuccessful repetitions.
827 - AVERAGE-QUALITY is the mean quality of recall of the item over
828 all its repetitions, successful and unsuccessful.
829 - EASE is a number reflecting how easy the item is to learn. Higher is easier.
831 (let ((learn-str (org-entry-get (point) "LEARN_DATA"))
832 (repeats (org-drill-entry-total-repeats :missing)))
833 (cond
834 (learn-str
835 (let ((learn-data (or (and learn-str
836 (read learn-str))
837 (copy-list initial-repetition-state))))
838 (list (nth 0 learn-data) ; last interval
839 (nth 1 learn-data) ; repetitions
840 (org-drill-entry-failure-count)
841 (nth 1 learn-data)
842 (org-drill-entry-last-quality)
843 (nth 2 learn-data) ; EF
845 ((not (eql :missing repeats))
846 (list (org-drill-entry-last-interval)
847 (org-drill-entry-repeats-since-fail)
848 (org-drill-entry-failure-count)
849 (org-drill-entry-total-repeats)
850 (org-drill-entry-average-quality)
851 (org-drill-entry-ease)))
852 (t ; virgin item
853 (list 0 0 0 0 nil nil)))))
856 (defun org-drill-store-item-data (last-interval repeats failures
857 total-repeats meanq
858 ease)
859 "Stores the given data in the item at point."
860 (org-entry-delete (point) "LEARN_DATA")
861 (org-set-property "DRILL_LAST_INTERVAL"
862 (number-to-string (round-float last-interval 4)))
863 (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats))
864 (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats))
865 (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures))
866 (org-set-property "DRILL_AVERAGE_QUALITY"
867 (number-to-string (round-float meanq 3)))
868 (org-set-property "DRILL_EASE"
869 (number-to-string (round-float ease 3))))
873 ;;; SM2 Algorithm =============================================================
876 (defun determine-next-interval-sm2 (last-interval n ef quality
877 failures meanq total-repeats)
878 "Arguments:
879 - LAST-INTERVAL -- the number of days since the item was last reviewed.
880 - REPEATS -- the number of times the item has been successfully reviewed
881 - EF -- the 'easiness factor'
882 - QUALITY -- 0 to 5
884 Returns a list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX), where:
885 - INTERVAL is the number of days until the item should next be reviewed
886 - REPEATS is incremented by 1.
887 - EF is modified based on the recall quality for the item.
888 - OF-MATRIX is not modified."
889 (assert (> n 0))
890 (assert (and (>= quality 0) (<= quality 5)))
891 (if (<= quality org-drill-failure-quality)
892 ;; When an item is failed, its interval is reset to 0,
893 ;; but its EF is unchanged
894 (list -1 1 ef (1+ failures) meanq (1+ total-repeats)
895 org-drill-optimal-factor-matrix)
896 ;; else:
897 (let* ((next-ef (modify-e-factor ef quality))
898 (interval
899 (cond
900 ((<= n 1) 1)
901 ((= n 2)
902 (cond
903 (org-drill-add-random-noise-to-intervals-p
904 (case quality
905 (5 6)
906 (4 4)
907 (3 3)
908 (2 1)
909 (t -1)))
910 (t 6)))
911 (t (* last-interval next-ef)))))
912 (list (if org-drill-add-random-noise-to-intervals-p
913 (+ last-interval (* (- interval last-interval)
914 (org-drill-random-dispersal-factor)))
915 interval)
916 (1+ n)
917 next-ef
918 failures meanq (1+ total-repeats)
919 org-drill-optimal-factor-matrix))))
922 ;;; SM5 Algorithm =============================================================
926 (defun initial-optimal-factor-sm5 (n ef)
927 (if (= 1 n)
928 org-drill-sm5-initial-interval
929 ef))
931 (defun get-optimal-factor-sm5 (n ef of-matrix)
932 (let ((factors (assoc n of-matrix)))
933 (or (and factors
934 (let ((ef-of (assoc ef (cdr factors))))
935 (and ef-of (cdr ef-of))))
936 (initial-optimal-factor-sm5 n ef))))
939 (defun inter-repetition-interval-sm5 (last-interval n ef &optional of-matrix)
940 (let ((of (get-optimal-factor-sm5 n ef (or of-matrix
941 org-drill-optimal-factor-matrix))))
942 (if (= 1 n)
944 (* of last-interval))))
947 (defun determine-next-interval-sm5 (last-interval n ef quality
948 failures meanq total-repeats
949 of-matrix &optional delta-days)
950 (if (zerop n) (setq n 1))
951 (if (null ef) (setq ef 2.5))
952 (assert (> n 0))
953 (assert (and (>= quality 0) (<= quality 5)))
954 (unless of-matrix
955 (setq of-matrix org-drill-optimal-factor-matrix))
956 (setq of-matrix (cl-copy-tree of-matrix))
958 (setq meanq (if meanq
959 (/ (+ quality (* meanq total-repeats 1.0))
960 (1+ total-repeats))
961 quality))
963 (let ((next-ef (modify-e-factor ef quality))
964 (old-ef ef)
965 (new-of (modify-of (get-optimal-factor-sm5 n ef of-matrix)
966 quality org-drill-learn-fraction))
967 (interval nil))
968 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
969 delta-days (minusp delta-days))
970 (setq new-of (org-drill-early-interval-factor
971 (get-optimal-factor-sm5 n ef of-matrix)
972 (inter-repetition-interval-sm5
973 last-interval n ef of-matrix)
974 delta-days)))
976 (setq of-matrix
977 (set-optimal-factor n next-ef of-matrix
978 (round-float new-of 3))) ; round OF to 3 d.p.
980 (setq ef next-ef)
982 (cond
983 ;; "Failed" -- reset repetitions to 0,
984 ((<= quality org-drill-failure-quality)
985 (list -1 1 old-ef (1+ failures) meanq (1+ total-repeats)
986 of-matrix)) ; Not clear if OF matrix is supposed to be
987 ; preserved
988 ;; For a zero-based quality of 4 or 5, don't repeat
989 ;; ((and (>= quality 4)
990 ;; (not org-learn-always-reschedule))
991 ;; (list 0 (1+ n) ef failures meanq
992 ;; (1+ total-repeats) of-matrix)) ; 0 interval = unschedule
994 (setq interval (inter-repetition-interval-sm5
995 last-interval n ef of-matrix))
996 (if org-drill-add-random-noise-to-intervals-p
997 (setq interval (* interval (org-drill-random-dispersal-factor))))
998 (list interval
999 (1+ n)
1001 failures
1002 meanq
1003 (1+ total-repeats)
1004 of-matrix)))))
1007 ;;; Simple8 Algorithm =========================================================
1010 (defun org-drill-simple8-first-interval (failures)
1011 "Arguments:
1012 - FAILURES: integer >= 0. The total number of times the item has
1013 been forgotten, ever.
1015 Returns the optimal FIRST interval for an item which has previously been
1016 forgotten on FAILURES occasions."
1017 (* 2.4849 (exp (* -0.057 failures))))
1020 (defun org-drill-simple8-interval-factor (ease repetition)
1021 "Arguments:
1022 - EASE: floating point number >= 1.2. Corresponds to `AF' in SM8 algorithm.
1023 - REPETITION: the number of times the item has been tested.
1024 1 is the first repetition (ie the second trial).
1025 Returns:
1026 The factor by which the last interval should be
1027 multiplied to give the next interval. Corresponds to `RF' or `OF'."
1028 (+ 1.2 (* (- ease 1.2) (expt org-drill-learn-fraction (log repetition 2)))))
1031 (defun org-drill-simple8-quality->ease (quality)
1032 "Returns the ease (`AF' in the SM8 algorithm) which corresponds
1033 to a mean item quality of QUALITY."
1034 (+ (* 0.0542 (expt quality 4))
1035 (* -0.4848 (expt quality 3))
1036 (* 1.4916 (expt quality 2))
1037 (* -1.2403 quality)
1038 1.4515))
1041 (defun determine-next-interval-simple8 (last-interval repeats quality
1042 failures meanq totaln
1043 &optional delta-days)
1044 "Arguments:
1045 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1046 - REPEATS -- the number of times the item has been successfully reviewed
1047 - EASE -- the 'easiness factor'
1048 - QUALITY -- 0 to 5
1049 - DELTA-DAYS -- how many days overdue was the item when it was reviewed.
1050 0 = reviewed on the scheduled day. +N = N days overdue.
1051 -N = reviewed N days early.
1053 Returns the new item data, as a list of 6 values:
1054 - NEXT-INTERVAL
1055 - REPEATS
1056 - EASE
1057 - FAILURES
1058 - AVERAGE-QUALITY
1059 - TOTAL-REPEATS.
1060 See the documentation for `org-drill-get-item-data' for a description of these."
1061 (assert (>= repeats 0))
1062 (assert (and (>= quality 0) (<= quality 5)))
1063 (assert (or (null meanq) (and (>= meanq 0) (<= meanq 5))))
1064 (let ((next-interval nil))
1065 (setf meanq (if meanq
1066 (/ (+ quality (* meanq totaln 1.0)) (1+ totaln))
1067 quality))
1068 (cond
1069 ((<= quality org-drill-failure-quality)
1070 (incf failures)
1071 (setf repeats 0
1072 next-interval -1))
1073 ((or (zerop repeats)
1074 (zerop last-interval))
1075 (setf next-interval (org-drill-simple8-first-interval failures))
1076 (incf repeats)
1077 (incf totaln))
1079 (let* ((use-n
1080 (if (and
1081 org-drill-adjust-intervals-for-early-and-late-repetitions-p
1082 (numberp delta-days) (plusp delta-days)
1083 (plusp last-interval))
1084 (+ repeats (min 1 (/ delta-days last-interval 1.0)))
1085 repeats))
1086 (factor (org-drill-simple8-interval-factor
1087 (org-drill-simple8-quality->ease meanq) use-n))
1088 (next-int (* last-interval factor)))
1089 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1090 (numberp delta-days) (minusp delta-days))
1091 ;; The item was reviewed earlier than scheduled.
1092 (setf factor (org-drill-early-interval-factor
1093 factor next-int (abs delta-days))
1094 next-int (* last-interval factor)))
1095 (setf next-interval next-int)
1096 (incf repeats)
1097 (incf totaln))))
1098 (list
1099 (if (and org-drill-add-random-noise-to-intervals-p
1100 (plusp next-interval))
1101 (* next-interval (org-drill-random-dispersal-factor))
1102 next-interval)
1103 repeats
1104 (org-drill-simple8-quality->ease meanq)
1105 failures
1106 meanq
1107 totaln
1113 ;;; Essentially copied from `org-learn.el', but modified to
1114 ;;; optionally call the SM2 or simple8 functions.
1115 (defun org-drill-smart-reschedule (quality &optional days-ahead)
1116 "If DAYS-AHEAD is supplied it must be a positive integer. The
1117 item will be scheduled exactly this many days into the future."
1118 (let ((delta-days (- (time-to-days (current-time))
1119 (time-to-days (or (org-get-scheduled-time (point))
1120 (current-time)))))
1121 (ofmatrix org-drill-optimal-factor-matrix)
1122 ;; Entries can have weights, 1 by default. Intervals are divided by the
1123 ;; item's weight, so an item with a weight of 2 will have all intervals
1124 ;; halved, meaning you will end up reviewing it twice as often.
1125 ;; Useful for entries which randomly present any of several facts.
1126 (weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1127 (if (stringp weight)
1128 (setq weight (read weight)))
1129 (destructuring-bind (last-interval repetitions failures
1130 total-repeats meanq ease)
1131 (org-drill-get-item-data)
1132 (destructuring-bind (next-interval repetitions ease
1133 failures meanq total-repeats
1134 &optional new-ofmatrix)
1135 (case org-drill-spaced-repetition-algorithm
1136 (sm5 (determine-next-interval-sm5 last-interval repetitions
1137 ease quality failures
1138 meanq total-repeats ofmatrix))
1139 (sm2 (determine-next-interval-sm2 last-interval repetitions
1140 ease quality failures
1141 meanq total-repeats))
1142 (simple8 (determine-next-interval-simple8 last-interval repetitions
1143 quality failures meanq
1144 total-repeats
1145 delta-days)))
1146 (if (numberp days-ahead)
1147 (setq next-interval days-ahead))
1149 (if (and (null days-ahead)
1150 (numberp weight) (plusp weight)
1151 (not (minusp next-interval)))
1152 (setq next-interval
1153 (max 1.0 (+ last-interval
1154 (/ (- next-interval last-interval) weight)))))
1156 (org-drill-store-item-data next-interval repetitions failures
1157 total-repeats meanq ease)
1159 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
1160 (setq org-drill-optimal-factor-matrix new-ofmatrix))
1162 (cond
1163 ((= 0 days-ahead)
1164 (org-schedule t))
1165 ((minusp days-ahead)
1166 (org-schedule nil (current-time)))
1168 (org-schedule nil (time-add (current-time)
1169 (days-to-time
1170 (round next-interval))))))))))
1173 (defun org-drill-hypothetical-next-review-date (quality)
1174 "Returns an integer representing the number of days into the future
1175 that the current item would be scheduled, based on a recall quality
1176 of QUALITY."
1177 (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1178 (destructuring-bind (last-interval repetitions failures
1179 total-repeats meanq ease)
1180 (org-drill-get-item-data)
1181 (if (stringp weight)
1182 (setq weight (read weight)))
1183 (destructuring-bind (next-interval repetitions ease
1184 failures meanq total-repeats
1185 &optional ofmatrix)
1186 (case org-drill-spaced-repetition-algorithm
1187 (sm5 (determine-next-interval-sm5 last-interval repetitions
1188 ease quality failures
1189 meanq total-repeats
1190 org-drill-optimal-factor-matrix))
1191 (sm2 (determine-next-interval-sm2 last-interval repetitions
1192 ease quality failures
1193 meanq total-repeats))
1194 (simple8 (determine-next-interval-simple8 last-interval repetitions
1195 quality failures meanq
1196 total-repeats)))
1197 (cond
1198 ((not (plusp next-interval))
1200 ((and (numberp weight) (plusp weight))
1201 (+ last-interval
1202 (max 1.0 (/ (- next-interval last-interval) weight))))
1204 next-interval))))))
1207 (defun org-drill-hypothetical-next-review-dates ()
1208 (let ((intervals nil))
1209 (dotimes (q 6)
1210 (push (max (or (car intervals) 0)
1211 (org-drill-hypothetical-next-review-date q))
1212 intervals))
1213 (reverse intervals)))
1216 (defun org-drill-reschedule ()
1217 "Returns quality rating (0-5), or nil if the user quit."
1218 (let ((ch nil)
1219 (input nil)
1220 (next-review-dates (org-drill-hypothetical-next-review-dates)))
1221 (save-excursion
1222 (while (not (memq ch '(?q ?e ?0 ?1 ?2 ?3 ?4 ?5)))
1223 (setq input (read-key-sequence
1224 (if (eq ch ??)
1225 (format "0-2 Means you have forgotten the item.
1226 3-5 Means you have remembered the item.
1228 0 - Completely forgot.
1229 1 - Even after seeing the answer, it still took a bit to sink in.
1230 2 - After seeing the answer, you remembered it.
1231 3 - It took you awhile, but you finally remembered. (+%s days)
1232 4 - After a little bit of thought you remembered. (+%s days)
1233 5 - You remembered the item really easily. (+%s days)
1235 How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)"
1236 (round (nth 3 next-review-dates))
1237 (round (nth 4 next-review-dates))
1238 (round (nth 5 next-review-dates)))
1239 "How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)")))
1240 (cond
1241 ((stringp input)
1242 (setq ch (elt input 0)))
1243 ((and (vectorp input) (symbolp (elt input 0)))
1244 (case (elt input 0)
1245 (up (ignore-errors (forward-line -1)))
1246 (down (ignore-errors (forward-line 1)))
1247 (left (ignore-errors (backward-char)))
1248 (right (ignore-errors (forward-char)))
1249 (prior (ignore-errors (scroll-down))) ; pgup
1250 (next (ignore-errors (scroll-up))))) ; pgdn
1251 ((and (vectorp input) (listp (elt input 0))
1252 (eventp (elt input 0)))
1253 (case (car (elt input 0))
1254 (wheel-up (ignore-errors (mwheel-scroll (elt input 0))))
1255 (wheel-down (ignore-errors (mwheel-scroll (elt input 0)))))))
1256 (if (eql ch ?t)
1257 (org-set-tags-command))))
1258 (cond
1259 ((and (>= ch ?0) (<= ch ?5))
1260 (let ((quality (- ch ?0))
1261 (failures (org-drill-entry-failure-count)))
1262 (save-excursion
1263 (org-drill-smart-reschedule quality
1264 (nth quality next-review-dates)))
1265 (push quality *org-drill-session-qualities*)
1266 (cond
1267 ((<= quality org-drill-failure-quality)
1268 (when org-drill-leech-failure-threshold
1269 ;;(setq failures (if failures (string-to-number failures) 0))
1270 ;; (org-set-property "DRILL_FAILURE_COUNT"
1271 ;; (format "%d" (1+ failures)))
1272 (if (> (1+ failures) org-drill-leech-failure-threshold)
1273 (org-toggle-tag "leech" 'on))))
1275 (let ((scheduled-time (org-get-scheduled-time (point))))
1276 (when scheduled-time
1277 (message "Next review in %d days"
1278 (- (time-to-days scheduled-time)
1279 (time-to-days (current-time))))
1280 (sit-for 0.5)))))
1281 (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality))
1282 (org-set-property "DRILL_LAST_REVIEWED"
1283 (time-to-inactive-org-timestamp (current-time)))
1284 quality))
1285 ((= ch ?e)
1286 'edit)
1288 nil))))
1291 ;; (defun org-drill-hide-all-subheadings-except (heading-list)
1292 ;; "Returns a list containing the position of each immediate subheading of
1293 ;; the current topic."
1294 ;; (let ((drill-entry-level (org-current-level))
1295 ;; (drill-sections nil)
1296 ;; (drill-heading nil))
1297 ;; (org-show-subtree)
1298 ;; (save-excursion
1299 ;; (org-map-entries
1300 ;; (lambda ()
1301 ;; (when (and (not (outline-invisible-p))
1302 ;; (> (org-current-level) drill-entry-level))
1303 ;; (setq drill-heading (org-get-heading t))
1304 ;; (unless (and (= (org-current-level) (1+ drill-entry-level))
1305 ;; (member drill-heading heading-list))
1306 ;; (hide-subtree))
1307 ;; (push (point) drill-sections)))
1308 ;; "" 'tree))
1309 ;; (reverse drill-sections)))
1313 (defun org-drill-hide-subheadings-if (test)
1314 "TEST is a function taking no arguments. TEST will be called for each
1315 of the immediate subheadings of the current drill item, with the point
1316 on the relevant subheading. TEST should return nil if the subheading is
1317 to be revealed, non-nil if it is to be hidden.
1318 Returns a list containing the position of each immediate subheading of
1319 the current topic."
1320 (let ((drill-entry-level (org-current-level))
1321 (drill-sections nil))
1322 (org-show-subtree)
1323 (save-excursion
1324 (org-map-entries
1325 (lambda ()
1326 (when (and (not (outline-invisible-p))
1327 (> (org-current-level) drill-entry-level))
1328 (when (or (/= (org-current-level) (1+ drill-entry-level))
1329 (funcall test))
1330 (hide-subtree))
1331 (push (point) drill-sections)))
1332 "" 'tree))
1333 (reverse drill-sections)))
1336 (defun org-drill-hide-all-subheadings-except (heading-list)
1337 (org-drill-hide-subheadings-if
1338 (lambda () (let ((drill-heading (org-get-heading t)))
1339 (not (member drill-heading heading-list))))))
1342 (defun org-drill-presentation-prompt (&rest fmt-and-args)
1343 (let* ((item-start-time (current-time))
1344 (input nil)
1345 (ch nil)
1346 (last-second 0)
1347 (mature-entry-count (+ (length *org-drill-young-mature-entries*)
1348 (length *org-drill-old-mature-entries*)
1349 (length *org-drill-overdue-entries*)))
1350 (status (first (org-drill-entry-status)))
1351 (prompt
1352 (if fmt-and-args
1353 (apply 'format
1354 (first fmt-and-args)
1355 (rest fmt-and-args))
1356 (concat "Press key for answer, "
1357 "e=edit, t=tags, s=skip, q=quit."))))
1358 (setq prompt
1359 (format "%s %s %s %s %s %s"
1360 (propertize
1361 (char-to-string
1362 (case status
1363 (:new ?N) (:young ?Y) (:old ?o) (:overdue ?!)
1364 (:failed ?F) (t ??)))
1365 'face `(:foreground
1366 ,(case status
1367 (:new org-drill-new-count-color)
1368 ((:young :old) org-drill-mature-count-color)
1369 ((:overdue :failed) org-drill-failed-count-color)
1370 (t org-drill-done-count-color))))
1371 (propertize
1372 (number-to-string (length *org-drill-done-entries*))
1373 'face `(:foreground ,org-drill-done-count-color)
1374 'help-echo "The number of items you have reviewed this session.")
1375 (propertize
1376 (number-to-string (+ (length *org-drill-again-entries*)
1377 (length *org-drill-failed-entries*)))
1378 'face `(:foreground ,org-drill-failed-count-color)
1379 'help-echo (concat "The number of items that you failed, "
1380 "and need to review again."))
1381 (propertize
1382 (number-to-string mature-entry-count)
1383 'face `(:foreground ,org-drill-mature-count-color)
1384 'help-echo "The number of old items due for review.")
1385 (propertize
1386 (number-to-string (length *org-drill-new-entries*))
1387 'face `(:foreground ,org-drill-new-count-color)
1388 'help-echo (concat "The number of new items that you "
1389 "have never reviewed."))
1390 prompt))
1391 (if (and (eql 'warn org-drill-leech-method)
1392 (org-drill-entry-leech-p))
1393 (setq prompt (concat
1394 (propertize "!!! LEECH ITEM !!!
1395 You seem to be having a lot of trouble memorising this item.
1396 Consider reformulating the item to make it easier to remember.\n"
1397 'face '(:foreground "red"))
1398 prompt)))
1399 (while (memq ch '(nil ?t))
1400 (setq ch nil)
1401 (while (not (input-pending-p))
1402 (let ((elapsed (time-subtract (current-time) item-start-time)))
1403 (message (concat (if (>= (time-to-seconds elapsed) (* 60 60))
1404 "++:++ "
1405 (format-time-string "%M:%S " elapsed))
1406 prompt))
1407 (sit-for 1)))
1408 (setq input (read-key-sequence nil))
1409 (if (stringp input) (setq ch (elt input 0)))
1410 (if (eql ch ?t)
1411 (org-set-tags-command)))
1412 (case ch
1413 (?q nil)
1414 (?e 'edit)
1415 (?s 'skip)
1416 (otherwise t))))
1419 (defun org-pos-in-regexp (pos regexp &optional nlines)
1420 (save-excursion
1421 (goto-char pos)
1422 (org-in-regexp regexp nlines)))
1425 (defun org-drill-hide-region (beg end &optional text)
1426 "Hide the buffer region between BEG and END with an 'invisible text'
1427 visual overlay, or with the string TEXT if it is supplied."
1428 (let ((ovl (make-overlay beg end)))
1429 (overlay-put ovl 'category
1430 'org-drill-hidden-text-overlay)
1431 (when (stringp text)
1432 (overlay-put ovl 'invisible nil)
1433 (overlay-put ovl 'face 'default)
1434 (overlay-put ovl 'display text))))
1437 (defun org-drill-hide-heading-at-point (&optional text)
1438 (unless (org-at-heading-p)
1439 (error "Point is not on a heading."))
1440 (save-excursion
1441 (let ((beg (point)))
1442 (end-of-line)
1443 (org-drill-hide-region beg (point) text))))
1446 (defun org-drill-hide-comments ()
1447 (save-excursion
1448 (while (re-search-forward "^#.*$" nil t)
1449 (org-drill-hide-region (match-beginning 0) (match-end 0)))))
1452 (defun org-drill-unhide-text ()
1453 ;; This will also unhide the item's heading.
1454 (save-excursion
1455 (dolist (ovl (overlays-in (point-min) (point-max)))
1456 (when (eql 'org-drill-hidden-text-overlay (overlay-get ovl 'category))
1457 (delete-overlay ovl)))))
1460 (defun org-drill-hide-clozed-text ()
1461 (save-excursion
1462 (while (re-search-forward org-drill-cloze-regexp nil t)
1463 ;; Don't hide org links, partly because they might contain inline
1464 ;; images which we want to keep visible
1465 (unless (save-match-data
1466 (org-pos-in-regexp (match-beginning 0)
1467 org-bracket-link-regexp 1))
1468 (org-drill-hide-matched-cloze-text)))))
1471 (defun org-drill-hide-matched-cloze-text ()
1472 "Hide the current match with a 'cloze' visual overlay."
1473 (let ((ovl (make-overlay (match-beginning 0) (match-end 0))))
1474 (overlay-put ovl 'category
1475 'org-drill-cloze-overlay-defaults)
1476 (when (find ?| (match-string 0))
1477 (let ((hint (substring-no-properties
1478 (match-string 0)
1479 (1+ (position ?| (match-string 0)))
1480 (1- (length (match-string 0))))))
1481 (overlay-put
1482 ovl 'display
1483 ;; If hint is like `X...' then display [X...]
1484 ;; otherwise display [...X]
1485 (format (if (string-match-p "\\.\\.\\." hint) "[%s]" "[%s...]")
1486 hint))))))
1489 (defun org-drill-hide-cloze-hints ()
1490 (save-excursion
1491 (while (re-search-forward org-drill-cloze-regexp nil t)
1492 (unless (or (save-match-data
1493 (org-pos-in-regexp (match-beginning 0)
1494 org-bracket-link-regexp 1))
1495 (null (match-beginning 2))) ; hint subexpression matched
1496 (org-drill-hide-region (match-beginning 2) (match-end 2))))))
1499 (defmacro with-replaced-entry-text (text &rest body)
1500 "During the execution of BODY, the entire text of the current entry is
1501 concealed by an overlay that displays the string TEXT."
1502 `(progn
1503 (org-drill-replace-entry-text ,text)
1504 (unwind-protect
1505 (progn
1506 ,@body)
1507 (org-drill-unreplace-entry-text))))
1510 (defun org-drill-replace-entry-text (text)
1511 "Make an overlay that conceals the entire text of the item, not
1512 including properties or the contents of subheadings. The overlay shows
1513 the string TEXT.
1514 Note: does not actually alter the item."
1515 (let ((ovl (make-overlay (point-min)
1516 (save-excursion
1517 (outline-next-heading)
1518 (point)))))
1519 (overlay-put ovl 'category
1520 'org-drill-replaced-text-overlay)
1521 (overlay-put ovl 'display text)))
1524 (defun org-drill-unreplace-entry-text ()
1525 (save-excursion
1526 (dolist (ovl (overlays-in (point-min) (point-max)))
1527 (when (eql 'org-drill-replaced-text-overlay (overlay-get ovl 'category))
1528 (delete-overlay ovl)))))
1531 (defmacro with-replaced-entry-heading (heading &rest body)
1532 `(progn
1533 (org-drill-replace-entry-heading ,heading)
1534 (unwind-protect
1535 (progn
1536 ,@body)
1537 (org-drill-unhide-text))))
1540 (defun org-drill-replace-entry-heading (heading)
1541 "Make an overlay that conceals the heading of the item. The overlay shows
1542 the string TEXT.
1543 Note: does not actually alter the item."
1544 (org-drill-hide-heading-at-point heading))
1547 (defun org-drill-unhide-clozed-text ()
1548 (save-excursion
1549 (dolist (ovl (overlays-in (point-min) (point-max)))
1550 (when (eql 'org-drill-cloze-overlay-defaults (overlay-get ovl 'category))
1551 (delete-overlay ovl)))))
1554 (defun org-drill-get-entry-text (&optional keep-properties-p)
1555 (let ((text (org-agenda-get-some-entry-text (point-marker) 100)))
1556 (if keep-properties-p
1557 text
1558 (substring-no-properties text))))
1561 (defun org-drill-entry-empty-p ()
1562 (zerop (length (org-drill-get-entry-text))))
1566 ;;; Presentation functions ====================================================
1568 ;; Each of these is called with point on topic heading. Each needs to show the
1569 ;; topic in the form of a 'question' or with some information 'hidden', as
1570 ;; appropriate for the card type. The user should then be prompted to press a
1571 ;; key. The function should then reveal either the 'answer' or the entire
1572 ;; topic, and should return t if the user chose to see the answer and rate their
1573 ;; recall, nil if they chose to quit.
1575 (defun org-drill-present-simple-card ()
1576 (with-hidden-comments
1577 (with-hidden-cloze-hints
1578 (with-hidden-cloze-text
1579 (org-drill-hide-all-subheadings-except nil)
1580 (org-display-inline-images t)
1581 (org-cycle-hide-drawers 'all)
1582 (prog1 (org-drill-presentation-prompt)
1583 (org-drill-hide-subheadings-if 'org-drill-entry-p))))))
1586 (defun org-drill-present-default-answer (reschedule-fn)
1587 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1588 (org-drill-unhide-clozed-text)
1589 (with-hidden-cloze-hints
1590 (funcall reschedule-fn)))
1593 (defun org-drill-present-two-sided-card ()
1594 (with-hidden-comments
1595 (with-hidden-cloze-hints
1596 (with-hidden-cloze-text
1597 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1598 (when drill-sections
1599 (save-excursion
1600 (goto-char (nth (random* (min 2 (length drill-sections)))
1601 drill-sections))
1602 (org-show-subtree)))
1603 (org-display-inline-images t)
1604 (org-cycle-hide-drawers 'all)
1605 (prog1 (org-drill-presentation-prompt)
1606 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1610 (defun org-drill-present-multi-sided-card ()
1611 (with-hidden-comments
1612 (with-hidden-cloze-hints
1613 (with-hidden-cloze-text
1614 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1615 (when drill-sections
1616 (save-excursion
1617 (goto-char (nth (random* (length drill-sections)) drill-sections))
1618 (org-show-subtree)))
1619 (org-display-inline-images t)
1620 (org-cycle-hide-drawers 'all)
1621 (prog1 (org-drill-presentation-prompt)
1622 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1625 (defun org-drill-present-multicloze-hide-n (number-to-hide
1626 &optional
1627 force-show-first
1628 force-show-last
1629 force-hide-first)
1630 "Hides NUMBER-TO-HIDE pieces of text that are marked for cloze deletion,
1631 chosen at random.
1632 If NUMBER-TO-HIDE is negative, show only (ABS NUMBER-TO-HIDE) pieces,
1633 hiding all the rest.
1634 If FORCE-HIDE-FIRST is non-nil, force the first piece of text to be one of
1635 the hidden items.
1636 If FORCE-SHOW-FIRST is non-nil, never hide the first piece of text.
1637 If FORCE-SHOW-LAST is non-nil, never hide the last piece of text.
1638 If the number of text pieces in the item is less than
1639 NUMBER-TO-HIDE, then all text pieces will be hidden (except the first or last
1640 items if FORCE-SHOW-FIRST or FORCE-SHOW-LAST is non-nil)."
1641 (with-hidden-comments
1642 (with-hidden-cloze-hints
1643 (let ((item-end nil)
1644 (match-count 0)
1645 (body-start (or (cdr (org-get-property-block))
1646 (point))))
1647 (if (and force-hide-first force-show-first)
1648 (error "FORCE-HIDE-FIRST and FORCE-SHOW-FIRST are mutually exclusive"))
1649 (org-drill-hide-all-subheadings-except nil)
1650 (save-excursion
1651 (outline-next-heading)
1652 (setq item-end (point)))
1653 (save-excursion
1654 (goto-char body-start)
1655 (while (re-search-forward org-drill-cloze-regexp item-end t)
1656 (let ((in-regexp? (save-match-data
1657 (org-pos-in-regexp (match-beginning 0)
1658 org-bracket-link-regexp 1))))
1659 (unless in-regexp?
1660 (incf match-count)))))
1661 (if (minusp number-to-hide)
1662 (setq number-to-hide (+ match-count number-to-hide)))
1663 (when (plusp match-count)
1664 (let* ((positions (shuffle-list (loop for i from 1
1665 to match-count
1666 collect i)))
1667 (match-nums nil)
1668 (cnt nil))
1669 (if force-hide-first
1670 ;; Force '1' to be in the list, and to be the first item
1671 ;; in the list.
1672 (setq positions (cons 1 (remove 1 positions))))
1673 (if force-show-first
1674 (setq positions (remove 1 positions)))
1675 (if force-show-last
1676 (setq positions (remove match-count positions)))
1677 (setq match-nums
1678 (subseq positions
1679 0 (min number-to-hide (length positions))))
1680 ;; (dolist (pos-to-hide match-nums)
1681 (save-excursion
1682 (goto-char body-start)
1683 (setq cnt 0)
1684 (while (re-search-forward org-drill-cloze-regexp item-end t)
1685 (unless (save-match-data
1686 (org-pos-in-regexp (match-beginning 0)
1687 org-bracket-link-regexp 1))
1688 (incf cnt)
1689 (if (memq cnt match-nums)
1690 (org-drill-hide-matched-cloze-text)))))))
1691 ;; (loop
1692 ;; do (re-search-forward org-drill-cloze-regexp
1693 ;; item-end t pos-to-hide)
1694 ;; while (org-pos-in-regexp (match-beginning 0)
1695 ;; org-bracket-link-regexp 1))
1696 ;; (org-drill-hide-matched-cloze-text)))))
1697 (org-display-inline-images t)
1698 (org-cycle-hide-drawers 'all)
1699 (prog1 (org-drill-presentation-prompt)
1700 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1701 (org-drill-unhide-clozed-text))))))
1704 (defun org-drill-present-multicloze-hide-nth (to-hide)
1705 "Hide the TO-HIDE'th piece of clozed text. 1 is the first piece. If
1706 TO-HIDE is negative, count backwards, so -1 means the last item, -2
1707 the second to last, etc."
1708 (with-hidden-comments
1709 (with-hidden-cloze-hints
1710 (let ((item-end nil)
1711 (match-count 0)
1712 (body-start (or (cdr (org-get-property-block))
1713 (point)))
1714 (cnt 0))
1715 (org-drill-hide-all-subheadings-except nil)
1716 (save-excursion
1717 (outline-next-heading)
1718 (setq item-end (point)))
1719 (save-excursion
1720 (goto-char body-start)
1721 (while (re-search-forward org-drill-cloze-regexp item-end t)
1722 (let ((in-regexp? (save-match-data
1723 (org-pos-in-regexp (match-beginning 0)
1724 org-bracket-link-regexp 1))))
1725 (unless in-regexp?
1726 (incf match-count)))))
1727 (if (minusp to-hide)
1728 (setq to-hide (+ 1 to-hide match-count)))
1729 (cond
1730 ((or (not (plusp match-count))
1731 (> to-hide match-count))
1732 nil)
1734 (save-excursion
1735 (goto-char body-start)
1736 (setq cnt 0)
1737 (while (re-search-forward org-drill-cloze-regexp item-end t)
1738 (unless (save-match-data
1739 (org-pos-in-regexp (match-beginning 0)
1740 org-bracket-link-regexp 1))
1741 (incf cnt)
1742 (if (= cnt to-hide)
1743 (org-drill-hide-matched-cloze-text)))))))
1744 (org-display-inline-images t)
1745 (org-cycle-hide-drawers 'all)
1746 (prog1 (org-drill-presentation-prompt)
1747 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1748 (org-drill-unhide-clozed-text))))))
1751 (defun org-drill-present-multicloze-hide1 ()
1752 "Hides one of the pieces of text that are marked for cloze deletion,
1753 chosen at random."
1754 (org-drill-present-multicloze-hide-n 1))
1757 (defun org-drill-present-multicloze-hide2 ()
1758 "Hides two of the pieces of text that are marked for cloze deletion,
1759 chosen at random."
1760 (org-drill-present-multicloze-hide-n 2))
1763 (defun org-drill-present-multicloze-hide-first ()
1764 "Hides the first piece of text that is marked for cloze deletion."
1765 (org-drill-present-multicloze-hide-nth 1))
1768 (defun org-drill-present-multicloze-hide-last ()
1769 "Hides the last piece of text that is marked for cloze deletion."
1770 (org-drill-present-multicloze-hide-nth -1))
1773 (defun org-drill-present-multicloze-hide1-firstmore ()
1774 "Commonly, hides the FIRST piece of text that is marked for
1775 cloze deletion. Uncommonly, hide one of the other pieces of text,
1776 chosen at random.
1778 The definitions of 'commonly' and 'uncommonly' are determined by
1779 the value of `org-drill-cloze-text-weight'."
1780 ;; The 'firstmore' and 'lastmore' functions used to randomly choose whether
1781 ;; to hide the 'favoured' piece of text. However even when the chance of
1782 ;; hiding it was set quite high (80%), the outcome was too unpredictable over
1783 ;; the small number of repetitions where most learning takes place for each
1784 ;; item. In other words, the actual frequency during the first 10 repetitions
1785 ;; was often very different from 80%. Hence we use modulo instead.
1786 (cond
1787 ((null org-drill-cloze-text-weight)
1788 ;; Behave as hide1cloze
1789 (org-drill-present-multicloze-hide1))
1790 ((not (and (integerp org-drill-cloze-text-weight)
1791 (plusp org-drill-cloze-text-weight)))
1792 (error "Illegal value for org-drill-cloze-text-weight: %S"
1793 org-drill-cloze-text-weight))
1794 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1795 org-drill-cloze-text-weight))
1796 ;; Uncommonly, hide any item except the first
1797 (org-drill-present-multicloze-hide-n 1 t))
1799 ;; Commonly, hide first item
1800 (org-drill-present-multicloze-hide-first))))
1803 (defun org-drill-present-multicloze-show1-lastmore ()
1804 "Commonly, hides all pieces except the last. Uncommonly, shows
1805 any random piece. The effect is similar to 'show1cloze' except
1806 that the last item is much less likely to be the item that is
1807 visible.
1809 The definitions of 'commonly' and 'uncommonly' are determined by
1810 the value of `org-drill-cloze-text-weight'."
1811 (cond
1812 ((null org-drill-cloze-text-weight)
1813 ;; Behave as show1cloze
1814 (org-drill-present-multicloze-show1))
1815 ((not (and (integerp org-drill-cloze-text-weight)
1816 (plusp org-drill-cloze-text-weight)))
1817 (error "Illegal value for org-drill-cloze-text-weight: %S"
1818 org-drill-cloze-text-weight))
1819 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1820 org-drill-cloze-text-weight))
1821 ;; Uncommonly, show any item except the last
1822 (org-drill-present-multicloze-hide-n -1 nil nil t))
1824 ;; Commonly, show the LAST item
1825 (org-drill-present-multicloze-hide-n -1 nil t))))
1828 (defun org-drill-present-multicloze-show1-firstless ()
1829 "Commonly, hides all pieces except one, where the shown piece
1830 is guaranteed NOT to be the first piece. Uncommonly, shows any
1831 random piece. The effect is similar to 'show1cloze' except that
1832 the first item is much less likely to be the item that is
1833 visible.
1835 The definitions of 'commonly' and 'uncommonly' are determined by
1836 the value of `org-drill-cloze-text-weight'."
1837 (cond
1838 ((null org-drill-cloze-text-weight)
1839 ;; Behave as show1cloze
1840 (org-drill-present-multicloze-show1))
1841 ((not (and (integerp org-drill-cloze-text-weight)
1842 (plusp org-drill-cloze-text-weight)))
1843 (error "Illegal value for org-drill-cloze-text-weight: %S"
1844 org-drill-cloze-text-weight))
1845 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1846 org-drill-cloze-text-weight))
1847 ;; Uncommonly, show the first item
1848 (org-drill-present-multicloze-hide-n -1 t))
1850 ;; Commonly, show any item, except the first
1851 (org-drill-present-multicloze-hide-n -1 nil nil t))))
1854 (defun org-drill-present-multicloze-show1 ()
1855 "Similar to `org-drill-present-multicloze-hide1', but hides all
1856 the pieces of text that are marked for cloze deletion, except for one
1857 piece which is chosen at random."
1858 (org-drill-present-multicloze-hide-n -1))
1861 (defun org-drill-present-multicloze-show2 ()
1862 "Similar to `org-drill-present-multicloze-show1', but reveals two
1863 pieces rather than one."
1864 (org-drill-present-multicloze-hide-n -2))
1867 ;; (defun org-drill-present-multicloze-show1 ()
1868 ;; "Similar to `org-drill-present-multicloze-hide1', but hides all
1869 ;; the pieces of text that are marked for cloze deletion, except for one
1870 ;; piece which is chosen at random."
1871 ;; (with-hidden-comments
1872 ;; (with-hidden-cloze-hints
1873 ;; (let ((item-end nil)
1874 ;; (match-count 0)
1875 ;; (body-start (or (cdr (org-get-property-block))
1876 ;; (point))))
1877 ;; (org-drill-hide-all-subheadings-except nil)
1878 ;; (save-excursion
1879 ;; (outline-next-heading)
1880 ;; (setq item-end (point)))
1881 ;; (save-excursion
1882 ;; (goto-char body-start)
1883 ;; (while (re-search-forward org-drill-cloze-regexp item-end t)
1884 ;; (incf match-count)))
1885 ;; (when (plusp match-count)
1886 ;; (let ((match-to-hide (random* match-count)))
1887 ;; (save-excursion
1888 ;; (goto-char body-start)
1889 ;; (dotimes (n match-count)
1890 ;; (re-search-forward org-drill-cloze-regexp
1891 ;; item-end t)
1892 ;; (unless (= n match-to-hide)
1893 ;; (org-drill-hide-matched-cloze-text))))))
1894 ;; (org-display-inline-images t)
1895 ;; (org-cycle-hide-drawers 'all)
1896 ;; (prog1 (org-drill-presentation-prompt)
1897 ;; (org-drill-hide-subheadings-if 'org-drill-entry-p)
1898 ;; (org-drill-unhide-clozed-text))))))
1901 (defun org-drill-present-card-using-text (question &optional answer)
1902 "Present the string QUESTION as the only visible content of the card."
1903 (with-hidden-comments
1904 (with-replaced-entry-text
1905 question
1906 (org-drill-hide-all-subheadings-except nil)
1907 (org-cycle-hide-drawers 'all)
1908 (prog1 (org-drill-presentation-prompt)
1909 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
1912 ;;; The following macro is necessary because `org-save-outline-visibility'
1913 ;;; currently discards the value returned by its body and returns a garbage
1914 ;;; value instead. (as at org mode v 7.5)
1916 (defmacro org-drill-save-visibility (&rest body)
1917 "Store the current visibility state of the org buffer, and restore it
1918 after executing BODY. Return the value of the last expression
1919 in BODY."
1920 (let ((retval (gensym)))
1921 `(let ((,retval nil))
1922 (org-save-outline-visibility t
1923 (setq ,retval
1924 (progn
1925 ,@body)))
1926 ,retval)))
1929 (defun org-drill-entry ()
1930 "Present the current topic for interactive review, as in `org-drill'.
1931 Review will occur regardless of whether the topic is due for review or whether
1932 it meets the definition of a 'review topic' used by `org-drill'.
1934 Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
1935 EDIT if the user chose to exit the drill and edit the current item. Choosing
1936 the latter option leaves the drill session suspended; it can be resumed
1937 later using `org-drill-resume'.
1939 See `org-drill' for more details."
1940 (interactive)
1941 (org-drill-goto-drill-entry-heading)
1942 ;;(unless (org-part-of-drill-entry-p)
1943 ;; (error "Point is not inside a drill entry"))
1944 ;;(unless (org-at-heading-p)
1945 ;; (org-back-to-heading))
1946 (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE"))
1947 (answer-fn 'org-drill-present-default-answer)
1948 (cont nil))
1949 (org-drill-save-visibility
1950 (save-restriction
1951 (org-narrow-to-subtree)
1952 (org-show-subtree)
1953 (org-cycle-hide-drawers 'all)
1955 (let ((presentation-fn (cdr (assoc card-type org-drill-card-type-alist))))
1956 (if (listp presentation-fn)
1957 (psetq answer-fn (or (second presentation-fn)
1958 'org-drill-present-default-answer)
1959 presentation-fn (first presentation-fn)))
1960 (cond
1961 ((null presentation-fn)
1962 (message "%s:%d: Unrecognised card type '%s', skipping..."
1963 (buffer-name) (point) card-type)
1964 (sit-for 0.5)
1965 'skip)
1967 (setq cont (funcall presentation-fn))
1968 (cond
1969 ((not cont)
1970 (message "Quit")
1971 nil)
1972 ((eql cont 'edit)
1973 'edit)
1974 ((eql cont 'skip)
1975 'skip)
1977 (save-excursion
1978 (funcall answer-fn
1979 (lambda () (org-drill-reschedule)))))))))))))
1982 (defun org-drill-entries-pending-p ()
1983 (or *org-drill-again-entries*
1984 (and (not (org-drill-maximum-item-count-reached-p))
1985 (not (org-drill-maximum-duration-reached-p))
1986 (or *org-drill-new-entries*
1987 *org-drill-failed-entries*
1988 *org-drill-young-mature-entries*
1989 *org-drill-old-mature-entries*
1990 *org-drill-overdue-entries*
1991 *org-drill-again-entries*))))
1994 (defun org-drill-pending-entry-count ()
1995 (+ (length *org-drill-new-entries*)
1996 (length *org-drill-failed-entries*)
1997 (length *org-drill-young-mature-entries*)
1998 (length *org-drill-old-mature-entries*)
1999 (length *org-drill-overdue-entries*)
2000 (length *org-drill-again-entries*)))
2003 (defun org-drill-maximum-duration-reached-p ()
2004 "Returns true if the current drill session has continued past its
2005 maximum duration."
2006 (and org-drill-maximum-duration
2007 *org-drill-start-time*
2008 (> (- (float-time (current-time)) *org-drill-start-time*)
2009 (* org-drill-maximum-duration 60))))
2012 (defun org-drill-maximum-item-count-reached-p ()
2013 "Returns true if the current drill session has reached the
2014 maximum number of items."
2015 (and org-drill-maximum-items-per-session
2016 (>= (length *org-drill-done-entries*)
2017 org-drill-maximum-items-per-session)))
2020 (defun org-drill-pop-next-pending-entry ()
2021 (block org-drill-pop-next-pending-entry
2022 (let ((m nil))
2023 (while (or (null m)
2024 (not (org-drill-entry-p m)))
2025 (setq
2027 (cond
2028 ;; First priority is items we failed in a prior session.
2029 ((and *org-drill-failed-entries*
2030 (not (org-drill-maximum-item-count-reached-p))
2031 (not (org-drill-maximum-duration-reached-p)))
2032 (pop-random *org-drill-failed-entries*))
2033 ;; Next priority is overdue items.
2034 ((and *org-drill-overdue-entries*
2035 (not (org-drill-maximum-item-count-reached-p))
2036 (not (org-drill-maximum-duration-reached-p)))
2037 ;; We use `pop', not `pop-random', because we have already
2038 ;; sorted overdue items into a random order which takes
2039 ;; number of days overdue into account.
2040 (pop *org-drill-overdue-entries*))
2041 ;; Next priority is 'young' items.
2042 ((and *org-drill-young-mature-entries*
2043 (not (org-drill-maximum-item-count-reached-p))
2044 (not (org-drill-maximum-duration-reached-p)))
2045 (pop-random *org-drill-young-mature-entries*))
2046 ;; Next priority is newly added items, and older entries.
2047 ;; We pool these into a single group.
2048 ((and (or *org-drill-new-entries*
2049 *org-drill-old-mature-entries*)
2050 (not (org-drill-maximum-item-count-reached-p))
2051 (not (org-drill-maximum-duration-reached-p)))
2052 (cond
2053 ((< (random* (+ (length *org-drill-new-entries*)
2054 (length *org-drill-old-mature-entries*)))
2055 (length *org-drill-new-entries*))
2056 (pop-random *org-drill-new-entries*))
2058 (pop-random *org-drill-old-mature-entries*))))
2059 ;; After all the above are done, last priority is items
2060 ;; that were failed earlier THIS SESSION.
2061 (*org-drill-again-entries*
2062 (pop *org-drill-again-entries*))
2063 (t ; nothing left -- return nil
2064 (return-from org-drill-pop-next-pending-entry nil)))))
2065 m)))
2068 (defun org-drill-entries (&optional resuming-p)
2069 "Returns nil, t, or a list of markers representing entries that were
2070 'failed' and need to be presented again before the session ends.
2072 RESUMING-P is true if we are resuming a suspended drill session."
2073 (block org-drill-entries
2074 (while (org-drill-entries-pending-p)
2075 (let ((m (cond
2076 ((or (not resuming-p)
2077 (null *org-drill-current-item*)
2078 (not (org-drill-entry-p *org-drill-current-item*)))
2079 (org-drill-pop-next-pending-entry))
2080 (t ; resuming a suspended session.
2081 (setq resuming-p nil)
2082 *org-drill-current-item*))))
2083 (setq *org-drill-current-item* m)
2084 (unless m
2085 (error "Unexpectedly ran out of pending drill items"))
2086 (save-excursion
2087 (org-drill-goto-entry m)
2088 (cond
2089 ((not (org-drill-entry-due-p))
2090 ;; The entry is not due anymore. This could arise if the user
2091 ;; suspends a drill session, then drills an individual entry,
2092 ;; then resumes the session.
2093 (message "Entry no longer due, skipping...")
2094 (sit-for 0.3)
2095 nil)
2097 (setq result (org-drill-entry))
2098 (cond
2099 ((null result)
2100 (message "Quit")
2101 (setq end-pos :quit)
2102 (return-from org-drill-entries nil))
2103 ((eql result 'edit)
2104 (setq end-pos (point-marker))
2105 (return-from org-drill-entries nil))
2106 ((eql result 'skip)
2107 nil) ; skip this item
2109 (cond
2110 ((<= result org-drill-failure-quality)
2111 (if *org-drill-again-entries*
2112 (setq *org-drill-again-entries*
2113 (shuffle-list *org-drill-again-entries*)))
2114 (push-end m *org-drill-again-entries*))
2116 (push m *org-drill-done-entries*))))))))))))
2120 (defun org-drill-final-report ()
2121 (let ((pass-percent
2122 (round (* 100 (count-if (lambda (qual)
2123 (> qual org-drill-failure-quality))
2124 *org-drill-session-qualities*))
2125 (max 1 (length *org-drill-session-qualities*))))
2126 (prompt nil))
2127 (setq prompt
2128 (format
2129 "%d items reviewed. Session duration %s.
2130 Recall of reviewed items:
2131 Excellent (5): %3d%% | Near miss (2): %3d%%
2132 Good (4): %3d%% | Failure (1): %3d%%
2133 Hard (3): %3d%% | Abject failure (0): %3d%%
2135 You successfully recalled %d%% of reviewed items (quality > %s)
2136 %d/%d items still await review (%s, %s, %s, %s, %s).
2137 Tomorrow, %d more items will become due for review.
2138 Session finished. Press a key to continue..."
2139 (length *org-drill-done-entries*)
2140 (format-seconds "%h:%.2m:%.2s"
2141 (- (float-time (current-time)) *org-drill-start-time*))
2142 (round (* 100 (count 5 *org-drill-session-qualities*))
2143 (max 1 (length *org-drill-session-qualities*)))
2144 (round (* 100 (count 2 *org-drill-session-qualities*))
2145 (max 1 (length *org-drill-session-qualities*)))
2146 (round (* 100 (count 4 *org-drill-session-qualities*))
2147 (max 1 (length *org-drill-session-qualities*)))
2148 (round (* 100 (count 1 *org-drill-session-qualities*))
2149 (max 1 (length *org-drill-session-qualities*)))
2150 (round (* 100 (count 3 *org-drill-session-qualities*))
2151 (max 1 (length *org-drill-session-qualities*)))
2152 (round (* 100 (count 0 *org-drill-session-qualities*))
2153 (max 1 (length *org-drill-session-qualities*)))
2154 pass-percent
2155 org-drill-failure-quality
2156 (org-drill-pending-entry-count)
2157 (+ (org-drill-pending-entry-count)
2158 *org-drill-dormant-entry-count*)
2159 (propertize
2160 (format "%d failed"
2161 (+ (length *org-drill-failed-entries*)
2162 (length *org-drill-again-entries*)))
2163 'face `(:foreground ,org-drill-failed-count-color))
2164 (propertize
2165 (format "%d overdue"
2166 (length *org-drill-overdue-entries*))
2167 'face `(:foreground ,org-drill-failed-count-color))
2168 (propertize
2169 (format "%d new"
2170 (length *org-drill-new-entries*))
2171 'face `(:foreground ,org-drill-new-count-color))
2172 (propertize
2173 (format "%d young"
2174 (length *org-drill-young-mature-entries*))
2175 'face `(:foreground ,org-drill-mature-count-color))
2176 (propertize
2177 (format "%d old"
2178 (length *org-drill-old-mature-entries*))
2179 'face `(:foreground ,org-drill-mature-count-color))
2180 *org-drill-due-tomorrow-count*
2183 (while (not (input-pending-p))
2184 (message "%s" prompt)
2185 (sit-for 0.5))
2186 (read-char-exclusive)
2188 (if (and *org-drill-session-qualities*
2189 (< pass-percent (- 100 org-drill-forgetting-index)))
2190 (read-char-exclusive
2191 (format
2193 You failed %d%% of the items you reviewed during this session.
2194 %d (%d%%) of all items scanned were overdue.
2196 Are you keeping up with your items, and reviewing them
2197 when they are scheduled? If so, you may want to consider
2198 lowering the value of `org-drill-learn-fraction' slightly in
2199 order to make items appear more frequently over time."
2200 (propertize "WARNING!" 'face 'org-warning)
2201 (- 100 pass-percent)
2202 *org-drill-overdue-entry-count*
2203 (round (* 100 *org-drill-overdue-entry-count*)
2204 (+ *org-drill-dormant-entry-count*
2205 *org-drill-due-entry-count*)))
2206 ))))
2210 (defun org-drill-free-markers (markers)
2211 "MARKERS is a list of markers, all of which will be freed (set to
2212 point nowhere). Alternatively, MARKERS can be 't', in which case
2213 all the markers used by Org-Drill will be freed."
2214 (dolist (m (if (eql t markers)
2215 (append *org-drill-done-entries*
2216 *org-drill-new-entries*
2217 *org-drill-failed-entries*
2218 *org-drill-again-entries*
2219 *org-drill-overdue-entries*
2220 *org-drill-young-mature-entries*
2221 *org-drill-old-mature-entries*)
2222 markers))
2223 (free-marker m)))
2226 (defun org-drill-order-overdue-entries (overdue-data)
2227 (setq *org-drill-overdue-entries*
2228 (mapcar 'car
2229 (sort (shuffle-list overdue-data)
2230 (lambda (a b) (> (cdr a) (cdr b)))))))
2233 (defun org-drill-entry-status ()
2234 "Returns a list (STATUS DUE) where DUE is the number of days overdue,
2235 zero being due today, -1 being scheduled 1 day in the future. STATUS is
2236 one of the following values:
2237 - nil, if the item is not a drill entry, or has an empty body
2238 - :unscheduled
2239 - :future
2240 - :new
2241 - :failed
2242 - :overdue
2243 - :young
2244 - :old
2246 (save-excursion
2247 (unless (org-at-heading-p)
2248 (org-back-to-heading))
2249 (let ((due (org-drill-entry-days-overdue))
2250 (last-int (org-drill-entry-last-interval 1)))
2251 (list
2252 (cond
2253 ((not (org-drill-entry-p))
2254 nil)
2255 ((org-drill-entry-empty-p)
2256 nil) ; skip -- item body is empty
2257 ((null due) ; unscheduled - usually a skipped leech
2258 :unscheduled)
2259 ;; ((eql -1 due)
2260 ;; :tomorrow)
2261 ((minusp due) ; scheduled in the future
2262 :future)
2263 ;; The rest of the stati all denote 'due' items ==========================
2264 ((<= (org-drill-entry-last-quality 9999)
2265 org-drill-failure-quality)
2266 ;; Mature entries that were failed last time are
2267 ;; FAILED, regardless of how young, old or overdue
2268 ;; they are.
2269 :failed)
2270 ((org-drill-entry-new-p)
2271 :new)
2272 ((org-drill-entry-overdue-p due last-int)
2273 ;; Overdue status overrides young versus old
2274 ;; distinction.
2275 ;; Store marker + due, for sorting of overdue entries
2276 :overdue)
2277 ((<= (org-drill-entry-last-interval 9999)
2278 org-drill-days-before-old)
2279 :young)
2281 :old))
2282 due))))
2285 (defun org-drill-progress-message (collected scanned)
2286 (when (zerop (% scanned 50))
2287 (let* ((meter-width 40)
2288 (sym1 (if (oddp (floor scanned (* 50 meter-width))) ?| ?.))
2289 (sym2 (if (eql sym1 ?.) ?| ?.)))
2290 (message "Collecting due drill items:%4d %s%s"
2291 collected
2292 (make-string (% (ceiling scanned 50) meter-width)
2293 sym2)
2294 (make-string (- meter-width (% (ceiling scanned 50) meter-width))
2295 sym1)))))
2298 (defun org-drill (&optional scope resume-p)
2299 "Begin an interactive 'drill session'. The user is asked to
2300 review a series of topics (headers). Each topic is initially
2301 presented as a 'question', often with part of the topic content
2302 hidden. The user attempts to recall the hidden information or
2303 answer the question, then presses a key to reveal the answer. The
2304 user then rates his or her recall or performance on that
2305 topic. This rating information is used to reschedule the topic
2306 for future review.
2308 Org-drill proceeds by:
2310 - Finding all topics (headings) in SCOPE which have either been
2311 used and rescheduled before, or which have a tag that matches
2312 `org-drill-question-tag'.
2314 - All matching topics which are either unscheduled, or are
2315 scheduled for the current date or a date in the past, are
2316 considered to be candidates for the drill session.
2318 - If `org-drill-maximum-items-per-session' is set, a random
2319 subset of these topics is presented. Otherwise, all of the
2320 eligible topics will be presented.
2322 SCOPE determines the scope in which to search for
2323 questions. It accepts the same values as `org-drill-scope',
2324 which see.
2326 If RESUME-P is non-nil, resume a suspended drill session rather
2327 than starting a new one."
2329 (interactive)
2330 (let ((end-pos nil)
2331 (overdue-data nil)
2332 (cnt 0))
2333 (block org-drill
2334 (unless resume-p
2335 (org-drill-free-markers t)
2336 (setq *org-drill-current-item* nil
2337 *org-drill-done-entries* nil
2338 *org-drill-dormant-entry-count* 0
2339 *org-drill-due-entry-count* 0
2340 *org-drill-due-tomorrow-count* 0
2341 *org-drill-overdue-entry-count* 0
2342 *org-drill-new-entries* nil
2343 *org-drill-overdue-entries* nil
2344 *org-drill-young-mature-entries* nil
2345 *org-drill-old-mature-entries* nil
2346 *org-drill-failed-entries* nil
2347 *org-drill-again-entries* nil)
2348 (setq *org-drill-session-qualities* nil)
2349 (setq *org-drill-start-time* (float-time (current-time))))
2350 (setq *random-state* (make-random-state t)) ; reseed RNG
2351 (unwind-protect
2352 (save-excursion
2353 (unless resume-p
2354 (let ((org-trust-scanner-tags t)
2355 (warned-about-id-creation nil))
2356 (org-map-drill-entries
2357 (lambda ()
2358 (org-drill-progress-message
2359 (+ (length *org-drill-new-entries*)
2360 (length *org-drill-overdue-entries*)
2361 (length *org-drill-young-mature-entries*)
2362 (length *org-drill-old-mature-entries*)
2363 (length *org-drill-failed-entries*))
2364 (incf cnt))
2365 (cond
2366 ((not (org-drill-entry-p))
2367 nil) ; skip
2369 (when (and (not warned-about-id-creation)
2370 (null (org-id-get)))
2371 (message (concat "Creating unique IDs for items "
2372 "(slow, but only happens once)"))
2373 (sit-for 0.5)
2374 (setq warned-about-id-creation t))
2375 (org-id-get-create) ; ensure drill entry has unique ID
2376 (destructuring-bind (status due) (org-drill-entry-status)
2377 (case status
2378 (:unscheduled
2379 (incf *org-drill-dormant-entry-count*))
2380 ;; (:tomorrow
2381 ;; (incf *org-drill-dormant-entry-count*)
2382 ;; (incf *org-drill-due-tomorrow-count*))
2383 (:future
2384 (incf *org-drill-dormant-entry-count*)
2385 (if (eq -1 due)
2386 (incf *org-drill-due-tomorrow-count*)))
2387 (:new
2388 (push (point-marker) *org-drill-new-entries*))
2389 (:failed
2390 (push (point-marker) *org-drill-failed-entries*))
2391 (:young
2392 (push (point-marker) *org-drill-young-mature-entries*))
2393 (:overdue
2394 (push (cons (point-marker) due) overdue-data))
2395 (:old
2396 (push (point-marker) *org-drill-old-mature-entries*)))))))
2397 scope)
2398 ;; (let ((due (org-drill-entry-days-overdue))
2399 ;; (last-int (org-drill-entry-last-interval 1)))
2400 ;; (cond
2401 ;; ((org-drill-entry-empty-p)
2402 ;; nil) ; skip -- item body is empty
2403 ;; ((or (null due) ; unscheduled - usually a skipped leech
2404 ;; (minusp due)) ; scheduled in the future
2405 ;; (incf *org-drill-dormant-entry-count*)
2406 ;; (if (eq -1 due)
2407 ;; (incf *org-drill-due-tomorrow-count*)))
2408 ;; ((org-drill-entry-new-p)
2409 ;; (push (point-marker) *org-drill-new-entries*))
2410 ;; ((<= (org-drill-entry-last-quality 9999)
2411 ;; org-drill-failure-quality)
2412 ;; ;; Mature entries that were failed last time are
2413 ;; ;; FAILED, regardless of how young, old or overdue
2414 ;; ;; they are.
2415 ;; (push (point-marker) *org-drill-failed-entries*))
2416 ;; ((org-drill-entry-overdue-p due last-int)
2417 ;; ;; Overdue status overrides young versus old
2418 ;; ;; distinction.
2419 ;; ;; Store marker + due, for sorting of overdue entries
2420 ;; (push (cons (point-marker) due) overdue-data))
2421 ;; ((<= (org-drill-entry-last-interval 9999)
2422 ;; org-drill-days-before-old)
2423 ;; ;; Item is 'young'.
2424 ;; (push (point-marker)
2425 ;; *org-drill-young-mature-entries*))
2426 ;; (t
2427 ;; (push (point-marker)
2428 ;; *org-drill-old-mature-entries*))))
2429 ;; Order 'overdue' items so that the most overdue will tend to
2430 ;; come up for review first, while keeping exact order random
2431 (org-drill-order-overdue-entries overdue-data)
2432 (setq *org-drill-overdue-entry-count*
2433 (length *org-drill-overdue-entries*))))
2434 (setq *org-drill-due-entry-count* (org-drill-pending-entry-count))
2435 (cond
2436 ((and (null *org-drill-new-entries*)
2437 (null *org-drill-failed-entries*)
2438 (null *org-drill-overdue-entries*)
2439 (null *org-drill-young-mature-entries*)
2440 (null *org-drill-old-mature-entries*))
2441 (message "I did not find any pending drill items."))
2443 (org-drill-entries resume-p)
2444 (message "Drill session finished!"))))
2445 (progn
2446 (unless end-pos
2447 (org-drill-free-markers *org-drill-done-entries*)))))
2448 (cond
2449 (end-pos
2450 (when (markerp end-pos)
2451 (org-drill-goto-entry end-pos))
2452 (let ((keystr (command-keybinding-to-string 'org-drill-resume)))
2453 (message
2454 "You can continue the drill session with the command `org-drill-resume'.%s"
2455 (if keystr (format "\nYou can run this command by pressing %s." keystr)
2456 ""))))
2458 (org-drill-final-report)
2459 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
2460 (org-drill-save-optimal-factor-matrix))
2461 (if org-drill-save-buffers-after-drill-sessions-p
2462 (save-some-buffers))
2463 (message "Drill session finished!")
2464 ))))
2467 (defun org-drill-save-optimal-factor-matrix ()
2468 (message "Saving optimal factor matrix...")
2469 (customize-save-variable 'org-drill-optimal-factor-matrix
2470 org-drill-optimal-factor-matrix))
2473 (defun org-drill-cram (&optional scope)
2474 "Run an interactive drill session in 'cram mode'. In cram mode,
2475 all drill items are considered to be due for review, unless they
2476 have been reviewed within the last `org-drill-cram-hours'
2477 hours."
2478 (interactive)
2479 (let ((*org-drill-cram-mode* t))
2480 (org-drill scope)))
2483 (defun org-drill-tree ()
2484 "Run an interactive drill session using drill items within the
2485 subtree at point."
2486 (interactive)
2487 (org-drill 'tree))
2490 (defun org-drill-directory ()
2491 "Run an interactive drill session using drill items from all org
2492 files in the same directory as the current file."
2493 (interactive)
2494 (org-drill 'directory))
2497 (defun org-drill-again (&optional scope)
2498 "Run a new drill session, but try to use leftover due items that
2499 were not reviewed during the last session, rather than scanning for
2500 unreviewed items. If there are no leftover items in memory, a full
2501 scan will be performed."
2502 (interactive)
2503 (cond
2504 ((plusp (org-drill-pending-entry-count))
2505 (org-drill-free-markers *org-drill-done-entries*)
2506 (if (markerp *org-drill-current-item*)
2507 (free-marker *org-drill-current-item*))
2508 (setq *org-drill-start-time* (float-time (current-time))
2509 *org-drill-done-entries* nil
2510 *org-drill-current-item* nil)
2511 (org-drill scope t))
2513 (org-drill scope))))
2517 (defun org-drill-resume ()
2518 "Resume a suspended drill session. Sessions are suspended by
2519 exiting them with the `edit' or `quit' options."
2520 (interactive)
2521 (cond
2522 ((org-drill-entries-pending-p)
2523 (org-drill nil t))
2524 ((and (plusp (org-drill-pending-entry-count))
2525 ;; Current drill session is finished, but there are still
2526 ;; more items which need to be reviewed.
2527 (y-or-n-p (format
2528 "You have finished the drill session. However, %d items still
2529 need reviewing. Start a new drill session? "
2530 (org-drill-pending-entry-count))))
2531 (org-drill-again))
2533 (message "You have finished the drill session."))))
2536 (defun org-drill-strip-entry-data ()
2537 (dolist (prop org-drill-scheduling-properties)
2538 (org-delete-property prop))
2539 (org-schedule t))
2542 (defun org-drill-strip-all-data (&optional scope)
2543 "Delete scheduling data from every drill entry in scope. This
2544 function may be useful if you want to give your collection of
2545 entries to someone else. Scope defaults to the current buffer,
2546 and is specified by the argument SCOPE, which accepts the same
2547 values as `org-drill-scope'."
2548 (interactive)
2549 (when (yes-or-no-p
2550 "Delete scheduling data from ALL items in scope: are you sure?")
2551 (cond
2552 ((null scope)
2553 ;; Scope is the current buffer. This means we can use
2554 ;; `org-delete-property-globally', which is faster.
2555 (dolist (prop org-drill-scheduling-properties)
2556 (org-delete-property-globally prop))
2557 (org-map-drill-entries (lambda () (org-schedule t)) scope))
2559 (org-map-drill-entries 'org-drill-strip-entry-data scope)))
2560 (message "Done.")))
2564 (defun org-drill-add-cloze-fontification ()
2565 (when org-drill-use-visible-cloze-face-p
2566 (font-lock-add-keywords 'org-mode
2567 org-drill-cloze-keywords
2568 nil)))
2570 (add-hook 'org-mode-hook 'org-drill-add-cloze-fontification)
2572 (org-drill-add-cloze-fontification)
2575 ;;; Synching card collections =================================================
2578 (defvar *org-drill-dest-id-table* (make-hash-table :test 'equal))
2581 (defun org-drill-copy-entry-to-other-buffer (dest &optional path)
2582 "Copy the subtree at point to the buffer DEST. The copy will receive
2583 the tag 'imported'."
2584 (block org-drill-copy-entry-to-other-buffer
2585 (save-excursion
2586 (let ((src (current-buffer))
2587 (m nil))
2588 (flet ((paste-tree-here (&optional level)
2589 (org-paste-subtree level)
2590 (org-drill-strip-entry-data)
2591 (org-toggle-tag "imported" 'on)
2592 (org-map-drill-entries
2593 (lambda ()
2594 (let ((id (org-id-get)))
2595 (org-drill-strip-entry-data)
2596 (unless (gethash id *org-drill-dest-id-table*)
2597 (puthash id (point-marker)
2598 *org-drill-dest-id-table*))))
2599 'tree)))
2600 (unless path
2601 (setq path (org-get-outline-path)))
2602 (org-copy-subtree)
2603 (org-pop-to-buffer-same-window dest)
2604 (setq m
2605 (condition-case nil
2606 (org-find-olp path t)
2607 (error ; path does not exist in DEST
2608 (return-from org-drill-copy-entry-to-other-buffer
2609 (cond
2610 ((cdr path)
2611 (org-drill-copy-entry-to-other-buffer
2612 dest (butlast path)))
2614 ;; We've looked all the way up the path
2615 ;; Default to appending to the end of DEST
2616 (goto-char (point-max))
2617 (newline)
2618 (paste-tree-here)))))))
2619 (goto-char m)
2620 (outline-next-heading)
2621 (newline)
2622 (forward-line -1)
2623 (paste-tree-here (1+ (or (org-current-level) 0)))
2624 )))))
2628 (defun org-drill-merge-buffers (src &optional dest ignore-new-items-p)
2629 "SRC and DEST are two org mode buffers containing drill items.
2630 For each drill item in DEST that shares an ID with an item in SRC,
2631 overwrite scheduling data in DEST with data taken from the item in SRC.
2632 This is intended for use when two people are sharing a set of drill items,
2633 one person has made some updates to the item set, and the other person
2634 wants to migrate to the updated set without losing their scheduling data.
2636 By default, any drill items in SRC which do not exist in DEST are
2637 copied into DEST. We attempt to place the copied item in the
2638 equivalent location in DEST to its location in SRC, by matching
2639 the heading hierarchy. However if IGNORE-NEW-ITEMS-P is non-nil,
2640 we simply ignore any items that do not exist in DEST, and do not
2641 copy them across."
2642 (interactive "bImport scheduling info from which buffer?")
2643 (unless dest
2644 (setq dest (current-buffer)))
2645 (setq src (get-buffer src)
2646 dest (get-buffer dest))
2647 (when (yes-or-no-p
2648 (format
2649 (concat "About to overwrite all scheduling data for drill items in `%s' "
2650 "with information taken from matching items in `%s'. Proceed? ")
2651 (buffer-name dest) (buffer-name src)))
2652 ;; Compile list of all IDs in the destination buffer.
2653 (clrhash *org-drill-dest-id-table*)
2654 (with-current-buffer dest
2655 (org-map-drill-entries
2656 (lambda ()
2657 (let ((this-id (org-id-get)))
2658 (when this-id
2659 (puthash this-id (point-marker) *org-drill-dest-id-table*))))
2660 'file))
2661 ;; Look through all entries in source buffer.
2662 (with-current-buffer src
2663 (org-map-drill-entries
2664 (lambda ()
2665 (let ((id (org-id-get))
2666 (last-quality nil) (last-reviewed nil)
2667 (scheduled-time nil))
2668 (cond
2669 ((or (null id)
2670 (not (org-drill-entry-p)))
2671 nil)
2672 ((gethash id *org-drill-dest-id-table*)
2673 ;; This entry matches an entry in dest. Retrieve all its
2674 ;; scheduling data, then go to the matching location in dest
2675 ;; and write the data.
2676 (let ((marker (gethash id *org-drill-dest-id-table*)))
2677 (destructuring-bind (last-interval repetitions failures
2678 total-repeats meanq ease)
2679 (org-drill-get-item-data)
2680 (setq last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")
2681 last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")
2682 scheduled-time (org-get-scheduled-time (point)))
2683 (save-excursion
2684 ;; go to matching entry in destination buffer
2685 (org-pop-to-buffer-same-window (marker-buffer marker))
2686 (goto-char marker)
2687 (org-drill-strip-entry-data)
2688 (unless (zerop total-repeats)
2689 (org-drill-store-item-data last-interval repetitions failures
2690 total-repeats meanq ease)
2691 (if last-quality
2692 (org-set-property "LAST_QUALITY" last-quality)
2693 (org-delete-property "LAST_QUALITY"))
2694 (if last-reviewed
2695 (org-set-property "LAST_REVIEWED" last-reviewed)
2696 (org-delete-property "LAST_REVIEWED"))
2697 (if scheduled-time
2698 (org-schedule nil scheduled-time)))))
2699 (remhash id *org-drill-dest-id-table*)
2700 (free-marker marker)))
2702 ;; item in SRC has ID, but no matching ID in DEST.
2703 ;; It must be a new item that does not exist in DEST.
2704 ;; Copy the entire item to the *end* of DEST.
2705 (unless ignore-new-items-p
2706 (org-drill-copy-entry-to-other-buffer dest))))))
2707 'file))
2708 ;; Finally: there may be some items in DEST which are not in SRC, and
2709 ;; which have been scheduled by another user of DEST. Clear out the
2710 ;; scheduling info from all the unmatched items in DEST.
2711 (with-current-buffer dest
2712 (maphash (lambda (id m)
2713 (goto-char m)
2714 (org-drill-strip-entry-data)
2715 (free-marker m))
2716 *org-drill-dest-id-table*))))
2720 ;;; Card types for learning languages =========================================
2722 ;;; Get spell-number.el from:
2723 ;;; http://www.emacswiki.org/emacs/spell-number.el
2724 (autoload 'spelln-integer-in-words "spell-number")
2727 ;;; `conjugate' card type =====================================================
2728 ;;; See spanish.org for usage
2730 (defvar org-drill-verb-tense-alist
2731 '(("present" "tomato")
2732 ("simple present" "tomato")
2733 ("present indicative" "tomato")
2734 ;; past tenses
2735 ("past" "purple")
2736 ("simple past" "purple")
2737 ("preterite" "purple")
2738 ("imperfect" "darkturquoise")
2739 ("present perfect" "royalblue")
2740 ;; future tenses
2741 ("future" "green"))
2742 "Alist where each entry has the form (TENSE COLOUR), where
2743 TENSE is a string naming a tense in which verbs can be
2744 conjugated, and COLOUR is a string specifying a foreground colour
2745 which will be used by `org-drill-present-verb-conjugation' and
2746 `org-drill-show-answer-verb-conjugation' to fontify the verb and
2747 the name of the tense.")
2750 (defun org-drill-get-verb-conjugation-info ()
2751 "Auxiliary function used by `org-drill-present-verb-conjugation' and
2752 `org-drill-show-answer-verb-conjugation'."
2753 (let ((infinitive (org-entry-get (point) "VERB_INFINITIVE" t))
2754 (inf-hint (org-entry-get (point) "VERB_INFINITIVE_HINT" t))
2755 (translation (org-entry-get (point) "VERB_TRANSLATION" t))
2756 (tense (org-entry-get (point) "VERB_TENSE" nil))
2757 (highlight-face nil))
2758 (unless (and infinitive translation tense)
2759 (error "Missing information for verb conjugation card (%s, %s, %s) at %s"
2760 infinitive translation tense (point)))
2761 (setq tense (downcase (car (read-from-string tense)))
2762 infinitive (car (read-from-string infinitive))
2763 inf-hint (if inf-hint (car (read-from-string inf-hint)))
2764 translation (car (read-from-string translation)))
2765 (setq highlight-face
2766 (list :foreground
2767 (or (second (assoc-string tense org-drill-verb-tense-alist t))
2768 "red")))
2769 (setq infinitive (propertize infinitive 'face highlight-face))
2770 (setq translation (propertize translation 'face highlight-face))
2771 (setq tense (propertize tense 'face highlight-face))
2772 (list infinitive inf-hint translation tense)))
2775 (defun org-drill-present-verb-conjugation ()
2776 "Present a drill entry whose card type is 'conjugate'."
2777 (destructuring-bind (infinitive inf-hint translation tense)
2778 (org-drill-get-verb-conjugation-info)
2779 (org-drill-present-card-using-text
2780 (cond
2781 ((zerop (random* 2))
2782 (format "\nTranslate the verb\n\n%s\n\nand conjugate for the %s tense.\n\n"
2783 infinitive tense))
2785 (format "\nGive the verb that means\n\n%s %s\n
2786 and conjugate for the %s tense.\n\n"
2787 translation
2788 (if inf-hint (format " [HINT: %s]" inf-hint) "")
2789 tense))))))
2792 (defun org-drill-show-answer-verb-conjugation (reschedule-fn)
2793 "Show the answer for a drill item whose card type is 'conjugate'.
2794 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
2795 returns its return value."
2796 (destructuring-bind (infinitive inf-hint translation tense)
2797 (org-drill-get-verb-conjugation-info)
2798 (with-replaced-entry-heading
2799 (format "%s tense of %s ==> %s\n\n"
2800 (capitalize tense)
2801 infinitive translation)
2802 (funcall reschedule-fn))))
2805 ;;; `translate_number' card type ==============================================
2806 ;;; See spanish.org for usage
2808 (defvar *drilled-number* 0)
2809 (defvar *drilled-number-direction* 'to-english)
2811 (defun org-drill-present-translate-number ()
2812 (let ((num-min (read (org-entry-get (point) "DRILL_NUMBER_MIN")))
2813 (num-max (read (org-entry-get (point) "DRILL_NUMBER_MAX")))
2814 (language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
2815 (highlight-face 'font-lock-warning-face))
2816 (cond
2817 ((not (fboundp 'spelln-integer-in-words))
2818 (message "`spell-number.el' not loaded, skipping 'translate_number' card...")
2819 (sit-for 0.5)
2820 'skip)
2821 ((not (and (numberp num-min) (numberp num-max) language))
2822 (error "Missing language or minimum or maximum numbers for number card"))
2824 (if (> num-min num-max)
2825 (psetf num-min num-max
2826 num-max num-min))
2827 (setq *drilled-number*
2828 (+ num-min (random* (abs (1+ (- num-max num-min))))))
2829 (setq *drilled-number-direction*
2830 (if (zerop (random* 2)) 'from-english 'to-english))
2831 (org-drill-present-card-using-text
2832 (if (eql 'to-english *drilled-number-direction*)
2833 (format "\nTranslate into English:\n\n%s\n"
2834 (let ((spelln-language language))
2835 (propertize
2836 (spelln-integer-in-words *drilled-number*)
2837 'face highlight-face)))
2838 (format "\nTranslate into %s:\n\n%s\n"
2839 (capitalize (format "%s" language))
2840 (let ((spelln-language 'english-gb))
2841 (propertize
2842 (spelln-integer-in-words *drilled-number*)
2843 'face highlight-face)))))))))
2846 (defun org-drill-show-answer-translate-number (reschedule-fn)
2847 (let* ((language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
2848 (highlight-face 'font-lock-warning-face)
2849 (non-english
2850 (let ((spelln-language language))
2851 (propertize (spelln-integer-in-words *drilled-number*)
2852 'face highlight-face)))
2853 (english
2854 (let ((spelln-language 'english-gb))
2855 (propertize (spelln-integer-in-words *drilled-number*)
2856 'face 'highlight-face))))
2857 (with-replaced-entry-text
2858 (cond
2859 ((eql 'to-english *drilled-number-direction*)
2860 (format "\nThe English translation of %s is:\n\n%s\n"
2861 non-english english))
2863 (format "\nThe %s translation of %s is:\n\n%s\n"
2864 (capitalize (format "%s" language))
2865 english non-english)))
2866 (funcall reschedule-fn))))
2869 ;;; `spanish_verb' card type ==================================================
2870 ;;; Not very interesting, but included to demonstrate how a presentation
2871 ;;; function can manipulate which subheading are hidden versus shown.
2874 (defun org-drill-present-spanish-verb ()
2875 (let ((prompt nil)
2876 (reveal-headings nil))
2877 (with-hidden-comments
2878 (with-hidden-cloze-hints
2879 (with-hidden-cloze-text
2880 (case (random* 6)
2882 (org-drill-hide-all-subheadings-except '("Infinitive"))
2883 (setq prompt
2884 (concat "Translate this Spanish verb, and conjugate it "
2885 "for the *present* tense.")
2886 reveal-headings '("English" "Present Tense" "Notes")))
2888 (org-drill-hide-all-subheadings-except '("English"))
2889 (setq prompt (concat "For the *present* tense, conjugate the "
2890 "Spanish translation of this English verb.")
2891 reveal-headings '("Infinitive" "Present Tense" "Notes")))
2893 (org-drill-hide-all-subheadings-except '("Infinitive"))
2894 (setq prompt (concat "Translate this Spanish verb, and "
2895 "conjugate it for the *past* tense.")
2896 reveal-headings '("English" "Past Tense" "Notes")))
2898 (org-drill-hide-all-subheadings-except '("English"))
2899 (setq prompt (concat "For the *past* tense, conjugate the "
2900 "Spanish translation of this English verb.")
2901 reveal-headings '("Infinitive" "Past Tense" "Notes")))
2903 (org-drill-hide-all-subheadings-except '("Infinitive"))
2904 (setq prompt (concat "Translate this Spanish verb, and "
2905 "conjugate it for the *future perfect* tense.")
2906 reveal-headings '("English" "Future Perfect Tense" "Notes")))
2908 (org-drill-hide-all-subheadings-except '("English"))
2909 (setq prompt (concat "For the *future perfect* tense, conjugate the "
2910 "Spanish translation of this English verb.")
2911 reveal-headings '("Infinitive" "Future Perfect Tense" "Notes"))))
2912 (org-cycle-hide-drawers 'all)
2913 (prog1 (org-drill-presentation-prompt)
2914 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
2917 (provide 'org-drill)