contrib/lisp/: Update org-drill.el to version 2.3.7
[org-mode/org-kjn.git] / contrib / lisp / org-drill.el
blob5d8569d5c39244246cbe7f0c07226d8ee80d8418
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.7
6 ;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
7 ;;;
8 ;;;
9 ;;; Synopsis
10 ;;; ========
11 ;;;
12 ;;; Uses the SuperMemo spaced repetition algorithms to conduct interactive
13 ;;; "drill sessions", where the material to be remembered is presented to the
14 ;;; student in random order. The student rates his or her recall of each item,
15 ;;; and this information is used to schedule the item for later revision.
16 ;;;
17 ;;; Each drill session can be restricted to topics in the current buffer
18 ;;; (default), one or several files, all agenda files, or a subtree. A single
19 ;;; topic can also be drilled.
20 ;;;
21 ;;; Different "card types" can be defined, which present their information to
22 ;;; the student in different ways.
23 ;;;
24 ;;; See the file README.org 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)
41 (defcustom org-drill-question-tag
42 "drill"
43 "Tag which topics must possess in order to be identified as review topics
44 by `org-drill'."
45 :group 'org-drill
46 :type 'string)
49 (defcustom org-drill-maximum-items-per-session
51 "Each drill session will present at most this many topics for review.
52 Nil means unlimited."
53 :group 'org-drill
54 :type '(choice integer (const nil)))
58 (defcustom org-drill-maximum-duration
60 "Maximum duration of a drill session, in minutes.
61 Nil means unlimited."
62 :group 'org-drill
63 :type '(choice integer (const nil)))
66 (defcustom org-drill-failure-quality
68 "If the quality of recall for an item is this number or lower,
69 it is regarded as an unambiguous failure, and the repetition
70 interval for the card is reset to 0 days. If the quality is higher
71 than this number, it is regarded as successfully recalled, but the
72 time interval to the next repetition will be lowered if the quality
73 was near to a fail.
75 By default this is 2, for SuperMemo-like behaviour. For
76 Mnemosyne-like behaviour, set it to 1. Other values are not
77 really sensible."
78 :group 'org-drill
79 :type '(choice (const 2) (const 1)))
82 (defcustom org-drill-forgetting-index
84 "What percentage of items do you consider it is 'acceptable' to
85 forget each drill session? The default is 10%. A warning message
86 is displayed at the end of the session if the percentage forgotten
87 climbs above this number."
88 :group 'org-drill
89 :type 'integer)
92 (defcustom org-drill-leech-failure-threshold
94 "If an item is forgotten more than this many times, it is tagged
95 as a 'leech' item."
96 :group 'org-drill
97 :type '(choice integer (const nil)))
100 (defcustom org-drill-leech-method
101 'skip
102 "How should 'leech items' be handled during drill sessions?
103 Possible values:
104 - nil :: Leech items are treated the same as normal items.
105 - skip :: Leech items are not included in drill sessions.
106 - warn :: Leech items are still included in drill sessions,
107 but a warning message is printed when each leech item is
108 presented."
109 :group 'org-drill
110 :type '(choice (const 'warn) (const 'skip) (const nil)))
113 (defface org-drill-visible-cloze-face
114 '((t (:foreground "darkseagreen")))
115 "The face used to hide the contents of cloze phrases."
116 :group 'org-drill)
119 (defface org-drill-visible-cloze-hint-face
120 '((t (:foreground "dark slate blue")))
121 "The face used to hide the contents of cloze phrases."
122 :group 'org-drill)
125 (defface org-drill-hidden-cloze-face
126 '((t (:foreground "deep sky blue" :background "blue")))
127 "The face used to hide the contents of cloze phrases."
128 :group 'org-drill)
131 (defcustom org-drill-use-visible-cloze-face-p
133 "Use a special face to highlight cloze-deleted text in org mode
134 buffers?"
135 :group 'org-drill
136 :type 'boolean)
139 (defcustom org-drill-hide-item-headings-p
141 "Conceal the contents of the main heading of each item during drill
142 sessions? You may want to enable this behaviour if item headings or tags
143 contain information that could 'give away' the answer."
144 :group 'org-drill
145 :type 'boolean)
148 (defcustom org-drill-new-count-color
149 "royal blue"
150 "Foreground colour used to display the count of remaining new items
151 during a drill session."
152 :group 'org-drill
153 :type 'color)
155 (defcustom org-drill-mature-count-color
156 "green"
157 "Foreground colour used to display the count of remaining mature items
158 during a drill session. Mature items are due for review, but are not new."
159 :group 'org-drill
160 :type 'color)
162 (defcustom org-drill-failed-count-color
163 "red"
164 "Foreground colour used to display the count of remaining failed items
165 during a drill session."
166 :group 'org-drill
167 :type 'color)
169 (defcustom org-drill-done-count-color
170 "sienna"
171 "Foreground colour used to display the count of reviewed items
172 during a drill session."
173 :group 'org-drill
174 :type 'color)
177 (setplist 'org-drill-cloze-overlay-defaults
178 '(display "[...]"
179 face org-drill-hidden-cloze-face
180 window t))
182 (setplist 'org-drill-hidden-text-overlay
183 '(invisible t))
185 (setplist 'org-drill-replaced-text-overlay
186 '(display "Replaced text"
187 face default
188 window t))
191 (defvar org-drill-hint-separator "||"
192 "String which, if it occurs within a cloze expression, signifies that the
193 rest of the expression after the string is a `hint', to be displayed instead of
194 the hidden cloze during a test.")
197 (defvar org-drill-cloze-regexp
198 (concat "\\(\\[[[:cntrl:][:graph:][:space:]]+?\\)\\(\\|"
199 (regexp-quote org-drill-hint-separator)
200 ".+?\\)\\(\\]\\)"))
203 (defvar org-drill-cloze-keywords
204 `((,org-drill-cloze-regexp
205 (1 'org-drill-visible-cloze-face nil)
206 (2 'org-drill-visible-cloze-hint-face t)
207 (3 'org-drill-visible-cloze-face nil)
211 (defcustom org-drill-card-type-alist
212 '((nil org-drill-present-simple-card)
213 ("simple" org-drill-present-simple-card)
214 ("twosided" org-drill-present-two-sided-card nil t)
215 ("multisided" org-drill-present-multi-sided-card nil t)
216 ("hide1cloze" org-drill-present-multicloze-hide1)
217 ("hide2cloze" org-drill-present-multicloze-hide2)
218 ("show1cloze" org-drill-present-multicloze-show1)
219 ("show2cloze" org-drill-present-multicloze-show2)
220 ("multicloze" org-drill-present-multicloze-hide1)
221 ("hidefirst" org-drill-present-multicloze-hide-first)
222 ("hidelast" org-drill-present-multicloze-hide-last)
223 ("hide1_firstmore" org-drill-present-multicloze-hide1-firstmore)
224 ("show1_lastmore" org-drill-present-multicloze-show1-lastmore)
225 ("show1_firstless" org-drill-present-multicloze-show1-firstless)
226 ("conjugate"
227 org-drill-present-verb-conjugation
228 org-drill-show-answer-verb-conjugation)
229 ("decline_noun"
230 org-drill-present-noun-declension
231 org-drill-show-answer-noun-declension)
232 ("spanish_verb" org-drill-present-spanish-verb)
233 ("translate_number" org-drill-present-translate-number))
234 "Alist associating card types with presentation functions. Each
235 entry in the alist takes the form:
237 ;;; (CARDTYPE QUESTION-FN [ANSWER-FN DRILL-EMPTY-P])
239 Where CARDTYPE is a string or nil (for default), and QUESTION-FN
240 is a function which takes no arguments and returns a boolean
241 value.
243 When supplied, ANSWER-FN is a function that takes one argument --
244 that argument is a function of no arguments, which when called,
245 prompts the user to rate their recall and performs rescheduling
246 of the drill item. ANSWER-FN is called with the point on the
247 active item's heading, just prior to displaying the item's
248 'answer'. It can therefore be used to modify the appearance of
249 the answer. ANSWER-FN must call its argument before returning.
251 When supplied, DRILL-EMPTY-P is a boolean value, default nil.
252 When non-nil, cards of this type will be presented during tests
253 even if their bodies are empty."
254 :group 'org-drill
255 :type '(alist :key-type (choice string (const nil))
256 :value-type function))
259 (defcustom org-drill-scope
260 'file
261 "The scope in which to search for drill items when conducting a
262 drill session. This can be any of:
264 file The current buffer, respecting the restriction if any.
265 This is the default.
266 tree The subtree started with the entry at point
267 file-no-restriction The current buffer, without restriction
268 file-with-archives The current buffer, and any archives associated with it.
269 agenda All agenda files
270 agenda-with-archives All agenda files with any archive files associated
271 with them.
272 directory All files with the extension '.org' in the same
273 directory as the current file (includes the current
274 file if it is an .org file.)
275 (FILE1 FILE2 ...) If this is a list, all files in the list will be scanned.
277 ;; Note -- meanings differ slightly from the argument to org-map-entries:
278 ;; 'file' means current file/buffer, respecting any restriction
279 ;; 'file-no-restriction' means current file/buffer, ignoring restrictions
280 ;; 'directory' means all *.org files in current directory
281 :group 'org-drill
282 :type '(choice (const 'file) (const 'tree) (const 'file-no-restriction)
283 (const 'file-with-archives) (const 'agenda)
284 (const 'agenda-with-archives) (const 'directory)
285 list))
288 (defcustom org-drill-save-buffers-after-drill-sessions-p
290 "If non-nil, prompt to save all modified buffers after a drill session
291 finishes."
292 :group 'org-drill
293 :type 'boolean)
296 (defcustom org-drill-spaced-repetition-algorithm
297 'sm5
298 "Which SuperMemo spaced repetition algorithm to use for scheduling items.
299 Available choices are:
300 - SM2 :: the SM2 algorithm, used in SuperMemo 2.0
301 - SM5 :: the SM5 algorithm, used in SuperMemo 5.0
302 - Simple8 :: a modified version of the SM8 algorithm. SM8 is used in
303 SuperMemo 98. The version implemented here is simplified in that while it
304 'learns' the difficulty of each item using quality grades and number of
305 failures, it does not modify the matrix of values that
306 governs how fast the inter-repetition intervals increase. A method for
307 adjusting intervals when items are reviewed early or late has been taken
308 from SM11, a later version of the algorithm, and included in Simple8."
309 :group 'org-drill
310 :type '(choice (const 'sm2) (const 'sm5) (const 'simple8)))
313 (defcustom org-drill-optimal-factor-matrix
315 "DO NOT CHANGE THE VALUE OF THIS VARIABLE.
317 Persistent matrix of optimal factors, used by the SuperMemo SM5 algorithm.
318 The matrix is saved (using the 'customize' facility) at the end of each
319 drill session.
321 Over time, values in the matrix will adapt to the individual user's
322 pace of learning."
323 :group 'org-drill
324 :type 'sexp)
327 (defcustom org-drill-sm5-initial-interval
329 "In the SM5 algorithm, the initial interval after the first
330 successful presentation of an item is always 4 days. If you wish to change
331 this, you can do so here."
332 :group 'org-drill
333 :type 'float)
336 (defcustom org-drill-add-random-noise-to-intervals-p
338 "If true, the number of days until an item's next repetition
339 will vary slightly from the interval calculated by the SM2
340 algorithm. The variation is very small when the interval is
341 small, but scales up with the interval."
342 :group 'org-drill
343 :type 'boolean)
346 (defcustom org-drill-adjust-intervals-for-early-and-late-repetitions-p
348 "If true, when the student successfully reviews an item 1 or more days
349 before or after the scheduled review date, this will affect that date of
350 the item's next scheduled review, according to the algorithm presented at
351 [[http://www.supermemo.com/english/algsm11.htm#Advanced%20repetitions]].
353 Items that were reviewed early will have their next review date brought
354 forward. Those that were reviewed late will have their next review
355 date postponed further.
357 Note that this option currently has no effect if the SM2 algorithm
358 is used."
359 :group 'org-drill
360 :type 'boolean)
363 (defcustom org-drill-cloze-text-weight
365 "For card types 'hide1_firstmore', 'show1_lastmore' and 'show1_firstless',
366 this number determines how often the 'less favoured' situation
367 should arise. It will occur 1 in every N trials, where N is the
368 value of the variable.
370 For example, with the hide1_firstmore card type, the first piece
371 of clozed text should be hidden more often than the other
372 pieces. If this variable is set to 4 (default), the first item
373 will only be shown 25% of the time (1 in 4 trials). Similarly for
374 show1_lastmore, the last item will be shown 75% of the time, and
375 for show1_firstless, the first item would only be shown 25% of the
376 time.
378 If the value of this variable is NIL, then weighting is disabled, and
379 all weighted card types are treated as their unweighted equivalents."
380 :group 'org-drill
381 :type '(choice integer (const nil)))
384 (defcustom org-drill-cram-hours
386 "When in cram mode, items are considered due for review if
387 they were reviewed at least this many hours ago."
388 :group 'org-drill
389 :type 'integer)
392 ;;; NEW items have never been presented in a drill session before.
393 ;;; MATURE items HAVE been presented at least once before.
394 ;;; - YOUNG mature items were scheduled no more than
395 ;;; ORG-DRILL-DAYS-BEFORE-OLD days after their last
396 ;;; repetition. These items will have been learned 'recently' and will have a
397 ;;; low repetition count.
398 ;;; - OLD mature items have intervals greater than
399 ;;; ORG-DRILL-DAYS-BEFORE-OLD.
400 ;;; - OVERDUE items are past their scheduled review date by more than
401 ;;; LAST-INTERVAL * (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) days,
402 ;;; regardless of young/old status.
405 (defcustom org-drill-days-before-old
407 "When an item's inter-repetition interval rises above this value in days,
408 it is no longer considered a 'young' (recently learned) item."
409 :group 'org-drill
410 :type 'integer)
413 (defcustom org-drill-overdue-interval-factor
415 "An item is considered overdue if its scheduled review date is
416 more than (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) * LAST-INTERVAL
417 days in the past. For example, a value of 1.2 means an additional
418 20% of the last scheduled interval is allowed to elapse before
419 the item is overdue. A value of 1.0 means no extra time is
420 allowed at all - items are immediately considered overdue if
421 there is even one day's delay in reviewing them. This variable
422 should never be less than 1.0."
423 :group 'org-drill
424 :type 'float)
427 (defcustom org-drill-learn-fraction
429 "Fraction between 0 and 1 that governs how quickly the spaces
430 between successive repetitions increase, for all items. The
431 default value is 0.5. Higher values make spaces increase more
432 quickly with each successful repetition. You should only change
433 this in small increments (for example 0.05-0.1) as it has an
434 exponential effect on inter-repetition spacing."
435 :group 'org-drill
436 :type 'float)
439 (defvar drill-answer nil
440 "Global variable that can be bound to a correct answer when an
441 item is being presented. If this variable is non-nil, the default
442 presentation function will show its value instead of the default
443 behaviour of revealing the contents of the drilled item.
445 This variable is useful for card types that compute their answers
446 -- for example, a card type that asks the student to translate a
447 random number to another language. ")
450 (defvar *org-drill-session-qualities* nil)
451 (defvar *org-drill-start-time* 0)
452 (defvar *org-drill-new-entries* nil)
453 (defvar *org-drill-dormant-entry-count* 0)
454 (defvar *org-drill-due-entry-count* 0)
455 (defvar *org-drill-overdue-entry-count* 0)
456 (defvar *org-drill-due-tomorrow-count* 0)
457 (defvar *org-drill-overdue-entries* nil
458 "List of markers for items that are considered 'overdue', based on
459 the value of ORG-DRILL-OVERDUE-INTERVAL-FACTOR.")
460 (defvar *org-drill-young-mature-entries* nil
461 "List of markers for mature entries whose last inter-repetition
462 interval was <= ORG-DRILL-DAYS-BEFORE-OLD days.")
463 (defvar *org-drill-old-mature-entries* nil
464 "List of markers for mature entries whose last inter-repetition
465 interval was greater than ORG-DRILL-DAYS-BEFORE-OLD days.")
466 (defvar *org-drill-failed-entries* nil)
467 (defvar *org-drill-again-entries* nil)
468 (defvar *org-drill-done-entries* nil)
469 (defvar *org-drill-current-item* nil
470 "Set to the marker for the item currently being tested.")
471 (defvar *org-drill-cram-mode* nil
472 "Are we in 'cram mode', where all items are considered due
473 for review unless they were already reviewed in the recent past?")
474 (defvar org-drill-scheduling-properties
475 '("LEARN_DATA" "DRILL_LAST_INTERVAL" "DRILL_REPEATS_SINCE_FAIL"
476 "DRILL_TOTAL_REPEATS" "DRILL_FAILURE_COUNT" "DRILL_AVERAGE_QUALITY"
477 "DRILL_EASE" "DRILL_LAST_QUALITY" "DRILL_LAST_REVIEWED"))
480 ;;; Make the above settings safe as file-local variables.
483 (put 'org-drill-question-tag 'safe-local-variable 'stringp)
484 (put 'org-drill-maximum-items-per-session 'safe-local-variable
485 '(lambda (val) (or (integerp val) (null val))))
486 (put 'org-drill-maximum-duration 'safe-local-variable
487 '(lambda (val) (or (integerp val) (null val))))
488 (put 'org-drill-failure-quality 'safe-local-variable 'integerp)
489 (put 'org-drill-forgetting-index 'safe-local-variable 'integerp)
490 (put 'org-drill-leech-failure-threshold 'safe-local-variable 'integerp)
491 (put 'org-drill-leech-method 'safe-local-variable
492 '(lambda (val) (memq val '(nil skip warn))))
493 (put 'org-drill-use-visible-cloze-face-p 'safe-local-variable 'booleanp)
494 (put 'org-drill-hide-item-headings-p 'safe-local-variable 'booleanp)
495 (put 'org-drill-spaced-repetition-algorithm 'safe-local-variable
496 '(lambda (val) (memq val '(simple8 sm5 sm2))))
497 (put 'org-drill-sm5-initial-interval 'safe-local-variable 'floatp)
498 (put 'org-drill-add-random-noise-to-intervals-p 'safe-local-variable 'booleanp)
499 (put 'org-drill-adjust-intervals-for-early-and-late-repetitions-p
500 'safe-local-variable 'booleanp)
501 (put 'org-drill-cram-hours 'safe-local-variable 'integerp)
502 (put 'org-drill-learn-fraction 'safe-local-variable 'floatp)
503 (put 'org-drill-days-before-old 'safe-local-variable 'integerp)
504 (put 'org-drill-overdue-interval-factor 'safe-local-variable 'floatp)
505 (put 'org-drill-scope 'safe-local-variable
506 '(lambda (val) (or (symbolp val) (listp val))))
507 (put 'org-drill-save-buffers-after-drill-sessions-p 'safe-local-variable 'booleanp)
508 (put 'org-drill-cloze-text-weight 'safe-local-variable
509 '(lambda (val) (or (null val) (integerp val))))
512 ;;;; Utilities ================================================================
515 (defun free-marker (m)
516 (set-marker m nil))
519 (defmacro pop-random (place)
520 (let ((idx (gensym)))
521 `(if (null ,place)
523 (let ((,idx (random* (length ,place))))
524 (prog1 (nth ,idx ,place)
525 (setq ,place (append (subseq ,place 0 ,idx)
526 (subseq ,place (1+ ,idx)))))))))
529 (defmacro push-end (val place)
530 "Add VAL to the end of the sequence stored in PLACE. Return the new
531 value."
532 `(setq ,place (append ,place (list ,val))))
535 (defun shuffle-list (list)
536 "Randomly permute the elements of LIST (all permutations equally likely)."
537 ;; Adapted from 'shuffle-vector' in cookie1.el
538 (let ((i 0)
540 temp
541 (len (length list)))
542 (while (< i len)
543 (setq j (+ i (random* (- len i))))
544 (setq temp (nth i list))
545 (setf (nth i list) (nth j list))
546 (setf (nth j list) temp)
547 (setq i (1+ i))))
548 list)
551 (defun round-float (floatnum fix)
552 "Round the floating point number FLOATNUM to FIX decimal places.
553 Example: (round-float 3.56755765 3) -> 3.568"
554 (let ((n (expt 10 fix)))
555 (/ (float (round (* floatnum n))) n)))
558 (defun command-keybinding-to-string (cmd)
559 "Return a human-readable description of the key/keys to which the command
560 CMD is bound, or nil if it is not bound to a key."
561 (let ((key (where-is-internal cmd overriding-local-map t)))
562 (if key (key-description key))))
565 (defun time-to-inactive-org-timestamp (time)
566 (format-time-string
567 (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")
568 time))
571 (defun org-map-drill-entries (func &optional scope &rest skip)
572 "Like `org-map-entries', but only drill entries are processed."
573 (let ((org-drill-scope (or scope org-drill-scope)))
574 (apply 'org-map-entries func
575 (concat "+" org-drill-question-tag)
576 (case org-drill-scope
577 (file nil)
578 (file-no-restriction 'file)
579 (directory
580 (directory-files (file-name-directory (buffer-file-name))
581 t "\\.org$"))
582 (t org-drill-scope))
583 skip)))
586 (defmacro with-hidden-cloze-text (&rest body)
587 `(progn
588 (org-drill-hide-clozed-text)
589 (unwind-protect
590 (progn
591 ,@body)
592 (org-drill-unhide-clozed-text))))
595 (defmacro with-hidden-cloze-hints (&rest body)
596 `(progn
597 (org-drill-hide-cloze-hints)
598 (unwind-protect
599 (progn
600 ,@body)
601 (org-drill-unhide-text))))
604 (defmacro with-hidden-comments (&rest body)
605 `(progn
606 (if org-drill-hide-item-headings-p
607 (org-drill-hide-heading-at-point))
608 (org-drill-hide-comments)
609 (unwind-protect
610 (progn
611 ,@body)
612 (org-drill-unhide-text))))
615 (defun org-drill-days-since-last-review ()
616 "Nil means a last review date has not yet been stored for
617 the item.
618 Zero means it was reviewed today.
619 A positive number means it was reviewed that many days ago.
620 A negative number means the date of last review is in the future --
621 this should never happen."
622 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
623 (when datestr
624 (- (time-to-days (current-time))
625 (time-to-days (apply 'encode-time
626 (org-parse-time-string datestr)))))))
629 (defun org-drill-hours-since-last-review ()
630 "Like `org-drill-days-since-last-review', but return value is
631 in hours rather than days."
632 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
633 (when datestr
634 (floor
635 (/ (- (time-to-seconds (current-time))
636 (time-to-seconds (apply 'encode-time
637 (org-parse-time-string datestr))))
638 (* 60 60))))))
641 (defun org-drill-entry-p (&optional marker)
642 "Is MARKER, or the point, in a 'drill item'? This will return nil if
643 the point is inside a subheading of a drill item -- to handle that
644 situation use `org-part-of-drill-entry-p'."
645 (save-excursion
646 (when marker
647 (org-drill-goto-entry marker))
648 (member org-drill-question-tag (org-get-local-tags))))
651 (defun org-drill-goto-entry (marker)
652 (switch-to-buffer (marker-buffer marker))
653 (goto-char marker))
656 (defun org-part-of-drill-entry-p ()
657 "Is the current entry either the main heading of a 'drill item',
658 or a subheading within a drill item?"
659 (or (org-drill-entry-p)
660 ;; Does this heading INHERIT the drill tag
661 (member org-drill-question-tag (org-get-tags-at))))
664 (defun org-drill-goto-drill-entry-heading ()
665 "Move the point to the heading which holds the :drill: tag for this
666 drill entry."
667 (unless (org-at-heading-p)
668 (org-back-to-heading))
669 (unless (org-part-of-drill-entry-p)
670 (error "Point is not inside a drill entry"))
671 (while (not (org-drill-entry-p))
672 (unless (org-up-heading-safe)
673 (error "Cannot find a parent heading that is marked as a drill entry"))))
677 (defun org-drill-entry-leech-p ()
678 "Is the current entry a 'leech item'?"
679 (and (org-drill-entry-p)
680 (member "leech" (org-get-local-tags))))
683 ;; (defun org-drill-entry-due-p ()
684 ;; (cond
685 ;; (*org-drill-cram-mode*
686 ;; (let ((hours (org-drill-hours-since-last-review)))
687 ;; (and (org-drill-entry-p)
688 ;; (or (null hours)
689 ;; (>= hours org-drill-cram-hours)))))
690 ;; (t
691 ;; (let ((item-time (org-get-scheduled-time (point))))
692 ;; (and (org-drill-entry-p)
693 ;; (or (not (eql 'skip org-drill-leech-method))
694 ;; (not (org-drill-entry-leech-p)))
695 ;; (or (null item-time) ; not scheduled
696 ;; (not (minusp ; scheduled for today/in past
697 ;; (- (time-to-days (current-time))
698 ;; (time-to-days item-time))))))))))
701 (defun org-drill-entry-days-overdue ()
702 "Returns:
703 - NIL if the item is not to be regarded as scheduled for review at all.
704 This is the case if it is not a drill item, or if it is a leech item
705 that we wish to skip, or if we are in cram mode and have already reviewed
706 the item within the last few hours.
707 - 0 if the item is new, or if it scheduled for review today.
708 - A negative integer - item is scheduled that many days in the future.
709 - A positive integer - item is scheduled that many days in the past."
710 (cond
711 (*org-drill-cram-mode*
712 (let ((hours (org-drill-hours-since-last-review)))
713 (and (org-drill-entry-p)
714 (or (null hours)
715 (>= hours org-drill-cram-hours))
716 0)))
718 (let ((item-time (org-get-scheduled-time (point))))
719 (cond
720 ((or (not (org-drill-entry-p))
721 (and (eql 'skip org-drill-leech-method)
722 (org-drill-entry-leech-p)))
723 nil)
724 ((null item-time) ; not scheduled -> due now
727 (- (time-to-days (current-time))
728 (time-to-days item-time))))))))
731 (defun org-drill-entry-overdue-p (&optional days-overdue last-interval)
732 "Returns true if entry that is scheduled DAYS-OVERDUE dasy in the past,
733 and whose last inter-repetition interval was LAST-INTERVAL, should be
734 considered 'overdue'. If the arguments are not given they are extracted
735 from the entry at point."
736 (unless days-overdue
737 (setq days-overdue (org-drill-entry-days-overdue)))
738 (unless last-interval
739 (setq last-interval (org-drill-entry-last-interval 1)))
740 (and (numberp days-overdue)
741 (> days-overdue 1) ; enforce a sane minimum 'overdue' gap
742 ;;(> due org-drill-days-before-overdue)
743 (> (/ (+ days-overdue last-interval 1.0) last-interval)
744 org-drill-overdue-interval-factor)))
748 (defun org-drill-entry-due-p ()
749 (let ((due (org-drill-entry-days-overdue)))
750 (and (not (null due))
751 (not (minusp due)))))
754 (defun org-drill-entry-new-p ()
755 (and (org-drill-entry-p)
756 (let ((item-time (org-get-scheduled-time (point))))
757 (null item-time))))
760 (defun org-drill-entry-last-quality (&optional default)
761 (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
762 (if quality
763 (string-to-number quality)
764 default)))
767 (defun org-drill-entry-failure-count ()
768 (let ((quality (org-entry-get (point) "DRILL_FAILURE_COUNT")))
769 (if quality
770 (string-to-number quality)
771 0)))
774 (defun org-drill-entry-average-quality (&optional default)
775 (let ((val (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
776 (if val
777 (string-to-number val)
778 (or default nil))))
780 (defun org-drill-entry-last-interval (&optional default)
781 (let ((val (org-entry-get (point) "DRILL_LAST_INTERVAL")))
782 (if val
783 (string-to-number val)
784 (or default 0))))
786 (defun org-drill-entry-repeats-since-fail (&optional default)
787 (let ((val (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
788 (if val
789 (string-to-number val)
790 (or default 0))))
792 (defun org-drill-entry-total-repeats (&optional default)
793 (let ((val (org-entry-get (point) "DRILL_TOTAL_REPEATS")))
794 (if val
795 (string-to-number val)
796 (or default 0))))
798 (defun org-drill-entry-ease (&optional default)
799 (let ((val (org-entry-get (point) "DRILL_EASE")))
800 (if val
801 (string-to-number val)
802 default)))
805 ;;; From http://www.supermemo.com/english/ol/sm5.htm
806 (defun org-drill-random-dispersal-factor ()
807 "Returns a random number between 0.5 and 1.5."
808 (let ((a 0.047)
809 (b 0.092)
810 (p (- (random* 1.0) 0.5)))
811 (flet ((sign (n)
812 (cond ((zerop n) 0)
813 ((plusp n) 1)
814 (t -1))))
815 (/ (+ 100 (* (* (/ -1 b) (log (- 1 (* (/ b a ) (abs p)))))
816 (sign p)))
817 100.0))))
819 (defun pseudonormal (mean variation)
820 "Random numbers in a pseudo-normal distribution with mean MEAN, range
821 MEAN-VARIATION to MEAN+VARIATION"
822 (+ (random* variation)
823 (random* variation)
824 (- variation)
825 mean))
828 (defun org-drill-early-interval-factor (optimal-factor
829 optimal-interval
830 days-ahead)
831 "Arguments:
832 - OPTIMAL-FACTOR: interval-factor if the item had been tested
833 exactly when it was supposed to be.
834 - OPTIMAL-INTERVAL: interval for next repetition (days) if the item had been
835 tested exactly when it was supposed to be.
836 - DAYS-AHEAD: how many days ahead of time the item was reviewed.
838 Returns an adjusted optimal factor which should be used to
839 calculate the next interval, instead of the optimal factor found
840 in the matrix."
841 (let ((delta-ofmax (* (1- optimal-factor)
842 (/ (+ optimal-interval
843 (* 0.6 optimal-interval) -1) (1- optimal-interval)))))
844 (- optimal-factor
845 (* delta-ofmax (/ days-ahead (+ days-ahead (* 0.6 optimal-interval)))))))
848 (defun org-drill-get-item-data ()
849 "Returns a list of 6 items, containing all the stored recall
850 data for the item at point:
851 - LAST-INTERVAL is the interval in days that was used to schedule the item's
852 current review date.
853 - REPEATS is the number of items the item has been successfully recalled without
854 without any failures. It is reset to 0 upon failure to recall the item.
855 - FAILURES is the total number of times the user has failed to recall the item.
856 - TOTAL-REPEATS includes both successful and unsuccessful repetitions.
857 - AVERAGE-QUALITY is the mean quality of recall of the item over
858 all its repetitions, successful and unsuccessful.
859 - EASE is a number reflecting how easy the item is to learn. Higher is easier.
861 (let ((learn-str (org-entry-get (point) "LEARN_DATA"))
862 (repeats (org-drill-entry-total-repeats :missing)))
863 (cond
864 (learn-str
865 (let ((learn-data (or (and learn-str
866 (read learn-str))
867 (copy-list initial-repetition-state))))
868 (list (nth 0 learn-data) ; last interval
869 (nth 1 learn-data) ; repetitions
870 (org-drill-entry-failure-count)
871 (nth 1 learn-data)
872 (org-drill-entry-last-quality)
873 (nth 2 learn-data) ; EF
875 ((not (eql :missing repeats))
876 (list (org-drill-entry-last-interval)
877 (org-drill-entry-repeats-since-fail)
878 (org-drill-entry-failure-count)
879 (org-drill-entry-total-repeats)
880 (org-drill-entry-average-quality)
881 (org-drill-entry-ease)))
882 (t ; virgin item
883 (list 0 0 0 0 nil nil)))))
886 (defun org-drill-store-item-data (last-interval repeats failures
887 total-repeats meanq
888 ease)
889 "Stores the given data in the item at point."
890 (org-entry-delete (point) "LEARN_DATA")
891 (org-set-property "DRILL_LAST_INTERVAL"
892 (number-to-string (round-float last-interval 4)))
893 (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats))
894 (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats))
895 (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures))
896 (org-set-property "DRILL_AVERAGE_QUALITY"
897 (number-to-string (round-float meanq 3)))
898 (org-set-property "DRILL_EASE"
899 (number-to-string (round-float ease 3))))
903 ;;; SM2 Algorithm =============================================================
906 (defun determine-next-interval-sm2 (last-interval n ef quality
907 failures meanq total-repeats)
908 "Arguments:
909 - LAST-INTERVAL -- the number of days since the item was last reviewed.
910 - REPEATS -- the number of times the item has been successfully reviewed
911 - EF -- the 'easiness factor'
912 - QUALITY -- 0 to 5
914 Returns a list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX), where:
915 - INTERVAL is the number of days until the item should next be reviewed
916 - REPEATS is incremented by 1.
917 - EF is modified based on the recall quality for the item.
918 - OF-MATRIX is not modified."
919 (assert (> n 0))
920 (assert (and (>= quality 0) (<= quality 5)))
921 (if (<= quality org-drill-failure-quality)
922 ;; When an item is failed, its interval is reset to 0,
923 ;; but its EF is unchanged
924 (list -1 1 ef (1+ failures) meanq (1+ total-repeats)
925 org-drill-optimal-factor-matrix)
926 ;; else:
927 (let* ((next-ef (modify-e-factor ef quality))
928 (interval
929 (cond
930 ((<= n 1) 1)
931 ((= n 2)
932 (cond
933 (org-drill-add-random-noise-to-intervals-p
934 (case quality
935 (5 6)
936 (4 4)
937 (3 3)
938 (2 1)
939 (t -1)))
940 (t 6)))
941 (t (* last-interval next-ef)))))
942 (list (if org-drill-add-random-noise-to-intervals-p
943 (+ last-interval (* (- interval last-interval)
944 (org-drill-random-dispersal-factor)))
945 interval)
946 (1+ n)
947 next-ef
948 failures meanq (1+ total-repeats)
949 org-drill-optimal-factor-matrix))))
952 ;;; SM5 Algorithm =============================================================
956 (defun initial-optimal-factor-sm5 (n ef)
957 (if (= 1 n)
958 org-drill-sm5-initial-interval
959 ef))
961 (defun get-optimal-factor-sm5 (n ef of-matrix)
962 (let ((factors (assoc n of-matrix)))
963 (or (and factors
964 (let ((ef-of (assoc ef (cdr factors))))
965 (and ef-of (cdr ef-of))))
966 (initial-optimal-factor-sm5 n ef))))
969 (defun inter-repetition-interval-sm5 (last-interval n ef &optional of-matrix)
970 (let ((of (get-optimal-factor-sm5 n ef (or of-matrix
971 org-drill-optimal-factor-matrix))))
972 (if (= 1 n)
974 (* of last-interval))))
977 (defun determine-next-interval-sm5 (last-interval n ef quality
978 failures meanq total-repeats
979 of-matrix &optional delta-days)
980 (if (zerop n) (setq n 1))
981 (if (null ef) (setq ef 2.5))
982 (assert (> n 0))
983 (assert (and (>= quality 0) (<= quality 5)))
984 (unless of-matrix
985 (setq of-matrix org-drill-optimal-factor-matrix))
986 (setq of-matrix (cl-copy-tree of-matrix))
988 (setq meanq (if meanq
989 (/ (+ quality (* meanq total-repeats 1.0))
990 (1+ total-repeats))
991 quality))
993 (let ((next-ef (modify-e-factor ef quality))
994 (old-ef ef)
995 (new-of (modify-of (get-optimal-factor-sm5 n ef of-matrix)
996 quality org-drill-learn-fraction))
997 (interval nil))
998 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
999 delta-days (minusp delta-days))
1000 (setq new-of (org-drill-early-interval-factor
1001 (get-optimal-factor-sm5 n ef of-matrix)
1002 (inter-repetition-interval-sm5
1003 last-interval n ef of-matrix)
1004 delta-days)))
1006 (setq of-matrix
1007 (set-optimal-factor n next-ef of-matrix
1008 (round-float new-of 3))) ; round OF to 3 d.p.
1010 (setq ef next-ef)
1012 (cond
1013 ;; "Failed" -- reset repetitions to 0,
1014 ((<= quality org-drill-failure-quality)
1015 (list -1 1 old-ef (1+ failures) meanq (1+ total-repeats)
1016 of-matrix)) ; Not clear if OF matrix is supposed to be
1017 ; preserved
1018 ;; For a zero-based quality of 4 or 5, don't repeat
1019 ;; ((and (>= quality 4)
1020 ;; (not org-learn-always-reschedule))
1021 ;; (list 0 (1+ n) ef failures meanq
1022 ;; (1+ total-repeats) of-matrix)) ; 0 interval = unschedule
1024 (setq interval (inter-repetition-interval-sm5
1025 last-interval n ef of-matrix))
1026 (if org-drill-add-random-noise-to-intervals-p
1027 (setq interval (* interval (org-drill-random-dispersal-factor))))
1028 (list interval
1029 (1+ n)
1031 failures
1032 meanq
1033 (1+ total-repeats)
1034 of-matrix)))))
1037 ;;; Simple8 Algorithm =========================================================
1040 (defun org-drill-simple8-first-interval (failures)
1041 "Arguments:
1042 - FAILURES: integer >= 0. The total number of times the item has
1043 been forgotten, ever.
1045 Returns the optimal FIRST interval for an item which has previously been
1046 forgotten on FAILURES occasions."
1047 (* 2.4849 (exp (* -0.057 failures))))
1050 (defun org-drill-simple8-interval-factor (ease repetition)
1051 "Arguments:
1052 - EASE: floating point number >= 1.2. Corresponds to `AF' in SM8 algorithm.
1053 - REPETITION: the number of times the item has been tested.
1054 1 is the first repetition (ie the second trial).
1055 Returns:
1056 The factor by which the last interval should be
1057 multiplied to give the next interval. Corresponds to `RF' or `OF'."
1058 (+ 1.2 (* (- ease 1.2) (expt org-drill-learn-fraction (log repetition 2)))))
1061 (defun org-drill-simple8-quality->ease (quality)
1062 "Returns the ease (`AF' in the SM8 algorithm) which corresponds
1063 to a mean item quality of QUALITY."
1064 (+ (* 0.0542 (expt quality 4))
1065 (* -0.4848 (expt quality 3))
1066 (* 1.4916 (expt quality 2))
1067 (* -1.2403 quality)
1068 1.4515))
1071 (defun determine-next-interval-simple8 (last-interval repeats quality
1072 failures meanq totaln
1073 &optional delta-days)
1074 "Arguments:
1075 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1076 - REPEATS -- the number of times the item has been successfully reviewed
1077 - EASE -- the 'easiness factor'
1078 - QUALITY -- 0 to 5
1079 - DELTA-DAYS -- how many days overdue was the item when it was reviewed.
1080 0 = reviewed on the scheduled day. +N = N days overdue.
1081 -N = reviewed N days early.
1083 Returns the new item data, as a list of 6 values:
1084 - NEXT-INTERVAL
1085 - REPEATS
1086 - EASE
1087 - FAILURES
1088 - AVERAGE-QUALITY
1089 - TOTAL-REPEATS.
1090 See the documentation for `org-drill-get-item-data' for a description of these."
1091 (assert (>= repeats 0))
1092 (assert (and (>= quality 0) (<= quality 5)))
1093 (assert (or (null meanq) (and (>= meanq 0) (<= meanq 5))))
1094 (let ((next-interval nil))
1095 (setf meanq (if meanq
1096 (/ (+ quality (* meanq totaln 1.0)) (1+ totaln))
1097 quality))
1098 (cond
1099 ((<= quality org-drill-failure-quality)
1100 (incf failures)
1101 (setf repeats 0
1102 next-interval -1))
1103 ((or (zerop repeats)
1104 (zerop last-interval))
1105 (setf next-interval (org-drill-simple8-first-interval failures))
1106 (incf repeats)
1107 (incf totaln))
1109 (let* ((use-n
1110 (if (and
1111 org-drill-adjust-intervals-for-early-and-late-repetitions-p
1112 (numberp delta-days) (plusp delta-days)
1113 (plusp last-interval))
1114 (+ repeats (min 1 (/ delta-days last-interval 1.0)))
1115 repeats))
1116 (factor (org-drill-simple8-interval-factor
1117 (org-drill-simple8-quality->ease meanq) use-n))
1118 (next-int (* last-interval factor)))
1119 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1120 (numberp delta-days) (minusp delta-days))
1121 ;; The item was reviewed earlier than scheduled.
1122 (setf factor (org-drill-early-interval-factor
1123 factor next-int (abs delta-days))
1124 next-int (* last-interval factor)))
1125 (setf next-interval next-int)
1126 (incf repeats)
1127 (incf totaln))))
1128 (list
1129 (if (and org-drill-add-random-noise-to-intervals-p
1130 (plusp next-interval))
1131 (* next-interval (org-drill-random-dispersal-factor))
1132 next-interval)
1133 repeats
1134 (org-drill-simple8-quality->ease meanq)
1135 failures
1136 meanq
1137 totaln
1143 ;;; Essentially copied from `org-learn.el', but modified to
1144 ;;; optionally call the SM2 or simple8 functions.
1145 (defun org-drill-smart-reschedule (quality &optional days-ahead)
1146 "If DAYS-AHEAD is supplied it must be a positive integer. The
1147 item will be scheduled exactly this many days into the future."
1148 (let ((delta-days (- (time-to-days (current-time))
1149 (time-to-days (or (org-get-scheduled-time (point))
1150 (current-time)))))
1151 (ofmatrix org-drill-optimal-factor-matrix)
1152 ;; Entries can have weights, 1 by default. Intervals are divided by the
1153 ;; item's weight, so an item with a weight of 2 will have all intervals
1154 ;; halved, meaning you will end up reviewing it twice as often.
1155 ;; Useful for entries which randomly present any of several facts.
1156 (weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1157 (if (stringp weight)
1158 (setq weight (read weight)))
1159 (destructuring-bind (last-interval repetitions failures
1160 total-repeats meanq ease)
1161 (org-drill-get-item-data)
1162 (destructuring-bind (next-interval repetitions ease
1163 failures meanq total-repeats
1164 &optional new-ofmatrix)
1165 (case org-drill-spaced-repetition-algorithm
1166 (sm5 (determine-next-interval-sm5 last-interval repetitions
1167 ease quality failures
1168 meanq total-repeats ofmatrix))
1169 (sm2 (determine-next-interval-sm2 last-interval repetitions
1170 ease quality failures
1171 meanq total-repeats))
1172 (simple8 (determine-next-interval-simple8 last-interval repetitions
1173 quality failures meanq
1174 total-repeats
1175 delta-days)))
1176 (if (numberp days-ahead)
1177 (setq next-interval days-ahead))
1179 (if (and (null days-ahead)
1180 (numberp weight) (plusp weight)
1181 (not (minusp next-interval)))
1182 (setq next-interval
1183 (max 1.0 (+ last-interval
1184 (/ (- next-interval last-interval) weight)))))
1186 (org-drill-store-item-data next-interval repetitions failures
1187 total-repeats meanq ease)
1189 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
1190 (setq org-drill-optimal-factor-matrix new-ofmatrix))
1192 (cond
1193 ((= 0 days-ahead)
1194 (org-schedule t))
1195 ((minusp days-ahead)
1196 (org-schedule nil (current-time)))
1198 (org-schedule nil (time-add (current-time)
1199 (days-to-time
1200 (round next-interval))))))))))
1203 (defun org-drill-hypothetical-next-review-date (quality)
1204 "Returns an integer representing the number of days into the future
1205 that the current item would be scheduled, based on a recall quality
1206 of QUALITY."
1207 (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1208 (destructuring-bind (last-interval repetitions failures
1209 total-repeats meanq ease)
1210 (org-drill-get-item-data)
1211 (if (stringp weight)
1212 (setq weight (read weight)))
1213 (destructuring-bind (next-interval repetitions ease
1214 failures meanq total-repeats
1215 &optional ofmatrix)
1216 (case org-drill-spaced-repetition-algorithm
1217 (sm5 (determine-next-interval-sm5 last-interval repetitions
1218 ease quality failures
1219 meanq total-repeats
1220 org-drill-optimal-factor-matrix))
1221 (sm2 (determine-next-interval-sm2 last-interval repetitions
1222 ease quality failures
1223 meanq total-repeats))
1224 (simple8 (determine-next-interval-simple8 last-interval repetitions
1225 quality failures meanq
1226 total-repeats)))
1227 (cond
1228 ((not (plusp next-interval))
1230 ((and (numberp weight) (plusp weight))
1231 (+ last-interval
1232 (max 1.0 (/ (- next-interval last-interval) weight))))
1234 next-interval))))))
1237 (defun org-drill-hypothetical-next-review-dates ()
1238 (let ((intervals nil))
1239 (dotimes (q 6)
1240 (push (max (or (car intervals) 0)
1241 (org-drill-hypothetical-next-review-date q))
1242 intervals))
1243 (reverse intervals)))
1246 (defun org-drill-reschedule ()
1247 "Returns quality rating (0-5), or nil if the user quit."
1248 (let ((ch nil)
1249 (input nil)
1250 (next-review-dates (org-drill-hypothetical-next-review-dates)))
1251 (save-excursion
1252 (while (not (memq ch '(?q ?e ?0 ?1 ?2 ?3 ?4 ?5)))
1253 (setq input (read-key-sequence
1254 (if (eq ch ??)
1255 (format "0-2 Means you have forgotten the item.
1256 3-5 Means you have remembered the item.
1258 0 - Completely forgot.
1259 1 - Even after seeing the answer, it still took a bit to sink in.
1260 2 - After seeing the answer, you remembered it.
1261 3 - It took you awhile, but you finally remembered. (+%s days)
1262 4 - After a little bit of thought you remembered. (+%s days)
1263 5 - You remembered the item really easily. (+%s days)
1265 How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)"
1266 (round (nth 3 next-review-dates))
1267 (round (nth 4 next-review-dates))
1268 (round (nth 5 next-review-dates)))
1269 "How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)")))
1270 (cond
1271 ((stringp input)
1272 (setq ch (elt input 0)))
1273 ((and (vectorp input) (symbolp (elt input 0)))
1274 (case (elt input 0)
1275 (up (ignore-errors (forward-line -1)))
1276 (down (ignore-errors (forward-line 1)))
1277 (left (ignore-errors (backward-char)))
1278 (right (ignore-errors (forward-char)))
1279 (prior (ignore-errors (scroll-down))) ; pgup
1280 (next (ignore-errors (scroll-up))))) ; pgdn
1281 ((and (vectorp input) (listp (elt input 0))
1282 (eventp (elt input 0)))
1283 (case (car (elt input 0))
1284 (wheel-up (ignore-errors (mwheel-scroll (elt input 0))))
1285 (wheel-down (ignore-errors (mwheel-scroll (elt input 0)))))))
1286 (if (eql ch ?t)
1287 (org-set-tags-command))))
1288 (cond
1289 ((and (>= ch ?0) (<= ch ?5))
1290 (let ((quality (- ch ?0))
1291 (failures (org-drill-entry-failure-count)))
1292 (unless *org-drill-cram-mode*
1293 (save-excursion
1294 (org-drill-smart-reschedule quality
1295 (nth quality next-review-dates)))
1296 (push quality *org-drill-session-qualities*)
1297 (cond
1298 ((<= quality org-drill-failure-quality)
1299 (when org-drill-leech-failure-threshold
1300 ;;(setq failures (if failures (string-to-number failures) 0))
1301 ;; (org-set-property "DRILL_FAILURE_COUNT"
1302 ;; (format "%d" (1+ failures)))
1303 (if (> (1+ failures) org-drill-leech-failure-threshold)
1304 (org-toggle-tag "leech" 'on))))
1306 (let ((scheduled-time (org-get-scheduled-time (point))))
1307 (when scheduled-time
1308 (message "Next review in %d days"
1309 (- (time-to-days scheduled-time)
1310 (time-to-days (current-time))))
1311 (sit-for 0.5)))))
1312 (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality))
1313 (org-set-property "DRILL_LAST_REVIEWED"
1314 (time-to-inactive-org-timestamp (current-time))))
1315 quality))
1316 ((= ch ?e)
1317 'edit)
1319 nil))))
1322 ;; (defun org-drill-hide-all-subheadings-except (heading-list)
1323 ;; "Returns a list containing the position of each immediate subheading of
1324 ;; the current topic."
1325 ;; (let ((drill-entry-level (org-current-level))
1326 ;; (drill-sections nil)
1327 ;; (drill-heading nil))
1328 ;; (org-show-subtree)
1329 ;; (save-excursion
1330 ;; (org-map-entries
1331 ;; (lambda ()
1332 ;; (when (and (not (outline-invisible-p))
1333 ;; (> (org-current-level) drill-entry-level))
1334 ;; (setq drill-heading (org-get-heading t))
1335 ;; (unless (and (= (org-current-level) (1+ drill-entry-level))
1336 ;; (member drill-heading heading-list))
1337 ;; (hide-subtree))
1338 ;; (push (point) drill-sections)))
1339 ;; "" 'tree))
1340 ;; (reverse drill-sections)))
1344 (defun org-drill-hide-subheadings-if (test)
1345 "TEST is a function taking no arguments. TEST will be called for each
1346 of the immediate subheadings of the current drill item, with the point
1347 on the relevant subheading. TEST should return nil if the subheading is
1348 to be revealed, non-nil if it is to be hidden.
1349 Returns a list containing the position of each immediate subheading of
1350 the current topic."
1351 (let ((drill-entry-level (org-current-level))
1352 (drill-sections nil))
1353 (org-show-subtree)
1354 (save-excursion
1355 (org-map-entries
1356 (lambda ()
1357 (when (and (not (outline-invisible-p))
1358 (> (org-current-level) drill-entry-level))
1359 (when (or (/= (org-current-level) (1+ drill-entry-level))
1360 (funcall test))
1361 (hide-subtree))
1362 (push (point) drill-sections)))
1363 "" 'tree))
1364 (reverse drill-sections)))
1367 (defun org-drill-hide-all-subheadings-except (heading-list)
1368 (org-drill-hide-subheadings-if
1369 (lambda () (let ((drill-heading (org-get-heading t)))
1370 (not (member drill-heading heading-list))))))
1373 (defun org-drill-presentation-prompt (&rest fmt-and-args)
1374 (let* ((item-start-time (current-time))
1375 (input nil)
1376 (ch nil)
1377 (last-second 0)
1378 (mature-entry-count (+ (length *org-drill-young-mature-entries*)
1379 (length *org-drill-old-mature-entries*)
1380 (length *org-drill-overdue-entries*)))
1381 (status (first (org-drill-entry-status)))
1382 (prompt
1383 (if fmt-and-args
1384 (apply 'format
1385 (first fmt-and-args)
1386 (rest fmt-and-args))
1387 (concat "Press key for answer, "
1388 "e=edit, t=tags, s=skip, q=quit."))))
1389 (setq prompt
1390 (format "%s %s %s %s %s %s"
1391 (propertize
1392 (char-to-string
1393 (cond
1394 ((eql status :failed) ?F)
1395 (*org-drill-cram-mode* ?C)
1397 (case status
1398 (:new ?N) (:young ?Y) (:old ?o) (:overdue ?!)
1399 (t ??)))))
1400 'face `(:foreground
1401 ,(case status
1402 (:new org-drill-new-count-color)
1403 ((:young :old) org-drill-mature-count-color)
1404 ((:overdue :failed) org-drill-failed-count-color)
1405 (t org-drill-done-count-color))))
1406 (propertize
1407 (number-to-string (length *org-drill-done-entries*))
1408 'face `(:foreground ,org-drill-done-count-color)
1409 'help-echo "The number of items you have reviewed this session.")
1410 (propertize
1411 (number-to-string (+ (length *org-drill-again-entries*)
1412 (length *org-drill-failed-entries*)))
1413 'face `(:foreground ,org-drill-failed-count-color)
1414 'help-echo (concat "The number of items that you failed, "
1415 "and need to review again."))
1416 (propertize
1417 (number-to-string mature-entry-count)
1418 'face `(:foreground ,org-drill-mature-count-color)
1419 'help-echo "The number of old items due for review.")
1420 (propertize
1421 (number-to-string (length *org-drill-new-entries*))
1422 'face `(:foreground ,org-drill-new-count-color)
1423 'help-echo (concat "The number of new items that you "
1424 "have never reviewed."))
1425 prompt))
1426 (if (and (eql 'warn org-drill-leech-method)
1427 (org-drill-entry-leech-p))
1428 (setq prompt (concat
1429 (propertize "!!! LEECH ITEM !!!
1430 You seem to be having a lot of trouble memorising this item.
1431 Consider reformulating the item to make it easier to remember.\n"
1432 'face '(:foreground "red"))
1433 prompt)))
1434 (while (memq ch '(nil ?t))
1435 (setq ch nil)
1436 (while (not (input-pending-p))
1437 (let ((elapsed (time-subtract (current-time) item-start-time)))
1438 (message (concat (if (>= (time-to-seconds elapsed) (* 60 60))
1439 "++:++ "
1440 (format-time-string "%M:%S " elapsed))
1441 prompt))
1442 (sit-for 1)))
1443 (setq input (read-key-sequence nil))
1444 (if (stringp input) (setq ch (elt input 0)))
1445 (if (eql ch ?t)
1446 (org-set-tags-command)))
1447 (case ch
1448 (?q nil)
1449 (?e 'edit)
1450 (?s 'skip)
1451 (otherwise t))))
1454 (defun org-pos-in-regexp (pos regexp &optional nlines)
1455 (save-excursion
1456 (goto-char pos)
1457 (org-in-regexp regexp nlines)))
1460 (defun org-drill-hide-region (beg end &optional text)
1461 "Hide the buffer region between BEG and END with an 'invisible text'
1462 visual overlay, or with the string TEXT if it is supplied."
1463 (let ((ovl (make-overlay beg end)))
1464 (overlay-put ovl 'category
1465 'org-drill-hidden-text-overlay)
1466 (when (stringp text)
1467 (overlay-put ovl 'invisible nil)
1468 (overlay-put ovl 'face 'default)
1469 (overlay-put ovl 'display text))))
1472 (defun org-drill-hide-heading-at-point (&optional text)
1473 (unless (org-at-heading-p)
1474 (error "Point is not on a heading."))
1475 (save-excursion
1476 (let ((beg (point)))
1477 (end-of-line)
1478 (org-drill-hide-region beg (point) text))))
1481 (defun org-drill-hide-comments ()
1482 (save-excursion
1483 (while (re-search-forward "^#.*$" nil t)
1484 (org-drill-hide-region (match-beginning 0) (match-end 0)))))
1487 (defun org-drill-unhide-text ()
1488 ;; This will also unhide the item's heading.
1489 (save-excursion
1490 (dolist (ovl (overlays-in (point-min) (point-max)))
1491 (when (eql 'org-drill-hidden-text-overlay (overlay-get ovl 'category))
1492 (delete-overlay ovl)))))
1495 (defun org-drill-hide-clozed-text ()
1496 (save-excursion
1497 (while (re-search-forward org-drill-cloze-regexp nil t)
1498 ;; Don't hide org links, partly because they might contain inline
1499 ;; images which we want to keep visible
1500 (unless (save-match-data
1501 (org-pos-in-regexp (match-beginning 0)
1502 org-bracket-link-regexp 1))
1503 (org-drill-hide-matched-cloze-text)))))
1506 (defun org-drill-hide-matched-cloze-text ()
1507 "Hide the current match with a 'cloze' visual overlay."
1508 (let ((ovl (make-overlay (match-beginning 0) (match-end 0)))
1509 (hint-sep-pos (string-match-p (regexp-quote org-drill-hint-separator)
1510 (match-string 0))))
1511 (overlay-put ovl 'category
1512 'org-drill-cloze-overlay-defaults)
1513 (when (and hint-sep-pos
1514 (> hint-sep-pos 1))
1515 (let ((hint (substring-no-properties
1516 (match-string 0)
1517 (+ hint-sep-pos (length org-drill-hint-separator))
1518 (1- (length (match-string 0))))))
1519 (overlay-put
1520 ovl 'display
1521 ;; If hint is like `X...' then display [X...]
1522 ;; otherwise display [...X]
1523 (format (if (string-match-p (regexp-quote "...") hint) "[%s]" "[%s...]")
1524 hint))))))
1527 (defun org-drill-hide-cloze-hints ()
1528 (save-excursion
1529 (while (re-search-forward org-drill-cloze-regexp nil t)
1530 (unless (or (save-match-data
1531 (org-pos-in-regexp (match-beginning 0)
1532 org-bracket-link-regexp 1))
1533 (null (match-beginning 2))) ; hint subexpression matched
1534 (org-drill-hide-region (match-beginning 2) (match-end 2))))))
1537 (defmacro with-replaced-entry-text (text &rest body)
1538 "During the execution of BODY, the entire text of the current entry is
1539 concealed by an overlay that displays the string TEXT."
1540 `(progn
1541 (org-drill-replace-entry-text ,text)
1542 (unwind-protect
1543 (progn
1544 ,@body)
1545 (org-drill-unreplace-entry-text))))
1548 (defmacro with-replaced-entry-text-multi (replacements &rest body)
1549 "During the execution of BODY, the entire text of the current entry is
1550 concealed by an overlay that displays the overlays in REPLACEMENTS."
1551 `(progn
1552 (org-drill-replace-entry-text ,replacements t)
1553 (unwind-protect
1554 (progn
1555 ,@body)
1556 (org-drill-unreplace-entry-text))))
1559 (defun org-drill-replace-entry-text (text &optional multi-p)
1560 "Make an overlay that conceals the entire text of the item, not
1561 including properties or the contents of subheadings. The overlay shows
1562 the string TEXT.
1563 If MULTI-P is non-nil, TEXT must be a list of values which are legal
1564 for the `display' text property. The text of the item will be temporarily
1565 replaced by all of these items, in the order in which they appear in
1566 the list.
1567 Note: does not actually alter the item."
1568 (cond
1569 ((and multi-p
1570 (listp text))
1571 (org-drill-replace-entry-text-multi text))
1573 (let ((ovl (make-overlay (point-min)
1574 (save-excursion
1575 (outline-next-heading)
1576 (point)))))
1577 (overlay-put ovl 'category
1578 'org-drill-replaced-text-overlay)
1579 (overlay-put ovl 'display text)))))
1582 (defun org-drill-unreplace-entry-text ()
1583 (save-excursion
1584 (dolist (ovl (overlays-in (point-min) (point-max)))
1585 (when (eql 'org-drill-replaced-text-overlay (overlay-get ovl 'category))
1586 (delete-overlay ovl)))))
1589 (defun org-drill-replace-entry-text-multi (replacements)
1590 "Make overlays that conceal the entire text of the item, not
1591 including properties or the contents of subheadings. The overlay shows
1592 the string TEXT.
1593 Note: does not actually alter the item."
1594 (let ((ovl nil)
1595 (p-min (point-min))
1596 (p-max (save-excursion
1597 (outline-next-heading)
1598 (point))))
1599 (assert (>= (- p-max p-min) (length replacements)))
1600 (dotimes (i (length replacements))
1601 (setq ovl (make-overlay (+ p-min (* 2 i))
1602 (if (= i (1- (length replacements)))
1603 p-max
1604 (+ p-min (* 2 i) 1))))
1605 (overlay-put ovl 'category
1606 'org-drill-replaced-text-overlay)
1607 (overlay-put ovl 'display (nth i replacements)))))
1610 (defmacro with-replaced-entry-heading (heading &rest body)
1611 `(progn
1612 (org-drill-replace-entry-heading ,heading)
1613 (unwind-protect
1614 (progn
1615 ,@body)
1616 (org-drill-unhide-text))))
1619 (defun org-drill-replace-entry-heading (heading)
1620 "Make an overlay that conceals the heading of the item. The overlay shows
1621 the string TEXT.
1622 Note: does not actually alter the item."
1623 (org-drill-hide-heading-at-point heading))
1626 (defun org-drill-unhide-clozed-text ()
1627 (save-excursion
1628 (dolist (ovl (overlays-in (point-min) (point-max)))
1629 (when (eql 'org-drill-cloze-overlay-defaults (overlay-get ovl 'category))
1630 (delete-overlay ovl)))))
1633 (defun org-drill-get-entry-text (&optional keep-properties-p)
1634 (let ((text (org-agenda-get-some-entry-text (point-marker) 100)))
1635 (if keep-properties-p
1636 text
1637 (substring-no-properties text))))
1640 ;; (defun org-entry-empty-p ()
1641 ;; (zerop (length (org-drill-get-entry-text))))
1643 ;; This version is about 5x faster than the old version, above.
1644 (defun org-entry-empty-p ()
1645 (save-excursion
1646 (org-back-to-heading t)
1647 (let ((lim (save-excursion
1648 (outline-next-heading) (point))))
1649 (org-end-of-meta-data-and-drawers)
1650 (or (>= (point) lim)
1651 (null (re-search-forward "[[:graph:]]" lim t))))))
1653 (defun org-drill-entry-empty-p () (org-entry-empty-p))
1656 ;;; Presentation functions ====================================================
1658 ;; Each of these is called with point on topic heading. Each needs to show the
1659 ;; topic in the form of a 'question' or with some information 'hidden', as
1660 ;; appropriate for the card type. The user should then be prompted to press a
1661 ;; key. The function should then reveal either the 'answer' or the entire
1662 ;; topic, and should return t if the user chose to see the answer and rate their
1663 ;; recall, nil if they chose to quit.
1665 (defun org-drill-present-simple-card ()
1666 (with-hidden-comments
1667 (with-hidden-cloze-hints
1668 (with-hidden-cloze-text
1669 (org-drill-hide-all-subheadings-except nil)
1670 (ignore-errors
1671 (org-display-inline-images t))
1672 (org-cycle-hide-drawers 'all)
1673 (prog1 (org-drill-presentation-prompt)
1674 (org-drill-hide-subheadings-if 'org-drill-entry-p))))))
1677 (defun org-drill-present-default-answer (reschedule-fn)
1678 (cond
1679 (drill-answer
1680 (with-replaced-entry-text
1681 (format "\nAnswer:\n\n %s\n" drill-answer)
1682 (prog1
1683 (funcall reschedule-fn)
1684 (setq drill-answer nil))))
1686 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1687 (org-drill-unhide-clozed-text)
1688 (ignore-errors
1689 (org-display-inline-images t))
1690 (org-cycle-hide-drawers 'all)
1691 (with-hidden-cloze-hints
1692 (funcall reschedule-fn)))))
1695 (defun org-drill-present-two-sided-card ()
1696 (with-hidden-comments
1697 (with-hidden-cloze-hints
1698 (with-hidden-cloze-text
1699 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1700 (when drill-sections
1701 (save-excursion
1702 (goto-char (nth (random* (min 2 (length drill-sections)))
1703 drill-sections))
1704 (org-show-subtree)))
1705 (ignore-errors
1706 (org-display-inline-images t))
1707 (org-cycle-hide-drawers 'all)
1708 (prog1 (org-drill-presentation-prompt)
1709 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1713 (defun org-drill-present-multi-sided-card ()
1714 (with-hidden-comments
1715 (with-hidden-cloze-hints
1716 (with-hidden-cloze-text
1717 (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
1718 (when drill-sections
1719 (save-excursion
1720 (goto-char (nth (random* (length drill-sections)) drill-sections))
1721 (org-show-subtree)))
1722 (ignore-errors
1723 (org-display-inline-images t))
1724 (org-cycle-hide-drawers 'all)
1725 (prog1 (org-drill-presentation-prompt)
1726 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
1729 (defun org-drill-present-multicloze-hide-n (number-to-hide
1730 &optional
1731 force-show-first
1732 force-show-last
1733 force-hide-first)
1734 "Hides NUMBER-TO-HIDE pieces of text that are marked for cloze deletion,
1735 chosen at random.
1736 If NUMBER-TO-HIDE is negative, show only (ABS NUMBER-TO-HIDE) pieces,
1737 hiding all the rest.
1738 If FORCE-HIDE-FIRST is non-nil, force the first piece of text to be one of
1739 the hidden items.
1740 If FORCE-SHOW-FIRST is non-nil, never hide the first piece of text.
1741 If FORCE-SHOW-LAST is non-nil, never hide the last piece of text.
1742 If the number of text pieces in the item is less than
1743 NUMBER-TO-HIDE, then all text pieces will be hidden (except the first or last
1744 items if FORCE-SHOW-FIRST or FORCE-SHOW-LAST is non-nil)."
1745 (with-hidden-comments
1746 (with-hidden-cloze-hints
1747 (let ((item-end nil)
1748 (match-count 0)
1749 (body-start (or (cdr (org-get-property-block))
1750 (point))))
1751 (if (and force-hide-first force-show-first)
1752 (error "FORCE-HIDE-FIRST and FORCE-SHOW-FIRST are mutually exclusive"))
1753 (org-drill-hide-all-subheadings-except nil)
1754 (save-excursion
1755 (outline-next-heading)
1756 (setq item-end (point)))
1757 (save-excursion
1758 (goto-char body-start)
1759 (while (re-search-forward org-drill-cloze-regexp item-end t)
1760 (let ((in-regexp? (save-match-data
1761 (org-pos-in-regexp (match-beginning 0)
1762 org-bracket-link-regexp 1))))
1763 (unless in-regexp?
1764 (incf match-count)))))
1765 (if (minusp number-to-hide)
1766 (setq number-to-hide (+ match-count number-to-hide)))
1767 (when (plusp match-count)
1768 (let* ((positions (shuffle-list (loop for i from 1
1769 to match-count
1770 collect i)))
1771 (match-nums nil)
1772 (cnt nil))
1773 (if force-hide-first
1774 ;; Force '1' to be in the list, and to be the first item
1775 ;; in the list.
1776 (setq positions (cons 1 (remove 1 positions))))
1777 (if force-show-first
1778 (setq positions (remove 1 positions)))
1779 (if force-show-last
1780 (setq positions (remove match-count positions)))
1781 (setq match-nums
1782 (subseq positions
1783 0 (min number-to-hide (length positions))))
1784 ;; (dolist (pos-to-hide match-nums)
1785 (save-excursion
1786 (goto-char body-start)
1787 (setq cnt 0)
1788 (while (re-search-forward org-drill-cloze-regexp item-end t)
1789 (unless (save-match-data
1790 (org-pos-in-regexp (match-beginning 0)
1791 org-bracket-link-regexp 1))
1792 (incf cnt)
1793 (if (memq cnt match-nums)
1794 (org-drill-hide-matched-cloze-text)))))))
1795 ;; (loop
1796 ;; do (re-search-forward org-drill-cloze-regexp
1797 ;; item-end t pos-to-hide)
1798 ;; while (org-pos-in-regexp (match-beginning 0)
1799 ;; org-bracket-link-regexp 1))
1800 ;; (org-drill-hide-matched-cloze-text)))))
1801 (ignore-errors
1802 (org-display-inline-images t))
1803 (org-cycle-hide-drawers 'all)
1804 (prog1 (org-drill-presentation-prompt)
1805 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1806 (org-drill-unhide-clozed-text))))))
1809 (defun org-drill-present-multicloze-hide-nth (to-hide)
1810 "Hide the TO-HIDE'th piece of clozed text. 1 is the first piece. If
1811 TO-HIDE is negative, count backwards, so -1 means the last item, -2
1812 the second to last, etc."
1813 (with-hidden-comments
1814 (with-hidden-cloze-hints
1815 (let ((item-end nil)
1816 (match-count 0)
1817 (body-start (or (cdr (org-get-property-block))
1818 (point)))
1819 (cnt 0))
1820 (org-drill-hide-all-subheadings-except nil)
1821 (save-excursion
1822 (outline-next-heading)
1823 (setq item-end (point)))
1824 (save-excursion
1825 (goto-char body-start)
1826 (while (re-search-forward org-drill-cloze-regexp item-end t)
1827 (let ((in-regexp? (save-match-data
1828 (org-pos-in-regexp (match-beginning 0)
1829 org-bracket-link-regexp 1))))
1830 (unless in-regexp?
1831 (incf match-count)))))
1832 (if (minusp to-hide)
1833 (setq to-hide (+ 1 to-hide match-count)))
1834 (cond
1835 ((or (not (plusp match-count))
1836 (> to-hide match-count))
1837 nil)
1839 (save-excursion
1840 (goto-char body-start)
1841 (setq cnt 0)
1842 (while (re-search-forward org-drill-cloze-regexp item-end t)
1843 (unless (save-match-data
1844 (org-pos-in-regexp (match-beginning 0)
1845 org-bracket-link-regexp 1))
1846 (incf cnt)
1847 (if (= cnt to-hide)
1848 (org-drill-hide-matched-cloze-text)))))))
1849 (ignore-errors
1850 (org-display-inline-images t))
1851 (org-cycle-hide-drawers 'all)
1852 (prog1 (org-drill-presentation-prompt)
1853 (org-drill-hide-subheadings-if 'org-drill-entry-p)
1854 (org-drill-unhide-clozed-text))))))
1857 (defun org-drill-present-multicloze-hide1 ()
1858 "Hides one of the pieces of text that are marked for cloze deletion,
1859 chosen at random."
1860 (org-drill-present-multicloze-hide-n 1))
1863 (defun org-drill-present-multicloze-hide2 ()
1864 "Hides two of the pieces of text that are marked for cloze deletion,
1865 chosen at random."
1866 (org-drill-present-multicloze-hide-n 2))
1869 (defun org-drill-present-multicloze-hide-first ()
1870 "Hides the first piece of text that is marked for cloze deletion."
1871 (org-drill-present-multicloze-hide-nth 1))
1874 (defun org-drill-present-multicloze-hide-last ()
1875 "Hides the last piece of text that is marked for cloze deletion."
1876 (org-drill-present-multicloze-hide-nth -1))
1879 (defun org-drill-present-multicloze-hide1-firstmore ()
1880 "Commonly, hides the FIRST piece of text that is marked for
1881 cloze deletion. Uncommonly, hide one of the other pieces of text,
1882 chosen at random.
1884 The definitions of 'commonly' and 'uncommonly' are determined by
1885 the value of `org-drill-cloze-text-weight'."
1886 ;; The 'firstmore' and 'lastmore' functions used to randomly choose whether
1887 ;; to hide the 'favoured' piece of text. However even when the chance of
1888 ;; hiding it was set quite high (80%), the outcome was too unpredictable over
1889 ;; the small number of repetitions where most learning takes place for each
1890 ;; item. In other words, the actual frequency during the first 10 repetitions
1891 ;; was often very different from 80%. Hence we use modulo instead.
1892 (cond
1893 ((null org-drill-cloze-text-weight)
1894 ;; Behave as hide1cloze
1895 (org-drill-present-multicloze-hide1))
1896 ((not (and (integerp org-drill-cloze-text-weight)
1897 (plusp org-drill-cloze-text-weight)))
1898 (error "Illegal value for org-drill-cloze-text-weight: %S"
1899 org-drill-cloze-text-weight))
1900 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1901 org-drill-cloze-text-weight))
1902 ;; Uncommonly, hide any item except the first
1903 (org-drill-present-multicloze-hide-n 1 t))
1905 ;; Commonly, hide first item
1906 (org-drill-present-multicloze-hide-first))))
1909 (defun org-drill-present-multicloze-show1-lastmore ()
1910 "Commonly, hides all pieces except the last. Uncommonly, shows
1911 any random piece. The effect is similar to 'show1cloze' except
1912 that the last item is much less likely to be the item that is
1913 visible.
1915 The definitions of 'commonly' and 'uncommonly' are determined by
1916 the value of `org-drill-cloze-text-weight'."
1917 (cond
1918 ((null org-drill-cloze-text-weight)
1919 ;; Behave as show1cloze
1920 (org-drill-present-multicloze-show1))
1921 ((not (and (integerp org-drill-cloze-text-weight)
1922 (plusp org-drill-cloze-text-weight)))
1923 (error "Illegal value for org-drill-cloze-text-weight: %S"
1924 org-drill-cloze-text-weight))
1925 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1926 org-drill-cloze-text-weight))
1927 ;; Uncommonly, show any item except the last
1928 (org-drill-present-multicloze-hide-n -1 nil nil t))
1930 ;; Commonly, show the LAST item
1931 (org-drill-present-multicloze-hide-n -1 nil t))))
1934 (defun org-drill-present-multicloze-show1-firstless ()
1935 "Commonly, hides all pieces except one, where the shown piece
1936 is guaranteed NOT to be the first piece. Uncommonly, shows any
1937 random piece. The effect is similar to 'show1cloze' except that
1938 the first item is much less likely to be the item that is
1939 visible.
1941 The definitions of 'commonly' and 'uncommonly' are determined by
1942 the value of `org-drill-cloze-text-weight'."
1943 (cond
1944 ((null org-drill-cloze-text-weight)
1945 ;; Behave as show1cloze
1946 (org-drill-present-multicloze-show1))
1947 ((not (and (integerp org-drill-cloze-text-weight)
1948 (plusp org-drill-cloze-text-weight)))
1949 (error "Illegal value for org-drill-cloze-text-weight: %S"
1950 org-drill-cloze-text-weight))
1951 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
1952 org-drill-cloze-text-weight))
1953 ;; Uncommonly, show the first item
1954 (org-drill-present-multicloze-hide-n -1 t))
1956 ;; Commonly, show any item, except the first
1957 (org-drill-present-multicloze-hide-n -1 nil nil t))))
1960 (defun org-drill-present-multicloze-show1 ()
1961 "Similar to `org-drill-present-multicloze-hide1', but hides all
1962 the pieces of text that are marked for cloze deletion, except for one
1963 piece which is chosen at random."
1964 (org-drill-present-multicloze-hide-n -1))
1967 (defun org-drill-present-multicloze-show2 ()
1968 "Similar to `org-drill-present-multicloze-show1', but reveals two
1969 pieces rather than one."
1970 (org-drill-present-multicloze-hide-n -2))
1973 ;; (defun org-drill-present-multicloze-show1 ()
1974 ;; "Similar to `org-drill-present-multicloze-hide1', but hides all
1975 ;; the pieces of text that are marked for cloze deletion, except for one
1976 ;; piece which is chosen at random."
1977 ;; (with-hidden-comments
1978 ;; (with-hidden-cloze-hints
1979 ;; (let ((item-end nil)
1980 ;; (match-count 0)
1981 ;; (body-start (or (cdr (org-get-property-block))
1982 ;; (point))))
1983 ;; (org-drill-hide-all-subheadings-except nil)
1984 ;; (save-excursion
1985 ;; (outline-next-heading)
1986 ;; (setq item-end (point)))
1987 ;; (save-excursion
1988 ;; (goto-char body-start)
1989 ;; (while (re-search-forward org-drill-cloze-regexp item-end t)
1990 ;; (incf match-count)))
1991 ;; (when (plusp match-count)
1992 ;; (let ((match-to-hide (random* match-count)))
1993 ;; (save-excursion
1994 ;; (goto-char body-start)
1995 ;; (dotimes (n match-count)
1996 ;; (re-search-forward org-drill-cloze-regexp
1997 ;; item-end t)
1998 ;; (unless (= n match-to-hide)
1999 ;; (org-drill-hide-matched-cloze-text))))))
2000 ;; (org-display-inline-images t)
2001 ;; (org-cycle-hide-drawers 'all)
2002 ;; (prog1 (org-drill-presentation-prompt)
2003 ;; (org-drill-hide-subheadings-if 'org-drill-entry-p)
2004 ;; (org-drill-unhide-clozed-text))))))
2007 (defun org-drill-present-card-using-text (question &optional answer)
2008 "Present the string QUESTION as the only visible content of the card.
2009 If ANSWER is supplied, set the global variable `drill-answer' to its value."
2010 (if answer (setq drill-answer answer))
2011 (with-hidden-comments
2012 (with-replaced-entry-text
2013 (concat "\n" question)
2014 (org-drill-hide-all-subheadings-except nil)
2015 (org-cycle-hide-drawers 'all)
2016 (ignore-errors
2017 (org-display-inline-images t))
2018 (prog1 (org-drill-presentation-prompt)
2019 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
2022 (defun org-drill-present-card-using-multiple-overlays (replacements &optional answer)
2023 "TEXTS is a list of valid values for the 'display' text property.
2024 Present these overlays, in sequence, as the only
2025 visible content of the card.
2026 If ANSWER is supplied, set the global variable `drill-answer' to its value."
2027 (if answer (setq drill-answer answer))
2028 (with-hidden-comments
2029 (with-replaced-entry-text-multi
2030 replacements
2031 (org-drill-hide-all-subheadings-except nil)
2032 (org-cycle-hide-drawers 'all)
2033 (ignore-errors
2034 (org-display-inline-images t))
2035 (prog1 (org-drill-presentation-prompt)
2036 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))
2039 (defun org-drill-entry ()
2040 "Present the current topic for interactive review, as in `org-drill'.
2041 Review will occur regardless of whether the topic is due for review or whether
2042 it meets the definition of a 'review topic' used by `org-drill'.
2044 Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
2045 EDIT if the user chose to exit the drill and edit the current item. Choosing
2046 the latter option leaves the drill session suspended; it can be resumed
2047 later using `org-drill-resume'.
2049 See `org-drill' for more details."
2050 (interactive)
2051 (org-drill-goto-drill-entry-heading)
2052 ;;(unless (org-part-of-drill-entry-p)
2053 ;; (error "Point is not inside a drill entry"))
2054 ;;(unless (org-at-heading-p)
2055 ;; (org-back-to-heading))
2056 (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE"))
2057 (answer-fn 'org-drill-present-default-answer)
2058 (present-empty-cards nil)
2059 (cont nil)
2060 ;; fontification functions in `outline-view-change-hook' can cause big
2061 ;; slowdowns, so we temporarily bind this variable to nil here.
2062 (outline-view-change-hook nil))
2063 (setq drill-answer nil)
2064 (org-save-outline-visibility t
2065 (save-restriction
2066 (org-narrow-to-subtree)
2067 (org-show-subtree)
2068 (org-cycle-hide-drawers 'all)
2070 (let ((presentation-fn
2071 (cdr (assoc card-type org-drill-card-type-alist))))
2072 (if (listp presentation-fn)
2073 (psetq answer-fn (or (second presentation-fn)
2074 'org-drill-present-default-answer)
2075 present-empty-cards (third presentation-fn)
2076 presentation-fn (first presentation-fn)))
2077 (cond
2078 ((null presentation-fn)
2079 (message "%s:%d: Unrecognised card type '%s', skipping..."
2080 (buffer-name) (point) card-type)
2081 (sit-for 0.5)
2082 'skip)
2084 (setq cont (funcall presentation-fn))
2085 (cond
2086 ((not cont)
2087 (message "Quit")
2088 nil)
2089 ((eql cont 'edit)
2090 'edit)
2091 ((eql cont 'skip)
2092 'skip)
2094 (save-excursion
2095 (funcall answer-fn
2096 (lambda () (org-drill-reschedule)))))))))))))
2099 (defun org-drill-entries-pending-p ()
2100 (or *org-drill-again-entries*
2101 *org-drill-current-item*
2102 (and (not (org-drill-maximum-item-count-reached-p))
2103 (not (org-drill-maximum-duration-reached-p))
2104 (or *org-drill-new-entries*
2105 *org-drill-failed-entries*
2106 *org-drill-young-mature-entries*
2107 *org-drill-old-mature-entries*
2108 *org-drill-overdue-entries*
2109 *org-drill-again-entries*))))
2112 (defun org-drill-pending-entry-count ()
2113 (+ (if (markerp *org-drill-current-item*) 1 0)
2114 (length *org-drill-new-entries*)
2115 (length *org-drill-failed-entries*)
2116 (length *org-drill-young-mature-entries*)
2117 (length *org-drill-old-mature-entries*)
2118 (length *org-drill-overdue-entries*)
2119 (length *org-drill-again-entries*)))
2122 (defun org-drill-maximum-duration-reached-p ()
2123 "Returns true if the current drill session has continued past its
2124 maximum duration."
2125 (and org-drill-maximum-duration
2126 (not *org-drill-cram-mode*)
2127 *org-drill-start-time*
2128 (> (- (float-time (current-time)) *org-drill-start-time*)
2129 (* org-drill-maximum-duration 60))))
2132 (defun org-drill-maximum-item-count-reached-p ()
2133 "Returns true if the current drill session has reached the
2134 maximum number of items."
2135 (and org-drill-maximum-items-per-session
2136 (not *org-drill-cram-mode*)
2137 (>= (length *org-drill-done-entries*)
2138 org-drill-maximum-items-per-session)))
2141 (defun org-drill-pop-next-pending-entry ()
2142 (block org-drill-pop-next-pending-entry
2143 (let ((m nil))
2144 (while (or (null m)
2145 (not (org-drill-entry-p m)))
2146 (setq
2148 (cond
2149 ;; First priority is items we failed in a prior session.
2150 ((and *org-drill-failed-entries*
2151 (not (org-drill-maximum-item-count-reached-p))
2152 (not (org-drill-maximum-duration-reached-p)))
2153 (pop-random *org-drill-failed-entries*))
2154 ;; Next priority is overdue items.
2155 ((and *org-drill-overdue-entries*
2156 (not (org-drill-maximum-item-count-reached-p))
2157 (not (org-drill-maximum-duration-reached-p)))
2158 ;; We use `pop', not `pop-random', because we have already
2159 ;; sorted overdue items into a random order which takes
2160 ;; number of days overdue into account.
2161 (pop *org-drill-overdue-entries*))
2162 ;; Next priority is 'young' items.
2163 ((and *org-drill-young-mature-entries*
2164 (not (org-drill-maximum-item-count-reached-p))
2165 (not (org-drill-maximum-duration-reached-p)))
2166 (pop-random *org-drill-young-mature-entries*))
2167 ;; Next priority is newly added items, and older entries.
2168 ;; We pool these into a single group.
2169 ((and (or *org-drill-new-entries*
2170 *org-drill-old-mature-entries*)
2171 (not (org-drill-maximum-item-count-reached-p))
2172 (not (org-drill-maximum-duration-reached-p)))
2173 (cond
2174 ((< (random* (+ (length *org-drill-new-entries*)
2175 (length *org-drill-old-mature-entries*)))
2176 (length *org-drill-new-entries*))
2177 (pop-random *org-drill-new-entries*))
2179 (pop-random *org-drill-old-mature-entries*))))
2180 ;; After all the above are done, last priority is items
2181 ;; that were failed earlier THIS SESSION.
2182 (*org-drill-again-entries*
2183 (pop *org-drill-again-entries*))
2184 (t ; nothing left -- return nil
2185 (return-from org-drill-pop-next-pending-entry nil)))))
2186 m)))
2189 (defun org-drill-entries (&optional resuming-p)
2190 "Returns nil, t, or a list of markers representing entries that were
2191 'failed' and need to be presented again before the session ends.
2193 RESUMING-P is true if we are resuming a suspended drill session."
2194 (block org-drill-entries
2195 (while (org-drill-entries-pending-p)
2196 (let ((m (cond
2197 ((or (not resuming-p)
2198 (null *org-drill-current-item*)
2199 (not (org-drill-entry-p *org-drill-current-item*)))
2200 (org-drill-pop-next-pending-entry))
2201 (t ; resuming a suspended session.
2202 (setq resuming-p nil)
2203 *org-drill-current-item*))))
2204 (setq *org-drill-current-item* m)
2205 (unless m
2206 (error "Unexpectedly ran out of pending drill items"))
2207 (save-excursion
2208 (org-drill-goto-entry m)
2209 (cond
2210 ((not (org-drill-entry-due-p))
2211 ;; The entry is not due anymore. This could arise if the user
2212 ;; suspends a drill session, then drills an individual entry,
2213 ;; then resumes the session.
2214 (message "Entry no longer due, skipping...")
2215 (sit-for 0.3)
2216 nil)
2218 (setq result (org-drill-entry))
2219 (cond
2220 ((null result)
2221 (message "Quit")
2222 (setq end-pos :quit)
2223 (return-from org-drill-entries nil))
2224 ((eql result 'edit)
2225 (setq end-pos (point-marker))
2226 (return-from org-drill-entries nil))
2227 ((eql result 'skip)
2228 (setq *org-drill-current-item* nil)
2229 nil) ; skip this item
2231 (cond
2232 ((<= result org-drill-failure-quality)
2233 (if *org-drill-again-entries*
2234 (setq *org-drill-again-entries*
2235 (shuffle-list *org-drill-again-entries*)))
2236 (push-end m *org-drill-again-entries*))
2238 (push m *org-drill-done-entries*)))
2239 (setq *org-drill-current-item* nil))))))))))
2243 (defun org-drill-final-report ()
2244 (let ((pass-percent
2245 (round (* 100 (count-if (lambda (qual)
2246 (> qual org-drill-failure-quality))
2247 *org-drill-session-qualities*))
2248 (max 1 (length *org-drill-session-qualities*))))
2249 (prompt nil)
2250 (max-mini-window-height 0.6))
2251 (setq prompt
2252 (format
2253 "%d items reviewed. Session duration %s.
2254 Recall of reviewed items:
2255 Excellent (5): %3d%% | Near miss (2): %3d%%
2256 Good (4): %3d%% | Failure (1): %3d%%
2257 Hard (3): %3d%% | Abject failure (0): %3d%%
2259 You successfully recalled %d%% of reviewed items (quality > %s)
2260 %d/%d items still await review (%s, %s, %s, %s, %s).
2261 Tomorrow, %d more items will become due for review.
2262 Session finished. Press a key to continue..."
2263 (length *org-drill-done-entries*)
2264 (format-seconds "%h:%.2m:%.2s"
2265 (- (float-time (current-time)) *org-drill-start-time*))
2266 (round (* 100 (count 5 *org-drill-session-qualities*))
2267 (max 1 (length *org-drill-session-qualities*)))
2268 (round (* 100 (count 2 *org-drill-session-qualities*))
2269 (max 1 (length *org-drill-session-qualities*)))
2270 (round (* 100 (count 4 *org-drill-session-qualities*))
2271 (max 1 (length *org-drill-session-qualities*)))
2272 (round (* 100 (count 1 *org-drill-session-qualities*))
2273 (max 1 (length *org-drill-session-qualities*)))
2274 (round (* 100 (count 3 *org-drill-session-qualities*))
2275 (max 1 (length *org-drill-session-qualities*)))
2276 (round (* 100 (count 0 *org-drill-session-qualities*))
2277 (max 1 (length *org-drill-session-qualities*)))
2278 pass-percent
2279 org-drill-failure-quality
2280 (org-drill-pending-entry-count)
2281 (+ (org-drill-pending-entry-count)
2282 *org-drill-dormant-entry-count*)
2283 (propertize
2284 (format "%d failed"
2285 (+ (length *org-drill-failed-entries*)
2286 (length *org-drill-again-entries*)))
2287 'face `(:foreground ,org-drill-failed-count-color))
2288 (propertize
2289 (format "%d overdue"
2290 (length *org-drill-overdue-entries*))
2291 'face `(:foreground ,org-drill-failed-count-color))
2292 (propertize
2293 (format "%d new"
2294 (length *org-drill-new-entries*))
2295 'face `(:foreground ,org-drill-new-count-color))
2296 (propertize
2297 (format "%d young"
2298 (length *org-drill-young-mature-entries*))
2299 'face `(:foreground ,org-drill-mature-count-color))
2300 (propertize
2301 (format "%d old"
2302 (length *org-drill-old-mature-entries*))
2303 'face `(:foreground ,org-drill-mature-count-color))
2304 *org-drill-due-tomorrow-count*
2307 (while (not (input-pending-p))
2308 (message "%s" prompt)
2309 (sit-for 0.5))
2310 (read-char-exclusive)
2312 (if (and *org-drill-session-qualities*
2313 (< pass-percent (- 100 org-drill-forgetting-index)))
2314 (read-char-exclusive
2315 (format
2317 You failed %d%% of the items you reviewed during this session.
2318 %d (%d%%) of all items scanned were overdue.
2320 Are you keeping up with your items, and reviewing them
2321 when they are scheduled? If so, you may want to consider
2322 lowering the value of `org-drill-learn-fraction' slightly in
2323 order to make items appear more frequently over time."
2324 (propertize "WARNING!" 'face 'org-warning)
2325 (- 100 pass-percent)
2326 *org-drill-overdue-entry-count*
2327 (round (* 100 *org-drill-overdue-entry-count*)
2328 (+ *org-drill-dormant-entry-count*
2329 *org-drill-due-entry-count*)))
2330 ))))
2334 (defun org-drill-free-markers (markers)
2335 "MARKERS is a list of markers, all of which will be freed (set to
2336 point nowhere). Alternatively, MARKERS can be 't', in which case
2337 all the markers used by Org-Drill will be freed."
2338 (dolist (m (if (eql t markers)
2339 (append *org-drill-done-entries*
2340 *org-drill-new-entries*
2341 *org-drill-failed-entries*
2342 *org-drill-again-entries*
2343 *org-drill-overdue-entries*
2344 *org-drill-young-mature-entries*
2345 *org-drill-old-mature-entries*)
2346 markers))
2347 (free-marker m)))
2350 (defun org-drill-order-overdue-entries (overdue-data)
2351 (setq *org-drill-overdue-entries*
2352 (mapcar 'car
2353 (sort (shuffle-list overdue-data)
2354 (lambda (a b) (> (cdr a) (cdr b)))))))
2357 (defun org-drill-entry-status ()
2358 "Returns a list (STATUS DUE) where DUE is the number of days overdue,
2359 zero being due today, -1 being scheduled 1 day in the future. STATUS is
2360 one of the following values:
2361 - nil, if the item is not a drill entry, or has an empty body
2362 - :unscheduled
2363 - :future
2364 - :new
2365 - :failed
2366 - :overdue
2367 - :young
2368 - :old
2370 (save-excursion
2371 (unless (org-at-heading-p)
2372 (org-back-to-heading))
2373 (let ((due (org-drill-entry-days-overdue))
2374 (last-int (org-drill-entry-last-interval 1)))
2375 (list
2376 (cond
2377 ((not (org-drill-entry-p))
2378 nil)
2379 ((and (org-entry-empty-p)
2380 (let* ((card-type (org-entry-get (point) "DRILL_CARD_TYPE" nil))
2381 (dat (cdr (assoc card-type org-drill-card-type-alist))))
2382 (or (null card-type)
2383 (not (third dat)))))
2384 ;; body is empty, and this is not a card type where empty bodies are
2385 ;; meaningful, so skip it.
2386 nil)
2387 ((null due) ; unscheduled - usually a skipped leech
2388 :unscheduled)
2389 ;; ((eql -1 due)
2390 ;; :tomorrow)
2391 ((minusp due) ; scheduled in the future
2392 :future)
2393 ;; The rest of the stati all denote 'due' items ==========================
2394 ((<= (org-drill-entry-last-quality 9999)
2395 org-drill-failure-quality)
2396 ;; Mature entries that were failed last time are
2397 ;; FAILED, regardless of how young, old or overdue
2398 ;; they are.
2399 :failed)
2400 ((org-drill-entry-new-p)
2401 :new)
2402 ((org-drill-entry-overdue-p due last-int)
2403 ;; Overdue status overrides young versus old
2404 ;; distinction.
2405 ;; Store marker + due, for sorting of overdue entries
2406 :overdue)
2407 ((<= (org-drill-entry-last-interval 9999)
2408 org-drill-days-before-old)
2409 :young)
2411 :old))
2412 due))))
2415 (defun org-drill-progress-message (collected scanned)
2416 (when (zerop (% scanned 50))
2417 (let* ((meter-width 40)
2418 (sym1 (if (oddp (floor scanned (* 50 meter-width))) ?| ?.))
2419 (sym2 (if (eql sym1 ?.) ?| ?.)))
2420 (message "Collecting due drill items:%4d %s%s"
2421 collected
2422 (make-string (% (ceiling scanned 50) meter-width)
2423 sym2)
2424 (make-string (- meter-width (% (ceiling scanned 50) meter-width))
2425 sym1)))))
2428 (defun org-drill (&optional scope resume-p)
2429 "Begin an interactive 'drill session'. The user is asked to
2430 review a series of topics (headers). Each topic is initially
2431 presented as a 'question', often with part of the topic content
2432 hidden. The user attempts to recall the hidden information or
2433 answer the question, then presses a key to reveal the answer. The
2434 user then rates his or her recall or performance on that
2435 topic. This rating information is used to reschedule the topic
2436 for future review.
2438 Org-drill proceeds by:
2440 - Finding all topics (headings) in SCOPE which have either been
2441 used and rescheduled before, or which have a tag that matches
2442 `org-drill-question-tag'.
2444 - All matching topics which are either unscheduled, or are
2445 scheduled for the current date or a date in the past, are
2446 considered to be candidates for the drill session.
2448 - If `org-drill-maximum-items-per-session' is set, a random
2449 subset of these topics is presented. Otherwise, all of the
2450 eligible topics will be presented.
2452 SCOPE determines the scope in which to search for
2453 questions. It accepts the same values as `org-drill-scope',
2454 which see.
2456 If RESUME-P is non-nil, resume a suspended drill session rather
2457 than starting a new one."
2459 (interactive)
2460 (let ((end-pos nil)
2461 (overdue-data nil)
2462 (cnt 0))
2463 (block org-drill
2464 (unless resume-p
2465 (org-drill-free-markers t)
2466 (setq *org-drill-current-item* nil
2467 *org-drill-done-entries* nil
2468 *org-drill-dormant-entry-count* 0
2469 *org-drill-due-entry-count* 0
2470 *org-drill-due-tomorrow-count* 0
2471 *org-drill-overdue-entry-count* 0
2472 *org-drill-new-entries* nil
2473 *org-drill-overdue-entries* nil
2474 *org-drill-young-mature-entries* nil
2475 *org-drill-old-mature-entries* nil
2476 *org-drill-failed-entries* nil
2477 *org-drill-again-entries* nil)
2478 (setq *org-drill-session-qualities* nil)
2479 (setq *org-drill-start-time* (float-time (current-time))))
2480 (setq *random-state* (make-random-state t)) ; reseed RNG
2481 (unwind-protect
2482 (save-excursion
2483 (unless resume-p
2484 (let ((org-trust-scanner-tags t)
2485 (warned-about-id-creation nil))
2486 (org-map-drill-entries
2487 (lambda ()
2488 (org-drill-progress-message
2489 (+ (length *org-drill-new-entries*)
2490 (length *org-drill-overdue-entries*)
2491 (length *org-drill-young-mature-entries*)
2492 (length *org-drill-old-mature-entries*)
2493 (length *org-drill-failed-entries*))
2494 (incf cnt))
2495 (cond
2496 ((not (org-drill-entry-p))
2497 nil) ; skip
2499 (when (and (not warned-about-id-creation)
2500 (null (org-id-get)))
2501 (message (concat "Creating unique IDs for items "
2502 "(slow, but only happens once)"))
2503 (sit-for 0.5)
2504 (setq warned-about-id-creation t))
2505 (org-id-get-create) ; ensure drill entry has unique ID
2506 (destructuring-bind (status due) (org-drill-entry-status)
2507 (case status
2508 (:unscheduled
2509 (incf *org-drill-dormant-entry-count*))
2510 ;; (:tomorrow
2511 ;; (incf *org-drill-dormant-entry-count*)
2512 ;; (incf *org-drill-due-tomorrow-count*))
2513 (:future
2514 (incf *org-drill-dormant-entry-count*)
2515 (if (eq -1 due)
2516 (incf *org-drill-due-tomorrow-count*)))
2517 (:new
2518 (push (point-marker) *org-drill-new-entries*))
2519 (:failed
2520 (push (point-marker) *org-drill-failed-entries*))
2521 (:young
2522 (push (point-marker) *org-drill-young-mature-entries*))
2523 (:overdue
2524 (push (cons (point-marker) due) overdue-data))
2525 (:old
2526 (push (point-marker) *org-drill-old-mature-entries*))
2527 )))))
2528 scope)
2529 (org-drill-order-overdue-entries overdue-data)
2530 (setq *org-drill-overdue-entry-count*
2531 (length *org-drill-overdue-entries*))))
2532 (setq *org-drill-due-entry-count* (org-drill-pending-entry-count))
2533 (cond
2534 ((and (null *org-drill-current-item*)
2535 (null *org-drill-new-entries*)
2536 (null *org-drill-failed-entries*)
2537 (null *org-drill-overdue-entries*)
2538 (null *org-drill-young-mature-entries*)
2539 (null *org-drill-old-mature-entries*))
2540 (message "I did not find any pending drill items."))
2542 (org-drill-entries resume-p)
2543 (message "Drill session finished!"))))
2544 (progn
2545 (unless end-pos
2546 (setq *org-drill-cram-mode* nil)
2547 (org-drill-free-markers *org-drill-done-entries*)))))
2548 (cond
2549 (end-pos
2550 (when (markerp end-pos)
2551 (org-drill-goto-entry end-pos)
2552 (org-reveal)
2553 (org-show-entry))
2554 (let ((keystr (command-keybinding-to-string 'org-drill-resume)))
2555 (message
2556 "You can continue the drill session with the command `org-drill-resume'.%s"
2557 (if keystr (format "\nYou can run this command by pressing %s." keystr)
2558 ""))))
2560 (org-drill-final-report)
2561 (if (eql 'sm5 org-drill-spaced-repetition-algorithm)
2562 (org-drill-save-optimal-factor-matrix))
2563 (if org-drill-save-buffers-after-drill-sessions-p
2564 (save-some-buffers))
2565 (message "Drill session finished!")
2566 ))))
2569 (defun org-drill-save-optimal-factor-matrix ()
2570 (message "Saving optimal factor matrix...")
2571 (customize-save-variable 'org-drill-optimal-factor-matrix
2572 org-drill-optimal-factor-matrix))
2575 (defun org-drill-cram (&optional scope)
2576 "Run an interactive drill session in 'cram mode'. In cram mode,
2577 all drill items are considered to be due for review, unless they
2578 have been reviewed within the last `org-drill-cram-hours'
2579 hours."
2580 (interactive)
2581 (setq *org-drill-cram-mode* t)
2582 (org-drill scope))
2585 (defun org-drill-tree ()
2586 "Run an interactive drill session using drill items within the
2587 subtree at point."
2588 (interactive)
2589 (org-drill 'tree))
2592 (defun org-drill-directory ()
2593 "Run an interactive drill session using drill items from all org
2594 files in the same directory as the current file."
2595 (interactive)
2596 (org-drill 'directory))
2599 (defun org-drill-again (&optional scope)
2600 "Run a new drill session, but try to use leftover due items that
2601 were not reviewed during the last session, rather than scanning for
2602 unreviewed items. If there are no leftover items in memory, a full
2603 scan will be performed."
2604 (interactive)
2605 (setq *org-drill-cram-mode* nil)
2606 (cond
2607 ((plusp (org-drill-pending-entry-count))
2608 (org-drill-free-markers *org-drill-done-entries*)
2609 (if (markerp *org-drill-current-item*)
2610 (free-marker *org-drill-current-item*))
2611 (setq *org-drill-start-time* (float-time (current-time))
2612 *org-drill-done-entries* nil
2613 *org-drill-current-item* nil)
2614 (org-drill scope t))
2616 (org-drill scope))))
2620 (defun org-drill-resume ()
2621 "Resume a suspended drill session. Sessions are suspended by
2622 exiting them with the `edit' or `quit' options."
2623 (interactive)
2624 (cond
2625 ((org-drill-entries-pending-p)
2626 (org-drill nil t))
2627 ((and (plusp (org-drill-pending-entry-count))
2628 ;; Current drill session is finished, but there are still
2629 ;; more items which need to be reviewed.
2630 (y-or-n-p (format
2631 "You have finished the drill session. However, %d items still
2632 need reviewing. Start a new drill session? "
2633 (org-drill-pending-entry-count))))
2634 (org-drill-again))
2636 (message "You have finished the drill session."))))
2639 (defun org-drill-strip-entry-data ()
2640 (dolist (prop org-drill-scheduling-properties)
2641 (org-delete-property prop))
2642 (org-schedule t))
2645 (defun org-drill-strip-all-data (&optional scope)
2646 "Delete scheduling data from every drill entry in scope. This
2647 function may be useful if you want to give your collection of
2648 entries to someone else. Scope defaults to the current buffer,
2649 and is specified by the argument SCOPE, which accepts the same
2650 values as `org-drill-scope'."
2651 (interactive)
2652 (when (yes-or-no-p
2653 "Delete scheduling data from ALL items in scope: are you sure?")
2654 (cond
2655 ((null scope)
2656 ;; Scope is the current buffer. This means we can use
2657 ;; `org-delete-property-globally', which is faster.
2658 (dolist (prop org-drill-scheduling-properties)
2659 (org-delete-property-globally prop))
2660 (org-map-drill-entries (lambda () (org-schedule t)) scope))
2662 (org-map-drill-entries 'org-drill-strip-entry-data scope)))
2663 (message "Done.")))
2667 (defun org-drill-add-cloze-fontification ()
2668 (when org-drill-use-visible-cloze-face-p
2669 (font-lock-add-keywords 'org-mode
2670 org-drill-cloze-keywords
2671 nil)))
2673 (add-hook 'org-mode-hook 'org-drill-add-cloze-fontification)
2675 (org-drill-add-cloze-fontification)
2678 ;;; Synching card collections =================================================
2681 (defvar *org-drill-dest-id-table* (make-hash-table :test 'equal))
2684 (defun org-drill-copy-entry-to-other-buffer (dest &optional path)
2685 "Copy the subtree at point to the buffer DEST. The copy will receive
2686 the tag 'imported'."
2687 (block org-drill-copy-entry-to-other-buffer
2688 (save-excursion
2689 (let ((src (current-buffer))
2690 (m nil))
2691 (flet ((paste-tree-here (&optional level)
2692 (org-paste-subtree level)
2693 (org-drill-strip-entry-data)
2694 (org-toggle-tag "imported" 'on)
2695 (org-map-drill-entries
2696 (lambda ()
2697 (let ((id (org-id-get)))
2698 (org-drill-strip-entry-data)
2699 (unless (gethash id *org-drill-dest-id-table*)
2700 (puthash id (point-marker)
2701 *org-drill-dest-id-table*))))
2702 'tree)))
2703 (unless path
2704 (setq path (org-get-outline-path)))
2705 (org-copy-subtree)
2706 (switch-to-buffer dest)
2707 (setq m
2708 (condition-case nil
2709 (org-find-olp path t)
2710 (error ; path does not exist in DEST
2711 (return-from org-drill-copy-entry-to-other-buffer
2712 (cond
2713 ((cdr path)
2714 (org-drill-copy-entry-to-other-buffer
2715 dest (butlast path)))
2717 ;; We've looked all the way up the path
2718 ;; Default to appending to the end of DEST
2719 (goto-char (point-max))
2720 (newline)
2721 (paste-tree-here)))))))
2722 (goto-char m)
2723 (outline-next-heading)
2724 (newline)
2725 (forward-line -1)
2726 (paste-tree-here (1+ (or (org-current-level) 0)))
2727 )))))
2731 (defun org-drill-merge-buffers (src &optional dest ignore-new-items-p)
2732 "SRC and DEST are two org mode buffers containing drill items.
2733 For each drill item in DEST that shares an ID with an item in SRC,
2734 overwrite scheduling data in DEST with data taken from the item in SRC.
2735 This is intended for use when two people are sharing a set of drill items,
2736 one person has made some updates to the item set, and the other person
2737 wants to migrate to the updated set without losing their scheduling data.
2739 By default, any drill items in SRC which do not exist in DEST are
2740 copied into DEST. We attempt to place the copied item in the
2741 equivalent location in DEST to its location in SRC, by matching
2742 the heading hierarchy. However if IGNORE-NEW-ITEMS-P is non-nil,
2743 we simply ignore any items that do not exist in DEST, and do not
2744 copy them across."
2745 (interactive "bImport scheduling info from which buffer?")
2746 (unless dest
2747 (setq dest (current-buffer)))
2748 (setq src (get-buffer src)
2749 dest (get-buffer dest))
2750 (when (yes-or-no-p
2751 (format
2752 (concat "About to overwrite all scheduling data for drill items in `%s' "
2753 "with information taken from matching items in `%s'. Proceed? ")
2754 (buffer-name dest) (buffer-name src)))
2755 ;; Compile list of all IDs in the destination buffer.
2756 (clrhash *org-drill-dest-id-table*)
2757 (with-current-buffer dest
2758 (org-map-drill-entries
2759 (lambda ()
2760 (let ((this-id (org-id-get)))
2761 (when this-id
2762 (puthash this-id (point-marker) *org-drill-dest-id-table*))))
2763 'file))
2764 ;; Look through all entries in source buffer.
2765 (with-current-buffer src
2766 (org-map-drill-entries
2767 (lambda ()
2768 (let ((id (org-id-get))
2769 (last-quality nil) (last-reviewed nil)
2770 (scheduled-time nil))
2771 (cond
2772 ((or (null id)
2773 (not (org-drill-entry-p)))
2774 nil)
2775 ((gethash id *org-drill-dest-id-table*)
2776 ;; This entry matches an entry in dest. Retrieve all its
2777 ;; scheduling data, then go to the matching location in dest
2778 ;; and write the data.
2779 (let ((marker (gethash id *org-drill-dest-id-table*)))
2780 (destructuring-bind (last-interval repetitions failures
2781 total-repeats meanq ease)
2782 (org-drill-get-item-data)
2783 (setq last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")
2784 last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")
2785 scheduled-time (org-get-scheduled-time (point)))
2786 (save-excursion
2787 ;; go to matching entry in destination buffer
2788 (switch-to-buffer (marker-buffer marker))
2789 (goto-char marker)
2790 (org-drill-strip-entry-data)
2791 (unless (zerop total-repeats)
2792 (org-drill-store-item-data last-interval repetitions failures
2793 total-repeats meanq ease)
2794 (if last-quality
2795 (org-set-property "LAST_QUALITY" last-quality)
2796 (org-delete-property "LAST_QUALITY"))
2797 (if last-reviewed
2798 (org-set-property "LAST_REVIEWED" last-reviewed)
2799 (org-delete-property "LAST_REVIEWED"))
2800 (if scheduled-time
2801 (org-schedule nil scheduled-time)))))
2802 (remhash id *org-drill-dest-id-table*)
2803 (free-marker marker)))
2805 ;; item in SRC has ID, but no matching ID in DEST.
2806 ;; It must be a new item that does not exist in DEST.
2807 ;; Copy the entire item to the *end* of DEST.
2808 (unless ignore-new-items-p
2809 (org-drill-copy-entry-to-other-buffer dest))))))
2810 'file))
2811 ;; Finally: there may be some items in DEST which are not in SRC, and
2812 ;; which have been scheduled by another user of DEST. Clear out the
2813 ;; scheduling info from all the unmatched items in DEST.
2814 (with-current-buffer dest
2815 (maphash (lambda (id m)
2816 (goto-char m)
2817 (org-drill-strip-entry-data)
2818 (free-marker m))
2819 *org-drill-dest-id-table*))))
2823 ;;; Card types for learning languages =========================================
2825 ;;; Get spell-number.el from:
2826 ;;; http://www.emacswiki.org/emacs/spell-number.el
2827 (autoload 'spelln-integer-in-words "spell-number")
2830 ;;; `conjugate' card type =====================================================
2831 ;;; See spanish.org for usage
2833 (defvar org-drill-verb-tense-alist
2834 '(("present" "tomato")
2835 ("simple present" "tomato")
2836 ("present indicative" "tomato")
2837 ;; past tenses
2838 ("past" "purple")
2839 ("simple past" "purple")
2840 ("preterite" "purple")
2841 ("imperfect" "darkturquoise")
2842 ("present perfect" "royalblue")
2843 ;; future tenses
2844 ("future" "green")
2845 ;; moods (backgrounds).
2846 ("indicative" nil) ; default
2847 ("subjunctive" "medium blue")
2848 ("conditional" "grey30")
2849 ("negative imperative" "red4")
2850 ("positive imperative" "darkgreen")
2852 "Alist where each entry has the form (TENSE COLOUR), where
2853 TENSE is a string naming a tense in which verbs can be
2854 conjugated, and COLOUR is a string specifying a foreground colour
2855 which will be used by `org-drill-present-verb-conjugation' and
2856 `org-drill-show-answer-verb-conjugation' to fontify the verb and
2857 the name of the tense.")
2860 (defun org-drill-get-verb-conjugation-info ()
2861 "Auxiliary function used by `org-drill-present-verb-conjugation' and
2862 `org-drill-show-answer-verb-conjugation'."
2863 (let ((infinitive (org-entry-get (point) "VERB_INFINITIVE" t))
2864 (inf-hint (org-entry-get (point) "VERB_INFINITIVE_HINT" t))
2865 (translation (org-entry-get (point) "VERB_TRANSLATION" t))
2866 (tense (org-entry-get (point) "VERB_TENSE" nil))
2867 (mood (org-entry-get (point) "VERB_MOOD" nil))
2868 (highlight-face nil))
2869 (unless (and infinitive translation (or tense mood))
2870 (error "Missing information for verb conjugation card (%s, %s, %s, %s) at %s"
2871 infinitive translation tense mood (point)))
2872 (setq tense (if tense (downcase (car (read-from-string tense))))
2873 mood (if mood (downcase (car (read-from-string mood))))
2874 infinitive (car (read-from-string infinitive))
2875 inf-hint (if inf-hint (car (read-from-string inf-hint)))
2876 translation (car (read-from-string translation)))
2877 (setq highlight-face
2878 (list :foreground
2879 (or (second (assoc-string tense org-drill-verb-tense-alist t))
2880 "hotpink")
2881 :background
2882 (second (assoc-string mood org-drill-verb-tense-alist t))))
2883 (setq infinitive (propertize infinitive 'face highlight-face))
2884 (setq translation (propertize translation 'face highlight-face))
2885 (if tense (setq tense (propertize tense 'face highlight-face)))
2886 (if mood (setq mood (propertize mood 'face highlight-face)))
2887 (list infinitive inf-hint translation tense mood)))
2890 (defun org-drill-present-verb-conjugation ()
2891 "Present a drill entry whose card type is 'conjugate'."
2892 (flet ((tense-and-mood-to-string
2893 (tense mood)
2894 (cond
2895 ((and tense mood)
2896 (format "%s tense, %s mood" tense mood))
2897 (tense
2898 (format "%s tense" tense))
2899 (mood
2900 (format "%s mood" mood)))))
2901 (destructuring-bind (infinitive inf-hint translation tense mood)
2902 (org-drill-get-verb-conjugation-info)
2903 (org-drill-present-card-using-text
2904 (cond
2905 ((zerop (random* 2))
2906 (format "\nTranslate the verb\n\n%s\n\nand conjugate for the %s.\n\n"
2907 infinitive (tense-and-mood-to-string tense mood)))
2910 (format "\nGive the verb that means\n\n%s %s\n
2911 and conjugate for the %s.\n\n"
2912 translation
2913 (if inf-hint (format " [HINT: %s]" inf-hint) "")
2914 (tense-and-mood-to-string tense mood))))))))
2917 (defun org-drill-show-answer-verb-conjugation (reschedule-fn)
2918 "Show the answer for a drill item whose card type is 'conjugate'.
2919 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
2920 returns its return value."
2921 (destructuring-bind (infinitive inf-hint translation tense mood)
2922 (org-drill-get-verb-conjugation-info)
2923 (with-replaced-entry-heading
2924 (format "%s of %s ==> %s\n\n"
2925 (capitalize
2926 (cond
2927 ((and tense mood)
2928 (format "%s tense, %s mood" tense mood))
2929 (tense
2930 (format "%s tense" tense))
2931 (mood
2932 (format "%s mood" mood))))
2933 infinitive translation)
2934 (org-cycle-hide-drawers 'all)
2935 (funcall reschedule-fn))))
2938 ;;; `decline_noun' card type ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2941 (defvar org-drill-noun-gender-alist
2942 '(("masculine" "dodgerblue")
2943 ("masc" "dodgerblue")
2944 ("male" "dodgerblue")
2945 ("m" "dodgerblue")
2946 ("feminine" "orchid")
2947 ("fem" "orchid")
2948 ("female" "orchid")
2949 ("f" "orchid")
2950 ("neuter" "green")
2951 ("neutral" "green")
2952 ("neut" "green")
2953 ("n" "green")
2957 (defun org-drill-get-noun-info ()
2958 "Auxiliary function used by `org-drill-present-noun-declension' and
2959 `org-drill-show-answer-noun-declension'."
2960 (let ((noun (org-entry-get (point) "NOUN" t))
2961 (noun-hint (org-entry-get (point) "NOUN_HINT" t))
2962 (noun-root (org-entry-get (point) "NOUN_ROOT" t))
2963 (noun-gender (org-entry-get (point) "NOUN_GENDER" t))
2964 (translation (org-entry-get (point) "NOUN_TRANSLATION" t))
2965 (highlight-face nil))
2966 (unless (and noun translation)
2967 (error "Missing information for `decline_noun' card (%s, %s, %s, %s) at %s"
2968 noun translation noun-hint noun-root (point)))
2969 (setq noun-root (if noun-root (car (read-from-string noun-root)))
2970 noun (car (read-from-string noun))
2971 noun-gender (downcase (car (read-from-string noun-gender)))
2972 noun-hint (if noun-hint (car (read-from-string noun-hint)))
2973 translation (car (read-from-string translation)))
2974 (setq highlight-face
2975 (list :foreground
2976 (or (second (assoc-string noun-gender
2977 org-drill-noun-gender-alist t))
2978 "red")))
2979 (setq noun (propertize noun 'face highlight-face))
2980 (setq translation (propertize translation 'face highlight-face))
2981 (list noun noun-root noun-gender noun-hint translation)))
2984 (defun org-drill-present-noun-declension ()
2985 "Present a drill entry whose card type is 'decline_noun'."
2986 (destructuring-bind (noun noun-root noun-gender noun-hint translation)
2987 (org-drill-get-noun-info)
2988 (let* ((props (org-entry-properties (point)))
2989 (definite
2990 (cond
2991 ((assoc "DECLINE_DEFINITE" props)
2992 (propertize (if (org-entry-get (point) "DECLINE_DEFINITE")
2993 "definite" "indefinite")
2994 'face 'warning))
2995 (t nil)))
2996 (plural
2997 (cond
2998 ((assoc "DECLINE_PLURAL" props)
2999 (propertize (if (org-entry-get (point) "DECLINE_PLURAL")
3000 "plural" "singular")
3001 'face 'warning))
3002 (t nil))))
3003 (org-drill-present-card-using-text
3004 (cond
3005 ((zerop (random* 2))
3006 (format "\nTranslate the noun\n\n%s (%s)\n\nand list its declensions%s.\n\n"
3007 noun noun-gender
3008 (if (or plural definite)
3009 (format " for the %s %s form" definite plural)
3010 "")))
3012 (format "\nGive the noun that means\n\n%s %s\n
3013 and list its declensions%s.\n\n"
3014 translation
3015 (if noun-hint (format " [HINT: %s]" noun-hint) "")
3016 (if (or plural definite)
3017 (format " for the %s %s form" definite plural)
3018 ""))))))))
3021 (defun org-drill-show-answer-noun-declension (reschedule-fn)
3022 "Show the answer for a drill item whose card type is 'decline_noun'.
3023 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
3024 returns its return value."
3025 (destructuring-bind (noun noun-root noun-gender noun-hint translation)
3026 (org-drill-get-noun-info)
3027 (with-replaced-entry-heading
3028 (format "Declensions of %s (%s) ==> %s\n\n"
3029 noun noun-gender translation)
3030 (org-cycle-hide-drawers 'all)
3031 (funcall reschedule-fn))))
3034 ;;; `translate_number' card type ==============================================
3035 ;;; See spanish.org for usage
3038 (defun spelln-integer-in-language (n lang)
3039 (let ((spelln-language lang))
3040 (spelln-integer-in-words n)))
3042 (defun org-drill-present-translate-number ()
3043 (let ((num-min (read (org-entry-get (point) "DRILL_NUMBER_MIN")))
3044 (num-max (read (org-entry-get (point) "DRILL_NUMBER_MAX")))
3045 (language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
3046 (drilled-number 0)
3047 (drilled-number-direction 'to-english)
3048 (highlight-face 'font-lock-warning-face))
3049 (cond
3050 ((not (fboundp 'spelln-integer-in-words))
3051 (message "`spell-number.el' not loaded, skipping 'translate_number' card...")
3052 (sit-for 0.5)
3053 'skip)
3054 ((not (and (numberp num-min) (numberp num-max) language))
3055 (error "Missing language or minimum or maximum numbers for number card"))
3057 (if (> num-min num-max)
3058 (psetf num-min num-max
3059 num-max num-min))
3060 (setq drilled-number
3061 (+ num-min (random* (abs (1+ (- num-max num-min))))))
3062 (setq drilled-number-direction
3063 (if (zerop (random* 2)) 'from-english 'to-english))
3064 (cond
3065 ((eql 'to-english drilled-number-direction)
3066 (org-drill-present-card-using-text
3067 (format "\nTranslate into English:\n\n%s\n"
3068 (propertize
3069 (spelln-integer-in-language drilled-number language)
3070 'face highlight-face))
3071 (spelln-integer-in-language drilled-number 'english-gb)))
3073 (org-drill-present-card-using-text
3074 (format "\nTranslate into %s:\n\n%s\n"
3075 (capitalize (format "%s" language))
3076 (propertize
3077 (spelln-integer-in-language drilled-number 'english-gb)
3078 'face highlight-face))
3079 (spelln-integer-in-language drilled-number language))))))))
3082 ;; (defun org-drill-show-answer-translate-number (reschedule-fn)
3083 ;; (let* ((language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
3084 ;; (highlight-face 'font-lock-warning-face)
3085 ;; (non-english
3086 ;; (let ((spelln-language language))
3087 ;; (propertize (spelln-integer-in-words *drilled-number*)
3088 ;; 'face highlight-face)))
3089 ;; (english
3090 ;; (let ((spelln-language 'english-gb))
3091 ;; (propertize (spelln-integer-in-words *drilled-number*)
3092 ;; 'face 'highlight-face))))
3093 ;; (with-replaced-entry-text
3094 ;; (cond
3095 ;; ((eql 'to-english *drilled-number-direction*)
3096 ;; (format "\nThe English translation of %s is:\n\n%s\n"
3097 ;; non-english english))
3098 ;; (t
3099 ;; (format "\nThe %s translation of %s is:\n\n%s\n"
3100 ;; (capitalize (format "%s" language))
3101 ;; english non-english)))
3102 ;; (funcall reschedule-fn))))
3105 ;;; `spanish_verb' card type ==================================================
3106 ;;; Not very interesting, but included to demonstrate how a presentation
3107 ;;; function can manipulate which subheading are hidden versus shown.
3110 (defun org-drill-present-spanish-verb ()
3111 (let ((prompt nil)
3112 (reveal-headings nil))
3113 (with-hidden-comments
3114 (with-hidden-cloze-hints
3115 (with-hidden-cloze-text
3116 (case (random* 6)
3118 (org-drill-hide-all-subheadings-except '("Infinitive"))
3119 (setq prompt
3120 (concat "Translate this Spanish verb, and conjugate it "
3121 "for the *present* tense.")
3122 reveal-headings '("English" "Present Tense" "Notes")))
3124 (org-drill-hide-all-subheadings-except '("English"))
3125 (setq prompt (concat "For the *present* tense, conjugate the "
3126 "Spanish translation of this English verb.")
3127 reveal-headings '("Infinitive" "Present Tense" "Notes")))
3129 (org-drill-hide-all-subheadings-except '("Infinitive"))
3130 (setq prompt (concat "Translate this Spanish verb, and "
3131 "conjugate it for the *past* tense.")
3132 reveal-headings '("English" "Past Tense" "Notes")))
3134 (org-drill-hide-all-subheadings-except '("English"))
3135 (setq prompt (concat "For the *past* tense, conjugate the "
3136 "Spanish translation of this English verb.")
3137 reveal-headings '("Infinitive" "Past Tense" "Notes")))
3139 (org-drill-hide-all-subheadings-except '("Infinitive"))
3140 (setq prompt (concat "Translate this Spanish verb, and "
3141 "conjugate it for the *future perfect* tense.")
3142 reveal-headings '("English" "Future Perfect Tense" "Notes")))
3144 (org-drill-hide-all-subheadings-except '("English"))
3145 (setq prompt (concat "For the *future perfect* tense, conjugate the "
3146 "Spanish translation of this English verb.")
3147 reveal-headings '("Infinitive" "Future Perfect Tense" "Notes"))))
3148 (org-cycle-hide-drawers 'all)
3149 (prog1 (org-drill-presentation-prompt)
3150 (org-drill-hide-subheadings-if 'org-drill-entry-p)))))))
3153 (provide 'org-drill)