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