1 ;; -*- coding: utf-8-unix -*-
2 ;;; org-drill.el - Self-testing using spaced repetition
4 ;;; Copyright (C) 2010-2015 Paul Sexton
6 ;;; Author: Paul Sexton <eeeickythump@gmail.com>
8 ;;; Keywords: flashcards, memory, learning, memorization
9 ;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
11 ;;; This file is not part of GNU Emacs.
13 ;;; This program is free software; you can redistribute it and/or modify
14 ;;; it under the terms of the GNU General Public License as published by
15 ;;; the Free Software Foundation, either version 3 of the License, or
16 ;;; (at your option) any later version.
18 ;;; This program is distaributed in the hope that it will be useful,
19 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;;; GNU General Public License for more details.
23 ;;; You should have received a copy of the GNU General Public License
24 ;;; along with this program. If not, see <http://www.gnu.org/licenses/>.
30 ;;; Uses the SuperMemo spaced repetition algorithms to conduct interactive
31 ;;; "drill sessions", where the material to be remembered is presented to the
32 ;;; student in random order. The student rates his or her recall of each item,
33 ;;; and this information is used to schedule the item for later revision.
35 ;;; Each drill session can be restricted to topics in the current buffer
36 ;;; (default), one or several files, all agenda files, or a subtree. A single
37 ;;; topic can also be drilled.
39 ;;; Different "card types" can be defined, which present their information to
40 ;;; the student in different ways.
42 ;;; See the file README.org for more detailed documentation.
45 (eval-when-compile (require 'cl
))
46 (eval-when-compile (require 'hi-lock
))
55 (defgroup org-drill nil
56 "Options concerning interactive drill sessions in Org mode (org-drill)."
62 (defcustom org-drill-question-tag
64 "Tag which topics must possess in order to be identified as review topics
70 (defcustom org-drill-maximum-items-per-session
72 "Each drill session will present at most this many topics for review.
75 :type
'(choice integer
(const nil
)))
79 (defcustom org-drill-maximum-duration
81 "Maximum duration of a drill session, in minutes.
84 :type
'(choice integer
(const nil
)))
87 (defcustom org-drill-failure-quality
89 "If the quality of recall for an item is this number or lower,
90 it is regarded as an unambiguous failure, and the repetition
91 interval for the card is reset to 0 days. If the quality is higher
92 than this number, it is regarded as successfully recalled, but the
93 time interval to the next repetition will be lowered if the quality
96 By default this is 2, for SuperMemo-like behaviour. For
97 Mnemosyne-like behaviour, set it to 1. Other values are not
100 :type
'(choice (const 2) (const 1)))
103 (defcustom org-drill-forgetting-index
105 "What percentage of items do you consider it is 'acceptable' to
106 forget each drill session? The default is 10%. A warning message
107 is displayed at the end of the session if the percentage forgotten
108 climbs above this number."
113 (defcustom org-drill-leech-failure-threshold
115 "If an item is forgotten more than this many times, it is tagged
118 :type
'(choice integer
(const nil
)))
121 (defcustom org-drill-leech-method
123 "How should 'leech items' be handled during drill sessions?
125 - nil :: Leech items are treated the same as normal items.
126 - skip :: Leech items are not included in drill sessions.
127 - warn :: Leech items are still included in drill sessions,
128 but a warning message is printed when each leech item is
131 :type
'(choice (const warn
) (const skip
) (const nil
)))
134 (defface org-drill-visible-cloze-face
135 '((t (:foreground
"darkseagreen")))
136 "The face used to hide the contents of cloze phrases."
140 (defface org-drill-visible-cloze-hint-face
141 '((t (:foreground
"dark slate blue")))
142 "The face used to hide the contents of cloze phrases."
146 (defface org-drill-hidden-cloze-face
147 '((t (:foreground
"deep sky blue" :background
"blue")))
148 "The face used to hide the contents of cloze phrases."
152 (defcustom org-drill-use-visible-cloze-face-p
154 "Use a special face to highlight cloze-deleted text in org mode
160 (defcustom org-drill-hide-item-headings-p
162 "Conceal the contents of the main heading of each item during drill
163 sessions? You may want to enable this behaviour if item headings or tags
164 contain information that could 'give away' the answer."
169 (defcustom org-drill-new-count-color
171 "Foreground colour used to display the count of remaining new items
172 during a drill session."
176 (defcustom org-drill-mature-count-color
178 "Foreground colour used to display the count of remaining mature items
179 during a drill session. Mature items are due for review, but are not new."
183 (defcustom org-drill-failed-count-color
185 "Foreground colour used to display the count of remaining failed items
186 during a drill session."
190 (defcustom org-drill-done-count-color
192 "Foreground colour used to display the count of reviewed items
193 during a drill session."
197 (defcustom org-drill-left-cloze-delimiter
199 "String used within org buffers to delimit cloze deletions."
203 (defcustom org-drill-right-cloze-delimiter
205 "String used within org buffers to delimit cloze deletions."
210 (setplist 'org-drill-cloze-overlay-defaults
211 `(display ,(format "%s...%s"
212 org-drill-left-cloze-delimiter
213 org-drill-right-cloze-delimiter
)
214 face org-drill-hidden-cloze-face
217 (setplist 'org-drill-hidden-text-overlay
220 (setplist 'org-drill-replaced-text-overlay
221 '(display "Replaced text"
225 (add-hook 'org-font-lock-set-keywords-hook
'org-drill-add-cloze-fontification
)
228 (defvar org-drill-hint-separator
"||"
229 "String which, if it occurs within a cloze expression, signifies that the
230 rest of the expression after the string is a `hint', to be displayed instead of
231 the hidden cloze during a test.")
233 (defun org-drill--compute-cloze-regexp ()
235 (regexp-quote org-drill-left-cloze-delimiter
)
236 "[[:cntrl:][:graph:][:space:]]+?\\)\\(\\|"
237 (regexp-quote org-drill-hint-separator
)
239 (regexp-quote org-drill-right-cloze-delimiter
)
242 (defun org-drill--compute-cloze-keywords ()
243 (list (list (org-drill--compute-cloze-regexp)
244 (copy-list '(1 'org-drill-visible-cloze-face nil
))
245 (copy-list '(2 'org-drill-visible-cloze-hint-face t
))
246 (copy-list '(3 'org-drill-visible-cloze-face nil
))
249 (defvar-local org-drill-cloze-regexp
250 (org-drill--compute-cloze-regexp))
253 (defvar-local org-drill-cloze-keywords
254 (org-drill--compute-cloze-keywords))
257 ;; Variables defining what keys can be pressed during drill sessions to quit the
258 ;; session, edit the item, etc.
259 (defvar org-drill--quit-key ?q
260 "If this character is pressed during a drill session, quit the session.")
261 (defvar org-drill--edit-key ?e
262 "If this character is pressed during a drill session, suspend the session
263 with the cursor at the current item..")
264 (defvar org-drill--help-key ??
265 "If this character is pressed during a drill session, show help.")
266 (defvar org-drill--skip-key ?s
267 "If this character is pressed during a drill session, skip to the next
269 (defvar org-drill--tags-key ?t
270 "If this character is pressed during a drill session, edit the tags for
274 (defcustom org-drill-card-type-alist
275 '((nil org-drill-present-simple-card
)
276 ("simple" org-drill-present-simple-card
)
277 ("twosided" org-drill-present-two-sided-card nil t
)
278 ("multisided" org-drill-present-multi-sided-card nil t
)
279 ("hide1cloze" org-drill-present-multicloze-hide1
)
280 ("hide2cloze" org-drill-present-multicloze-hide2
)
281 ("show1cloze" org-drill-present-multicloze-show1
)
282 ("show2cloze" org-drill-present-multicloze-show2
)
283 ("multicloze" org-drill-present-multicloze-hide1
)
284 ("hidefirst" org-drill-present-multicloze-hide-first
)
285 ("hidelast" org-drill-present-multicloze-hide-last
)
286 ("hide1_firstmore" org-drill-present-multicloze-hide1-firstmore
)
287 ("show1_lastmore" org-drill-present-multicloze-show1-lastmore
)
288 ("show1_firstless" org-drill-present-multicloze-show1-firstless
)
290 org-drill-present-verb-conjugation
291 org-drill-show-answer-verb-conjugation
)
293 org-drill-present-noun-declension
294 org-drill-show-answer-noun-declension
)
295 ("spanish_verb" org-drill-present-spanish-verb
)
296 ("translate_number" org-drill-present-translate-number
))
297 "Alist associating card types with presentation functions. Each
298 entry in the alist takes the form:
300 ;;; (CARDTYPE QUESTION-FN [ANSWER-FN DRILL-EMPTY-P])
302 Where CARDTYPE is a string or nil (for default), and QUESTION-FN
303 is a function which takes no arguments and returns a boolean
306 When supplied, ANSWER-FN is a function that takes one argument --
307 that argument is a function of no arguments, which when called,
308 prompts the user to rate their recall and performs rescheduling
309 of the drill item. ANSWER-FN is called with the point on the
310 active item's heading, just prior to displaying the item's
311 'answer'. It can therefore be used to modify the appearance of
312 the answer. ANSWER-FN must call its argument before returning.
314 When supplied, DRILL-EMPTY-P is a boolean value, default nil.
315 When non-nil, cards of this type will be presented during tests
316 even if their bodies are empty."
318 :type
'(alist :key-type
(choice string
(const nil
))
319 :value-type function
))
322 (defcustom org-drill-scope
324 "The scope in which to search for drill items when conducting a
325 drill session. This can be any of:
327 file The current buffer, respecting the restriction if any.
329 tree The subtree started with the entry at point
330 file-no-restriction The current buffer, without restriction
331 file-with-archives The current buffer, and any archives associated with it.
332 agenda All agenda files
333 agenda-with-archives All agenda files with any archive files associated
335 directory All files with the extension '.org' in the same
336 directory as the current file (includes the current
337 file if it is an .org file.)
338 (FILE1 FILE2 ...) If this is a list, all files in the list will be scanned.
340 ;; Note -- meanings differ slightly from the argument to org-map-entries:
341 ;; 'file' means current file/buffer, respecting any restriction
342 ;; 'file-no-restriction' means current file/buffer, ignoring restrictions
343 ;; 'directory' means all *.org files in current directory
345 :type
'(choice (const :tag
"The current buffer, respecting the restriction if any." file
)
346 (const :tag
"The subtree started with the entry at point" tree
)
347 (const :tag
"The current buffer, without restriction" file-no-restriction
)
348 (const :tag
"The current buffer, and any archives associated with it." file-with-archives
)
349 (const :tag
"All agenda files" agenda
)
350 (const :tag
"All agenda files with any archive files associated with them." agenda-with-archives
)
351 (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
)
352 (repeat :tag
"List of files to scan for drill items." file
)))
355 (defcustom org-drill-match
357 "If non-nil, a string specifying a tags/property/TODO query. During
358 drill sessions, only items that match this query will be considered."
360 :type
'(choice (const nil
) string
))
363 (defcustom org-drill-save-buffers-after-drill-sessions-p
365 "If non-nil, prompt to save all modified buffers after a drill session
371 (defcustom org-drill-spaced-repetition-algorithm
373 "Which SuperMemo spaced repetition algorithm to use for scheduling items.
374 Available choices are:
375 - SM2 :: the SM2 algorithm, used in SuperMemo 2.0
376 - SM5 :: the SM5 algorithm, used in SuperMemo 5.0
377 - Simple8 :: a modified version of the SM8 algorithm. SM8 is used in
378 SuperMemo 98. The version implemented here is simplified in that while it
379 'learns' the difficulty of each item using quality grades and number of
380 failures, it does not modify the matrix of values that
381 governs how fast the inter-repetition intervals increase. A method for
382 adjusting intervals when items are reviewed early or late has been taken
383 from SM11, a later version of the algorithm, and included in Simple8."
385 :type
'(choice (const sm2
) (const sm5
) (const simple8
)))
388 (defcustom org-drill-optimal-factor-matrix
390 "Obsolete and will be removed in future. The SM5 optimal factor
391 matrix data is now stored in the variable
392 `org-drill-sm5-optimal-factor-matrix'."
397 (defvar org-drill-sm5-optimal-factor-matrix
399 "DO NOT CHANGE THE VALUE OF THIS VARIABLE.
401 Persistent matrix of optimal factors, used by the SuperMemo SM5
402 algorithm. The matrix is saved at the end of each drill session.
404 Over time, values in the matrix will adapt to the individual user's
408 (add-to-list 'savehist-additional-variables
409 'org-drill-sm5-optimal-factor-matrix
)
410 (unless savehist-mode
414 (defun org-drill--transfer-optimal-factor-matrix ()
415 (if (and org-drill-optimal-factor-matrix
416 (null org-drill-sm5-optimal-factor-matrix
))
417 (setq org-drill-sm5-optimal-factor-matrix
418 org-drill-optimal-factor-matrix
)))
420 (add-hook 'after-init-hook
'org-drill--transfer-optimal-factor-matrix
)
423 (defcustom org-drill-sm5-initial-interval
425 "In the SM5 algorithm, the initial interval after the first
426 successful presentation of an item is always 4 days. If you wish to change
427 this, you can do so here."
432 (defcustom org-drill-add-random-noise-to-intervals-p
434 "If true, the number of days until an item's next repetition
435 will vary slightly from the interval calculated by the SM2
436 algorithm. The variation is very small when the interval is
437 small, but scales up with the interval."
442 (defcustom org-drill-adjust-intervals-for-early-and-late-repetitions-p
444 "If true, when the student successfully reviews an item 1 or more days
445 before or after the scheduled review date, this will affect that date of
446 the item's next scheduled review, according to the algorithm presented at
447 [[http://www.supermemo.com/english/algsm11.htm#Advanced%20repetitions]].
449 Items that were reviewed early will have their next review date brought
450 forward. Those that were reviewed late will have their next review
451 date postponed further.
453 Note that this option currently has no effect if the SM2 algorithm
459 (defcustom org-drill-cloze-text-weight
461 "For card types 'hide1_firstmore', 'show1_lastmore' and 'show1_firstless',
462 this number determines how often the 'less favoured' situation
463 should arise. It will occur 1 in every N trials, where N is the
464 value of the variable.
466 For example, with the hide1_firstmore card type, the first piece
467 of clozed text should be hidden more often than the other
468 pieces. If this variable is set to 4 (default), the first item
469 will only be shown 25% of the time (1 in 4 trials). Similarly for
470 show1_lastmore, the last item will be shown 75% of the time, and
471 for show1_firstless, the first item would only be shown 25% of the
474 If the value of this variable is NIL, then weighting is disabled, and
475 all weighted card types are treated as their unweighted equivalents."
477 :type
'(choice integer
(const nil
)))
480 (defcustom org-drill-cram-hours
482 "When in cram mode, items are considered due for review if
483 they were reviewed at least this many hours ago."
488 ;;; NEW items have never been presented in a drill session before.
489 ;;; MATURE items HAVE been presented at least once before.
490 ;;; - YOUNG mature items were scheduled no more than
491 ;;; ORG-DRILL-DAYS-BEFORE-OLD days after their last
492 ;;; repetition. These items will have been learned 'recently' and will have a
493 ;;; low repetition count.
494 ;;; - OLD mature items have intervals greater than
495 ;;; ORG-DRILL-DAYS-BEFORE-OLD.
496 ;;; - OVERDUE items are past their scheduled review date by more than
497 ;;; LAST-INTERVAL * (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) days,
498 ;;; regardless of young/old status.
501 (defcustom org-drill-days-before-old
503 "When an item's inter-repetition interval rises above this value in days,
504 it is no longer considered a 'young' (recently learned) item."
509 (defcustom org-drill-overdue-interval-factor
511 "An item is considered overdue if its scheduled review date is
512 more than (ORG-DRILL-OVERDUE-INTERVAL-FACTOR - 1) * LAST-INTERVAL
513 days in the past. For example, a value of 1.2 means an additional
514 20% of the last scheduled interval is allowed to elapse before
515 the item is overdue. A value of 1.0 means no extra time is
516 allowed at all - items are immediately considered overdue if
517 there is even one day's delay in reviewing them. This variable
518 should never be less than 1.0."
523 (defcustom org-drill-learn-fraction
525 "Fraction between 0 and 1 that governs how quickly the spaces
526 between successive repetitions increase, for all items. The
527 default value is 0.5. Higher values make spaces increase more
528 quickly with each successful repetition. You should only change
529 this in small increments (for example 0.05-0.1) as it has an
530 exponential effect on inter-repetition spacing."
535 (defvar drill-answer nil
536 "Global variable that can be bound to a correct answer when an
537 item is being presented. If this variable is non-nil, the default
538 presentation function will show its value instead of the default
539 behaviour of revealing the contents of the drilled item.
541 This variable is useful for card types that compute their answers
542 -- for example, a card type that asks the student to translate a
543 random number to another language. ")
546 (defvar *org-drill-session-qualities
* nil
)
547 (defvar *org-drill-start-time
* 0)
548 (defvar *org-drill-new-entries
* nil
)
549 (defvar *org-drill-dormant-entry-count
* 0)
550 (defvar *org-drill-due-entry-count
* 0)
551 (defvar *org-drill-overdue-entry-count
* 0)
552 (defvar *org-drill-due-tomorrow-count
* 0)
553 (defvar *org-drill-overdue-entries
* nil
554 "List of markers for items that are considered 'overdue', based on
555 the value of ORG-DRILL-OVERDUE-INTERVAL-FACTOR.")
556 (defvar *org-drill-young-mature-entries
* nil
557 "List of markers for mature entries whose last inter-repetition
558 interval was <= ORG-DRILL-DAYS-BEFORE-OLD days.")
559 (defvar *org-drill-old-mature-entries
* nil
560 "List of markers for mature entries whose last inter-repetition
561 interval was greater than ORG-DRILL-DAYS-BEFORE-OLD days.")
562 (defvar *org-drill-failed-entries
* nil
)
563 (defvar *org-drill-again-entries
* nil
)
564 (defvar *org-drill-done-entries
* nil
)
565 (defvar *org-drill-current-item
* nil
566 "Set to the marker for the item currently being tested.")
567 (defvar *org-drill-cram-mode
* nil
568 "Are we in 'cram mode', where all items are considered due
569 for review unless they were already reviewed in the recent past?")
570 (defvar org-drill-scheduling-properties
571 '("LEARN_DATA" "DRILL_LAST_INTERVAL" "DRILL_REPEATS_SINCE_FAIL"
572 "DRILL_TOTAL_REPEATS" "DRILL_FAILURE_COUNT" "DRILL_AVERAGE_QUALITY"
573 "DRILL_EASE" "DRILL_LAST_QUALITY" "DRILL_LAST_REVIEWED"))
574 (defvar org-drill--lapse-very-overdue-entries-p nil
575 "If non-nil, entries more than 90 days overdue are regarded as 'lapsed'.
576 This means that when the item is eventually re-tested it will be
577 treated as 'failed' (quality 2) for rescheduling purposes,
578 regardless of whether the test was successful.")
581 ;;; Make the above settings safe as file-local variables.
584 (put 'org-drill-question-tag
'safe-local-variable
'stringp
)
585 (put 'org-drill-maximum-items-per-session
'safe-local-variable
586 '(lambda (val) (or (integerp val
) (null val
))))
587 (put 'org-drill-maximum-duration
'safe-local-variable
588 '(lambda (val) (or (integerp val
) (null val
))))
589 (put 'org-drill-failure-quality
'safe-local-variable
'integerp
)
590 (put 'org-drill-forgetting-index
'safe-local-variable
'integerp
)
591 (put 'org-drill-leech-failure-threshold
'safe-local-variable
'integerp
)
592 (put 'org-drill-leech-method
'safe-local-variable
593 '(lambda (val) (memq val
'(nil skip warn
))))
594 (put 'org-drill-use-visible-cloze-face-p
'safe-local-variable
'booleanp
)
595 (put 'org-drill-hide-item-headings-p
'safe-local-variable
'booleanp
)
596 (put 'org-drill-spaced-repetition-algorithm
'safe-local-variable
597 '(lambda (val) (memq val
'(simple8 sm5 sm2
))))
598 (put 'org-drill-sm5-initial-interval
'safe-local-variable
'floatp
)
599 (put 'org-drill-add-random-noise-to-intervals-p
'safe-local-variable
'booleanp
)
600 (put 'org-drill-adjust-intervals-for-early-and-late-repetitions-p
601 'safe-local-variable
'booleanp
)
602 (put 'org-drill-cram-hours
'safe-local-variable
'integerp
)
603 (put 'org-drill-learn-fraction
'safe-local-variable
'floatp
)
604 (put 'org-drill-days-before-old
'safe-local-variable
'integerp
)
605 (put 'org-drill-overdue-interval-factor
'safe-local-variable
'floatp
)
606 (put 'org-drill-scope
'safe-local-variable
607 '(lambda (val) (or (symbolp val
) (listp val
))))
608 (put 'org-drill-match
'safe-local-variable
609 '(lambda (val) (or (stringp val
) (null val
))))
610 (put 'org-drill-save-buffers-after-drill-sessions-p
'safe-local-variable
'booleanp
)
611 (put 'org-drill-cloze-text-weight
'safe-local-variable
612 '(lambda (val) (or (null val
) (integerp val
))))
613 (put 'org-drill-left-cloze-delimiter
'safe-local-variable
'stringp
)
614 (put 'org-drill-right-cloze-delimiter
'safe-local-variable
'stringp
)
617 ;;;; Utilities ================================================================
620 (defun free-marker (m)
624 (defmacro pop-random
(place)
625 (let ((idx (cl-gensym)))
628 (let ((,idx
(random* (length ,place
))))
629 (prog1 (nth ,idx
,place
)
630 (setq ,place
(append (subseq ,place
0 ,idx
)
631 (subseq ,place
(1+ ,idx
)))))))))
634 (defmacro push-end
(val place
)
635 "Add VAL to the end of the sequence stored in PLACE. Return the new
637 `(setq ,place
(append ,place
(list ,val
))))
640 (defun shuffle-list (list)
641 "Randomly permute the elements of LIST (all permutations equally likely)."
642 ;; Adapted from 'shuffle-vector' in cookie1.el
648 (setq j
(+ i
(random* (- len i
))))
649 (setq temp
(nth i list
))
650 (setf (nth i list
) (nth j list
))
651 (setf (nth j list
) temp
)
656 (defun round-float (floatnum fix
)
657 "Round the floating point number FLOATNUM to FIX decimal places.
658 Example: (round-float 3.56755765 3) -> 3.568"
659 (let ((n (expt 10 fix
)))
660 (/ (float (round (* floatnum n
))) n
)))
663 (defun command-keybinding-to-string (cmd)
664 "Return a human-readable description of the key/keys to which the command
665 CMD is bound, or nil if it is not bound to a key."
666 (let ((key (where-is-internal cmd overriding-local-map t
)))
667 (if key
(key-description key
))))
670 (defun time-to-inactive-org-timestamp (time)
672 (concat "[" (substring (cdr org-time-stamp-formats
) 1 -
1) "]")
676 (defun time-to-active-org-timestamp (time)
678 (concat "<" (substring (cdr org-time-stamp-formats
) 1 -
1) ">")
682 (defun org-map-drill-entries (func &optional scope drill-match
&rest skip
)
683 "Like `org-map-entries', but only drill entries are processed."
684 (let ((org-drill-scope (or scope org-drill-scope
))
685 (org-drill-match (or drill-match org-drill-match
)))
686 (apply 'org-map-entries func
687 (concat "+" org-drill-question-tag
688 (if (and (stringp org-drill-match
)
689 (not (member '(?
+ ?- ?|
) (elt org-drill-match
0))))
691 (or org-drill-match
""))
692 (case org-drill-scope
694 (file-no-restriction 'file
)
696 (directory-files (file-name-directory (buffer-file-name))
702 (defmacro with-hidden-cloze-text
(&rest body
)
704 (org-drill-hide-clozed-text)
708 (org-drill-unhide-clozed-text))))
711 (defmacro with-hidden-cloze-hints
(&rest body
)
713 (org-drill-hide-cloze-hints)
717 (org-drill-unhide-text))))
720 (defmacro with-hidden-comments
(&rest body
)
722 (if org-drill-hide-item-headings-p
723 (org-drill-hide-heading-at-point))
724 (org-drill-hide-comments)
728 (org-drill-unhide-text))))
731 (defun org-drill-days-since-last-review ()
732 "Nil means a last review date has not yet been stored for
734 Zero means it was reviewed today.
735 A positive number means it was reviewed that many days ago.
736 A negative number means the date of last review is in the future --
737 this should never happen."
738 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
740 (- (time-to-days (current-time))
741 (time-to-days (apply 'encode-time
742 (org-parse-time-string datestr
)))))))
745 (defun org-drill-hours-since-last-review ()
746 "Like `org-drill-days-since-last-review', but return value is
747 in hours rather than days."
748 (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
751 (/ (- (time-to-seconds (current-time))
752 (time-to-seconds (apply 'encode-time
753 (org-parse-time-string datestr
))))
757 (defun org-drill-entry-p (&optional marker
)
758 "Is MARKER, or the point, in a 'drill item'? This will return nil if
759 the point is inside a subheading of a drill item -- to handle that
760 situation use `org-part-of-drill-entry-p'."
763 (org-drill-goto-entry marker
))
764 (member org-drill-question-tag
(org-get-local-tags))))
767 (defun org-drill-goto-entry (marker)
768 (switch-to-buffer (marker-buffer marker
))
772 (defun org-part-of-drill-entry-p ()
773 "Is the current entry either the main heading of a 'drill item',
774 or a subheading within a drill item?"
775 (or (org-drill-entry-p)
776 ;; Does this heading INHERIT the drill tag
777 (member org-drill-question-tag
(org-get-tags-at))))
780 (defun org-drill-goto-drill-entry-heading ()
781 "Move the point to the heading which holds the :drill: tag for this
783 (unless (org-at-heading-p)
784 (org-back-to-heading))
785 (unless (org-part-of-drill-entry-p)
786 (error "Point is not inside a drill entry"))
787 (while (not (org-drill-entry-p))
788 (unless (org-up-heading-safe)
789 (error "Cannot find a parent heading that is marked as a drill entry"))))
793 (defun org-drill-entry-leech-p ()
794 "Is the current entry a 'leech item'?"
795 (and (org-drill-entry-p)
796 (member "leech" (org-get-local-tags))))
799 ;; (defun org-drill-entry-due-p ()
801 ;; (*org-drill-cram-mode*
802 ;; (let ((hours (org-drill-hours-since-last-review)))
803 ;; (and (org-drill-entry-p)
805 ;; (>= hours org-drill-cram-hours)))))
807 ;; (let ((item-time (org-get-scheduled-time (point))))
808 ;; (and (org-drill-entry-p)
809 ;; (or (not (eql 'skip org-drill-leech-method))
810 ;; (not (org-drill-entry-leech-p)))
811 ;; (or (null item-time) ; not scheduled
812 ;; (not (minusp ; scheduled for today/in past
813 ;; (- (time-to-days (current-time))
814 ;; (time-to-days item-time))))))))))
817 (defun org-drill-entry-days-overdue ()
819 - NIL if the item is not to be regarded as scheduled for review at all.
820 This is the case if it is not a drill item, or if it is a leech item
821 that we wish to skip, or if we are in cram mode and have already reviewed
822 the item within the last few hours.
823 - 0 if the item is new, or if it scheduled for review today.
824 - A negative integer - item is scheduled that many days in the future.
825 - A positive integer - item is scheduled that many days in the past."
827 (*org-drill-cram-mode
*
828 (let ((hours (org-drill-hours-since-last-review)))
829 (and (org-drill-entry-p)
831 (>= hours org-drill-cram-hours
))
834 (let ((item-time (org-get-scheduled-time (point))))
836 ((or (not (org-drill-entry-p))
837 (and (eql 'skip org-drill-leech-method
)
838 (org-drill-entry-leech-p)))
840 ((null item-time
) ; not scheduled -> due now
843 (- (time-to-days (current-time))
844 (time-to-days item-time
))))))))
847 (defun org-drill-entry-overdue-p (&optional days-overdue last-interval
)
848 "Returns true if entry that is scheduled DAYS-OVERDUE dasy in the past,
849 and whose last inter-repetition interval was LAST-INTERVAL, should be
850 considered 'overdue'. If the arguments are not given they are extracted
851 from the entry at point."
853 (setq days-overdue
(org-drill-entry-days-overdue)))
854 (unless last-interval
855 (setq last-interval
(org-drill-entry-last-interval 1)))
856 (and (numberp days-overdue
)
857 (> days-overdue
1) ; enforce a sane minimum 'overdue' gap
858 ;;(> due org-drill-days-before-overdue)
859 (> (/ (+ days-overdue last-interval
1.0) last-interval
)
860 org-drill-overdue-interval-factor
)))
864 (defun org-drill-entry-due-p ()
865 (let ((due (org-drill-entry-days-overdue)))
866 (and (not (null due
))
867 (not (minusp due
)))))
870 (defun org-drill-entry-new-p ()
871 (and (org-drill-entry-p)
872 (let ((item-time (org-get-scheduled-time (point))))
876 (defun org-drill-entry-last-quality (&optional default
)
877 (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
879 (string-to-number quality
)
883 (defun org-drill-entry-failure-count ()
884 (let ((quality (org-entry-get (point) "DRILL_FAILURE_COUNT")))
886 (string-to-number quality
)
890 (defun org-drill-entry-average-quality (&optional default
)
891 (let ((val (org-entry-get (point) "DRILL_AVERAGE_QUALITY")))
893 (string-to-number val
)
896 (defun org-drill-entry-last-interval (&optional default
)
897 (let ((val (org-entry-get (point) "DRILL_LAST_INTERVAL")))
899 (string-to-number val
)
902 (defun org-drill-entry-repeats-since-fail (&optional default
)
903 (let ((val (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")))
905 (string-to-number val
)
908 (defun org-drill-entry-total-repeats (&optional default
)
909 (let ((val (org-entry-get (point) "DRILL_TOTAL_REPEATS")))
911 (string-to-number val
)
914 (defun org-drill-entry-ease (&optional default
)
915 (let ((val (org-entry-get (point) "DRILL_EASE")))
917 (string-to-number val
)
921 ;;; From http://www.supermemo.com/english/ol/sm5.htm
922 (defun org-drill-random-dispersal-factor ()
923 "Returns a random number between 0.5 and 1.5."
926 (p (- (random* 1.0) 0.5)))
931 (/ (+ 100 (* (* (/ -
1 b
) (log (- 1 (* (/ b a
) (abs p
)))))
935 (defun pseudonormal (mean variation
)
936 "Random numbers in a pseudo-normal distribution with mean MEAN, range
937 MEAN-VARIATION to MEAN+VARIATION"
938 (+ (random* variation
)
944 (defun org-drill-early-interval-factor (optimal-factor
948 - OPTIMAL-FACTOR: interval-factor if the item had been tested
949 exactly when it was supposed to be.
950 - OPTIMAL-INTERVAL: interval for next repetition (days) if the item had been
951 tested exactly when it was supposed to be.
952 - DAYS-AHEAD: how many days ahead of time the item was reviewed.
954 Returns an adjusted optimal factor which should be used to
955 calculate the next interval, instead of the optimal factor found
957 (let ((delta-ofmax (* (1- optimal-factor
)
958 (/ (+ optimal-interval
959 (* 0.6 optimal-interval
) -
1) (1- optimal-interval
)))))
961 (* delta-ofmax
(/ days-ahead
(+ days-ahead
(* 0.6 optimal-interval
)))))))
964 (defun org-drill-get-item-data ()
965 "Returns a list of 6 items, containing all the stored recall
966 data for the item at point:
967 - LAST-INTERVAL is the interval in days that was used to schedule the item's
969 - REPEATS is the number of items the item has been successfully recalled without
970 without any failures. It is reset to 0 upon failure to recall the item.
971 - FAILURES is the total number of times the user has failed to recall the item.
972 - TOTAL-REPEATS includes both successful and unsuccessful repetitions.
973 - AVERAGE-QUALITY is the mean quality of recall of the item over
974 all its repetitions, successful and unsuccessful.
975 - EASE is a number reflecting how easy the item is to learn. Higher is easier.
977 (let ((learn-str (org-entry-get (point) "LEARN_DATA"))
978 (repeats (org-drill-entry-total-repeats :missing
)))
981 (let ((learn-data (or (and learn-str
983 (copy-list initial-repetition-state
))))
984 (list (nth 0 learn-data
) ; last interval
985 (nth 1 learn-data
) ; repetitions
986 (org-drill-entry-failure-count)
988 (org-drill-entry-last-quality)
989 (nth 2 learn-data
) ; EF
991 ((not (eql :missing repeats
))
992 (list (org-drill-entry-last-interval)
993 (org-drill-entry-repeats-since-fail)
994 (org-drill-entry-failure-count)
995 (org-drill-entry-total-repeats)
996 (org-drill-entry-average-quality)
997 (org-drill-entry-ease)))
999 (list 0 0 0 0 nil nil
)))))
1002 (defun org-drill-store-item-data (last-interval repeats failures
1005 "Stores the given data in the item at point."
1006 (org-entry-delete (point) "LEARN_DATA")
1007 (org-set-property "DRILL_LAST_INTERVAL"
1008 (number-to-string (round-float last-interval
4)))
1009 (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats
))
1010 (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats
))
1011 (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures
))
1012 (org-set-property "DRILL_AVERAGE_QUALITY"
1013 (number-to-string (round-float meanq
3)))
1014 (org-set-property "DRILL_EASE"
1015 (number-to-string (round-float ease
3))))
1019 ;;; SM2 Algorithm =============================================================
1022 (defun determine-next-interval-sm2 (last-interval n ef quality
1023 failures meanq total-repeats
)
1025 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1026 - REPEATS -- the number of times the item has been successfully reviewed
1027 - EF -- the 'easiness factor'
1030 Returns a list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX), where:
1031 - INTERVAL is the number of days until the item should next be reviewed
1032 - REPEATS is incremented by 1.
1033 - EF is modified based on the recall quality for the item.
1034 - OF-MATRIX is not modified."
1036 (assert (and (>= quality
0) (<= quality
5)))
1037 (if (<= quality org-drill-failure-quality
)
1038 ;; When an item is failed, its interval is reset to 0,
1039 ;; but its EF is unchanged
1040 (list -
1 1 ef
(1+ failures
) meanq
(1+ total-repeats
)
1041 org-drill-sm5-optimal-factor-matrix
)
1043 (let* ((next-ef (modify-e-factor ef quality
))
1049 (org-drill-add-random-noise-to-intervals-p
1057 (t (* last-interval next-ef
)))))
1058 (list (if org-drill-add-random-noise-to-intervals-p
1059 (+ last-interval
(* (- interval last-interval
)
1060 (org-drill-random-dispersal-factor)))
1064 failures meanq
(1+ total-repeats
)
1065 org-drill-sm5-optimal-factor-matrix
))))
1068 ;;; SM5 Algorithm =============================================================
1072 (defun initial-optimal-factor-sm5 (n ef
)
1074 org-drill-sm5-initial-interval
1077 (defun get-optimal-factor-sm5 (n ef of-matrix
)
1078 (let ((factors (assoc n of-matrix
)))
1080 (let ((ef-of (assoc ef
(cdr factors
))))
1081 (and ef-of
(cdr ef-of
))))
1082 (initial-optimal-factor-sm5 n ef
))))
1085 (defun inter-repetition-interval-sm5 (last-interval n ef
&optional of-matrix
)
1086 (let ((of (get-optimal-factor-sm5 n ef
(or of-matrix
1087 org-drill-sm5-optimal-factor-matrix
))))
1090 (* of last-interval
))))
1093 (defun determine-next-interval-sm5 (last-interval n ef quality
1094 failures meanq total-repeats
1095 of-matrix
&optional delta-days
)
1096 (if (zerop n
) (setq n
1))
1097 (if (null ef
) (setq ef
2.5))
1099 (assert (and (>= quality
0) (<= quality
5)))
1101 (setq of-matrix org-drill-sm5-optimal-factor-matrix
))
1102 (setq of-matrix
(cl-copy-tree of-matrix
))
1104 (setq meanq
(if meanq
1105 (/ (+ quality
(* meanq total-repeats
1.0))
1109 (let ((next-ef (modify-e-factor ef quality
))
1111 (new-of (modify-of (get-optimal-factor-sm5 n ef of-matrix
)
1112 quality org-drill-learn-fraction
))
1114 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1115 delta-days
(minusp delta-days
))
1116 (setq new-of
(org-drill-early-interval-factor
1117 (get-optimal-factor-sm5 n ef of-matrix
)
1118 (inter-repetition-interval-sm5
1119 last-interval n ef of-matrix
)
1123 (set-optimal-factor n next-ef of-matrix
1124 (round-float new-of
3))) ; round OF to 3 d.p.
1129 ;; "Failed" -- reset repetitions to 0,
1130 ((<= quality org-drill-failure-quality
)
1131 (list -
1 1 old-ef
(1+ failures
) meanq
(1+ total-repeats
)
1132 of-matrix
)) ; Not clear if OF matrix is supposed to be
1134 ;; For a zero-based quality of 4 or 5, don't repeat
1135 ;; ((and (>= quality 4)
1136 ;; (not org-learn-always-reschedule))
1137 ;; (list 0 (1+ n) ef failures meanq
1138 ;; (1+ total-repeats) of-matrix)) ; 0 interval = unschedule
1140 (setq interval
(inter-repetition-interval-sm5
1141 last-interval n ef of-matrix
))
1142 (if org-drill-add-random-noise-to-intervals-p
1143 (setq interval
(* interval
(org-drill-random-dispersal-factor))))
1153 ;;; Simple8 Algorithm =========================================================
1156 (defun org-drill-simple8-first-interval (failures)
1158 - FAILURES: integer >= 0. The total number of times the item has
1159 been forgotten, ever.
1161 Returns the optimal FIRST interval for an item which has previously been
1162 forgotten on FAILURES occasions."
1163 (* 2.4849 (exp (* -
0.057 failures
))))
1166 (defun org-drill-simple8-interval-factor (ease repetition
)
1168 - EASE: floating point number >= 1.2. Corresponds to `AF' in SM8 algorithm.
1169 - REPETITION: the number of times the item has been tested.
1170 1 is the first repetition (ie the second trial).
1172 The factor by which the last interval should be
1173 multiplied to give the next interval. Corresponds to `RF' or `OF'."
1174 (+ 1.2 (* (- ease
1.2) (expt org-drill-learn-fraction
(log repetition
2)))))
1177 (defun org-drill-simple8-quality->ease
(quality)
1178 "Returns the ease (`AF' in the SM8 algorithm) which corresponds
1179 to a mean item quality of QUALITY."
1180 (+ (* 0.0542 (expt quality
4))
1181 (* -
0.4848 (expt quality
3))
1182 (* 1.4916 (expt quality
2))
1187 (defun determine-next-interval-simple8 (last-interval repeats quality
1188 failures meanq totaln
1189 &optional delta-days
)
1191 - LAST-INTERVAL -- the number of days since the item was last reviewed.
1192 - REPEATS -- the number of times the item has been successfully reviewed
1193 - EASE -- the 'easiness factor'
1195 - DELTA-DAYS -- how many days overdue was the item when it was reviewed.
1196 0 = reviewed on the scheduled day. +N = N days overdue.
1197 -N = reviewed N days early.
1199 Returns the new item data, as a list of 6 values:
1206 See the documentation for `org-drill-get-item-data' for a description of these."
1207 (assert (>= repeats
0))
1208 (assert (and (>= quality
0) (<= quality
5)))
1209 (assert (or (null meanq
) (and (>= meanq
0) (<= meanq
5))))
1210 (let ((next-interval nil
))
1211 (setf meanq
(if meanq
1212 (/ (+ quality
(* meanq totaln
1.0)) (1+ totaln
))
1215 ((<= quality org-drill-failure-quality
)
1219 ((or (zerop repeats
)
1220 (zerop last-interval
))
1221 (setf next-interval
(org-drill-simple8-first-interval failures
))
1227 org-drill-adjust-intervals-for-early-and-late-repetitions-p
1228 (numberp delta-days
) (plusp delta-days
)
1229 (plusp last-interval
))
1230 (+ repeats
(min 1 (/ delta-days last-interval
1.0)))
1232 (factor (org-drill-simple8-interval-factor
1233 (org-drill-simple8-quality->ease meanq
) use-n
))
1234 (next-int (* last-interval factor
)))
1235 (when (and org-drill-adjust-intervals-for-early-and-late-repetitions-p
1236 (numberp delta-days
) (minusp delta-days
))
1237 ;; The item was reviewed earlier than scheduled.
1238 (setf factor
(org-drill-early-interval-factor
1239 factor next-int
(abs delta-days
))
1240 next-int
(* last-interval factor
)))
1241 (setf next-interval next-int
)
1245 (if (and org-drill-add-random-noise-to-intervals-p
1246 (plusp next-interval
))
1247 (* next-interval
(org-drill-random-dispersal-factor))
1250 (org-drill-simple8-quality->ease meanq
)
1259 ;;; Essentially copied from `org-learn.el', but modified to
1260 ;;; optionally call the SM2 or simple8 functions.
1261 (defun org-drill-smart-reschedule (quality &optional days-ahead
)
1262 "If DAYS-AHEAD is supplied it must be a positive integer. The
1263 item will be scheduled exactly this many days into the future."
1264 (let ((delta-days (- (time-to-days (current-time))
1265 (time-to-days (or (org-get-scheduled-time (point))
1267 (ofmatrix org-drill-sm5-optimal-factor-matrix
)
1268 ;; Entries can have weights, 1 by default. Intervals are divided by the
1269 ;; item's weight, so an item with a weight of 2 will have all intervals
1270 ;; halved, meaning you will end up reviewing it twice as often.
1271 ;; Useful for entries which randomly present any of several facts.
1272 (weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1273 (if (stringp weight
)
1274 (setq weight
(read weight
)))
1275 (destructuring-bind (last-interval repetitions failures
1276 total-repeats meanq ease
)
1277 (org-drill-get-item-data)
1278 (destructuring-bind (next-interval repetitions ease
1279 failures meanq total-repeats
1280 &optional new-ofmatrix
)
1281 (case org-drill-spaced-repetition-algorithm
1282 (sm5 (determine-next-interval-sm5 last-interval repetitions
1283 ease quality failures
1284 meanq total-repeats ofmatrix
))
1285 (sm2 (determine-next-interval-sm2 last-interval repetitions
1286 ease quality failures
1287 meanq total-repeats
))
1288 (simple8 (determine-next-interval-simple8 last-interval repetitions
1289 quality failures meanq
1292 (if (numberp days-ahead
)
1293 (setq next-interval days-ahead
))
1295 (if (and (null days-ahead
)
1296 (numberp weight
) (plusp weight
)
1297 (not (minusp next-interval
)))
1299 (max 1.0 (+ last-interval
1300 (/ (- next-interval last-interval
) weight
)))))
1302 (org-drill-store-item-data next-interval repetitions failures
1303 total-repeats meanq ease
)
1305 (if (eql 'sm5 org-drill-spaced-repetition-algorithm
)
1306 (setq org-drill-sm5-optimal-factor-matrix new-ofmatrix
))
1310 (org-schedule '(4)))
1311 ((minusp days-ahead
)
1312 (org-schedule nil
(current-time)))
1314 (org-schedule nil
(time-add (current-time)
1316 (round next-interval
))))))))))
1319 (defun org-drill-hypothetical-next-review-date (quality)
1320 "Returns an integer representing the number of days into the future
1321 that the current item would be scheduled, based on a recall quality
1323 (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")))
1324 (destructuring-bind (last-interval repetitions failures
1325 total-repeats meanq ease
)
1326 (org-drill-get-item-data)
1327 (if (stringp weight
)
1328 (setq weight
(read weight
)))
1329 (destructuring-bind (next-interval repetitions ease
1330 failures meanq total-repeats
1332 (case org-drill-spaced-repetition-algorithm
1333 (sm5 (determine-next-interval-sm5 last-interval repetitions
1334 ease quality failures
1336 org-drill-sm5-optimal-factor-matrix
))
1337 (sm2 (determine-next-interval-sm2 last-interval repetitions
1338 ease quality failures
1339 meanq total-repeats
))
1340 (simple8 (determine-next-interval-simple8 last-interval repetitions
1341 quality failures meanq
1344 ((not (plusp next-interval
))
1346 ((and (numberp weight
) (plusp weight
))
1348 (max 1.0 (/ (- next-interval last-interval
) weight
))))
1353 (defun org-drill-hypothetical-next-review-dates ()
1354 (let ((intervals nil
))
1356 (push (max (or (car intervals
) 0)
1357 (org-drill-hypothetical-next-review-date q
))
1359 (reverse intervals
)))
1362 (defun org-drill-reschedule ()
1363 "Returns quality rating (0-5), or nil if the user quit."
1366 (next-review-dates (org-drill-hypothetical-next-review-dates))
1367 (key-prompt (format "(0-5, %c=help, %c=edit, %c=tags, %c=quit)"
1371 org-drill--quit-key
)))
1373 (while (not (memq ch
(list org-drill--quit-key
1376 ?
0 ?
1 ?
2 ?
3 ?
4 ?
5)))
1377 (setq input
(read-key-sequence
1378 (if (eq ch org-drill--help-key
)
1379 (format "0-2 Means you have forgotten the item.
1380 3-5 Means you have remembered the item.
1382 0 - Completely forgot.
1383 1 - Even after seeing the answer, it still took a bit to sink in.
1384 2 - After seeing the answer, you remembered it.
1385 3 - It took you awhile, but you finally remembered. (+%s days)
1386 4 - After a little bit of thought you remembered. (+%s days)
1387 5 - You remembered the item really easily. (+%s days)
1389 How well did you do? %s"
1390 (round (nth 3 next-review-dates
))
1391 (round (nth 4 next-review-dates
))
1392 (round (nth 5 next-review-dates
))
1394 (format "How well did you do? %s" key-prompt
))))
1397 (setq ch
(elt input
0)))
1398 ((and (vectorp input
) (symbolp (elt input
0)))
1400 (up (ignore-errors (forward-line -
1)))
1401 (down (ignore-errors (forward-line 1)))
1402 (left (ignore-errors (backward-char)))
1403 (right (ignore-errors (forward-char)))
1404 (prior (ignore-errors (scroll-down))) ; pgup
1405 (next (ignore-errors (scroll-up))))) ; pgdn
1406 ((and (vectorp input
) (listp (elt input
0))
1407 (eventp (elt input
0)))
1408 (case (car (elt input
0))
1409 (wheel-up (ignore-errors (mwheel-scroll (elt input
0))))
1410 (wheel-down (ignore-errors (mwheel-scroll (elt input
0)))))))
1411 (if (eql ch org-drill--tags-key
)
1412 (org-set-tags-command))))
1414 ((and (>= ch ?
0) (<= ch ?
5))
1415 (let ((quality (- ch ?
0))
1416 (failures (org-drill-entry-failure-count)))
1417 (unless *org-drill-cram-mode
*
1419 (let ((quality (if (org-drill--entry-lapsed-p) 2 quality
)))
1420 (org-drill-smart-reschedule quality
1421 (nth quality next-review-dates
))))
1422 (push quality
*org-drill-session-qualities
*)
1424 ((<= quality org-drill-failure-quality
)
1425 (when org-drill-leech-failure-threshold
1426 ;;(setq failures (if failures (string-to-number failures) 0))
1427 ;; (org-set-property "DRILL_FAILURE_COUNT"
1428 ;; (format "%d" (1+ failures)))
1429 (if (> (1+ failures
) org-drill-leech-failure-threshold
)
1430 (org-toggle-tag "leech" 'on
))))
1432 (let ((scheduled-time (org-get-scheduled-time (point))))
1433 (when scheduled-time
1434 (message "Next review in %d days"
1435 (- (time-to-days scheduled-time
)
1436 (time-to-days (current-time))))
1438 (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality
))
1439 (org-set-property "DRILL_LAST_REVIEWED"
1440 (time-to-inactive-org-timestamp (current-time))))
1442 ((= ch org-drill--edit-key
)
1448 ;; (defun org-drill-hide-all-subheadings-except (heading-list)
1449 ;; "Returns a list containing the position of each immediate subheading of
1450 ;; the current topic."
1451 ;; (let ((drill-entry-level (org-current-level))
1452 ;; (drill-sections nil)
1453 ;; (drill-heading nil))
1454 ;; (org-show-subtree)
1458 ;; (when (and (not (org-invisible-p))
1459 ;; (> (org-current-level) drill-entry-level))
1460 ;; (setq drill-heading (org-get-heading t))
1461 ;; (unless (and (= (org-current-level) (1+ drill-entry-level))
1462 ;; (member drill-heading heading-list))
1464 ;; (push (point) drill-sections)))
1466 ;; (reverse drill-sections)))
1470 (defun org-drill-hide-subheadings-if (test)
1471 "TEST is a function taking no arguments. TEST will be called for each
1472 of the immediate subheadings of the current drill item, with the point
1473 on the relevant subheading. TEST should return nil if the subheading is
1474 to be revealed, non-nil if it is to be hidden.
1475 Returns a list containing the position of each immediate subheading of
1477 (let ((drill-entry-level (org-current-level))
1478 (drill-sections nil
))
1483 (when (and (not (org-invisible-p))
1484 (> (org-current-level) drill-entry-level
))
1485 (when (or (/= (org-current-level) (1+ drill-entry-level
))
1488 (push (point) drill-sections
)))
1490 (reverse drill-sections
)))
1493 (defun org-drill-hide-all-subheadings-except (heading-list)
1494 (org-drill-hide-subheadings-if
1495 (lambda () (let ((drill-heading (org-get-heading t
)))
1496 (not (member drill-heading heading-list
))))))
1499 (defun org-drill-presentation-prompt (&rest fmt-and-args
)
1500 (let* ((item-start-time (current-time))
1504 (mature-entry-count (+ (length *org-drill-young-mature-entries
*)
1505 (length *org-drill-old-mature-entries
*)
1506 (length *org-drill-overdue-entries
*)))
1507 (status (first (org-drill-entry-status)))
1511 (first fmt-and-args
)
1512 (rest fmt-and-args
))
1513 (format (concat "Press key for answer, "
1514 "%c=edit, %c=tags, %c=skip, %c=quit.")
1518 org-drill--quit-key
))))
1520 (format "%s %s %s %s %s %s"
1524 ((eql status
:failed
) ?F
)
1525 (*org-drill-cram-mode
* ?C
)
1528 (:new ?N
) (:young ?Y
) (:old ?o
) (:overdue ?
!)
1532 (:new org-drill-new-count-color
)
1533 ((:young
:old
) org-drill-mature-count-color
)
1534 ((:overdue
:failed
) org-drill-failed-count-color
)
1535 (t org-drill-done-count-color
))))
1537 (number-to-string (length *org-drill-done-entries
*))
1538 'face
`(:foreground
,org-drill-done-count-color
)
1539 'help-echo
"The number of items you have reviewed this session.")
1541 (number-to-string (+ (length *org-drill-again-entries
*)
1542 (length *org-drill-failed-entries
*)))
1543 'face
`(:foreground
,org-drill-failed-count-color
)
1544 'help-echo
(concat "The number of items that you failed, "
1545 "and need to review again."))
1547 (number-to-string mature-entry-count
)
1548 'face
`(:foreground
,org-drill-mature-count-color
)
1549 'help-echo
"The number of old items due for review.")
1551 (number-to-string (length *org-drill-new-entries
*))
1552 'face
`(:foreground
,org-drill-new-count-color
)
1553 'help-echo
(concat "The number of new items that you "
1554 "have never reviewed."))
1556 (if (and (eql 'warn org-drill-leech-method
)
1557 (org-drill-entry-leech-p))
1558 (setq prompt
(concat
1559 (propertize "!!! LEECH ITEM !!!
1560 You seem to be having a lot of trouble memorising this item.
1561 Consider reformulating the item to make it easier to remember.\n"
1562 'face
'(:foreground
"red"))
1564 (while (memq ch
'(nil org-drill--tags-key
))
1566 (while (not (input-pending-p))
1567 (let ((elapsed (time-subtract (current-time) item-start-time
)))
1568 (message (concat (if (>= (time-to-seconds elapsed
) (* 60 60))
1570 (format-time-string "%M:%S " elapsed
))
1573 (setq input
(read-key-sequence nil
))
1574 (if (stringp input
) (setq ch
(elt input
0)))
1575 (if (eql ch org-drill--tags-key
)
1576 (org-set-tags-command)))
1578 (org-drill--quit-key nil
)
1579 (org-drill--edit-key 'edit
)
1580 (org-drill--skip-key 'skip
)
1584 (defun org-pos-in-regexp (pos regexp
&optional nlines
)
1587 (org-in-regexp regexp nlines
)))
1590 (defun org-drill-hide-region (beg end
&optional text
)
1591 "Hide the buffer region between BEG and END with an 'invisible text'
1592 visual overlay, or with the string TEXT if it is supplied."
1593 (let ((ovl (make-overlay beg end
)))
1594 (overlay-put ovl
'category
1595 'org-drill-hidden-text-overlay
)
1596 (overlay-put ovl
'priority
9999)
1597 (when (stringp text
)
1598 (overlay-put ovl
'invisible nil
)
1599 (overlay-put ovl
'face
'default
)
1600 (overlay-put ovl
'display text
))))
1603 (defun org-drill-hide-heading-at-point (&optional text
)
1604 (unless (org-at-heading-p)
1605 (error "Point is not on a heading."))
1607 (let ((beg (point)))
1609 (org-drill-hide-region beg
(point) text
))))
1612 (defun org-drill-hide-comments ()
1614 (while (re-search-forward "^#.*$" nil t
)
1615 (org-drill-hide-region (match-beginning 0) (match-end 0)))))
1618 (defun org-drill-unhide-text ()
1619 ;; This will also unhide the item's heading.
1621 (dolist (ovl (overlays-in (point-min) (point-max)))
1622 (when (eql 'org-drill-hidden-text-overlay
(overlay-get ovl
'category
))
1623 (delete-overlay ovl
)))))
1626 (defun org-drill-hide-clozed-text ()
1628 (while (re-search-forward org-drill-cloze-regexp nil t
)
1630 ;; - org links, partly because they might contain inline
1631 ;; images which we want to keep visible.
1632 ;; - LaTeX math fragments
1633 ;; - the contents of SRC blocks
1634 (unless (save-match-data
1635 (or (org-pos-in-regexp (match-beginning 0)
1636 org-bracket-link-regexp
1)
1637 (org-in-src-block-p)
1638 (org-inside-LaTeX-fragment-p)))
1639 (org-drill-hide-matched-cloze-text)))))
1642 (defun org-drill-hide-matched-cloze-text ()
1643 "Hide the current match with a 'cloze' visual overlay."
1644 (let ((ovl (make-overlay (match-beginning 0) (match-end 0)))
1645 (hint-sep-pos (string-match-p (regexp-quote org-drill-hint-separator
)
1647 (overlay-put ovl
'category
1648 'org-drill-cloze-overlay-defaults
)
1649 (overlay-put ovl
'priority
9999)
1650 (when (and hint-sep-pos
1652 (let ((hint (substring-no-properties
1654 (+ hint-sep-pos
(length org-drill-hint-separator
))
1655 (1- (length (match-string 0))))))
1658 ;; If hint is like `X...' then display [X...]
1659 ;; otherwise display [...X]
1660 (format (if (string-match-p (regexp-quote "...") hint
) "[%s]" "[%s...]")
1664 (defun org-drill-hide-cloze-hints ()
1666 (while (re-search-forward org-drill-cloze-regexp nil t
)
1667 (unless (or (save-match-data
1668 (org-pos-in-regexp (match-beginning 0)
1669 org-bracket-link-regexp
1))
1670 (null (match-beginning 2))) ; hint subexpression matched
1671 (org-drill-hide-region (match-beginning 2) (match-end 2))))))
1674 (defmacro with-replaced-entry-text
(text &rest body
)
1675 "During the execution of BODY, the entire text of the current entry is
1676 concealed by an overlay that displays the string TEXT."
1678 (org-drill-replace-entry-text ,text
)
1682 (org-drill-unreplace-entry-text))))
1685 (defmacro with-replaced-entry-text-multi
(replacements &rest body
)
1686 "During the execution of BODY, the entire text of the current entry is
1687 concealed by an overlay that displays the overlays in REPLACEMENTS."
1689 (org-drill-replace-entry-text ,replacements t
)
1693 (org-drill-unreplace-entry-text))))
1696 (defun org-drill-replace-entry-text (text &optional multi-p
)
1697 "Make an overlay that conceals the entire text of the item, not
1698 including properties or the contents of subheadings. The overlay shows
1700 If MULTI-P is non-nil, TEXT must be a list of values which are legal
1701 for the `display' text property. The text of the item will be temporarily
1702 replaced by all of these items, in the order in which they appear in
1704 Note: does not actually alter the item."
1708 (org-drill-replace-entry-text-multi text
))
1710 (let ((ovl (make-overlay (point-min)
1712 (outline-next-heading)
1714 (overlay-put ovl
'priority
9999)
1715 (overlay-put ovl
'category
1716 'org-drill-replaced-text-overlay
)
1717 (overlay-put ovl
'display text
)))))
1720 (defun org-drill-unreplace-entry-text ()
1722 (dolist (ovl (overlays-in (point-min) (point-max)))
1723 (when (eql 'org-drill-replaced-text-overlay
(overlay-get ovl
'category
))
1724 (delete-overlay ovl
)))))
1727 (defun org-drill-replace-entry-text-multi (replacements)
1728 "Make overlays that conceal the entire text of the item, not
1729 including properties or the contents of subheadings. The overlay shows
1731 Note: does not actually alter the item."
1734 (p-max (save-excursion
1735 (outline-next-heading)
1737 (assert (>= (- p-max p-min
) (length replacements
)))
1738 (dotimes (i (length replacements
))
1739 (setq ovl
(make-overlay (+ p-min
(* 2 i
))
1740 (if (= i
(1- (length replacements
)))
1742 (+ p-min
(* 2 i
) 1))))
1743 (overlay-put ovl
'priority
9999)
1744 (overlay-put ovl
'category
1745 'org-drill-replaced-text-overlay
)
1746 (overlay-put ovl
'display
(nth i replacements
)))))
1749 (defmacro with-replaced-entry-heading
(heading &rest body
)
1751 (org-drill-replace-entry-heading ,heading
)
1755 (org-drill-unhide-text))))
1758 (defun org-drill-replace-entry-heading (heading)
1759 "Make an overlay that conceals the heading of the item. The overlay shows
1761 Note: does not actually alter the item."
1762 (org-drill-hide-heading-at-point heading
))
1765 (defun org-drill-unhide-clozed-text ()
1767 (dolist (ovl (overlays-in (point-min) (point-max)))
1768 (when (eql 'org-drill-cloze-overlay-defaults
(overlay-get ovl
'category
))
1769 (delete-overlay ovl
)))))
1772 (defun org-drill-get-entry-text (&optional keep-properties-p
)
1773 (let ((text (org-agenda-get-some-entry-text (point-marker) 100)))
1774 (if keep-properties-p
1776 (substring-no-properties text
))))
1779 ;; (defun org-entry-empty-p ()
1780 ;; (zerop (length (org-drill-get-entry-text))))
1782 ;; This version is about 5x faster than the old version, above.
1783 (defun org-entry-empty-p ()
1785 (org-back-to-heading t
)
1786 (let ((lim (save-excursion
1787 (outline-next-heading) (point))))
1788 (if (fboundp 'org-end-of-meta-data-and-drawers
)
1789 (org-end-of-meta-data-and-drawers) ; function removed Feb 2015
1790 (org-end-of-meta-data t
))
1791 (or (>= (point) lim
)
1792 (null (re-search-forward "[[:graph:]]" lim t
))))))
1794 (defun org-drill-entry-empty-p () (org-entry-empty-p))
1797 ;;; Presentation functions ====================================================
1799 ;; Each of these is called with point on topic heading. Each needs to show the
1800 ;; topic in the form of a 'question' or with some information 'hidden', as
1801 ;; appropriate for the card type. The user should then be prompted to press a
1802 ;; key. The function should then reveal either the 'answer' or the entire
1803 ;; topic, and should return t if the user chose to see the answer and rate their
1804 ;; recall, nil if they chose to quit.
1807 (defun org-drill-present-simple-card ()
1808 (with-hidden-comments
1809 (with-hidden-cloze-hints
1810 (with-hidden-cloze-text
1811 (org-drill-hide-all-subheadings-except nil
)
1812 (org-drill--show-latex-fragments) ; overlay all LaTeX fragments with images
1814 (org-display-inline-images t
))
1815 (org-cycle-hide-drawers 'all
)
1816 (prog1 (org-drill-presentation-prompt)
1817 (org-drill-hide-subheadings-if 'org-drill-entry-p
))))))
1820 (defun org-drill-present-default-answer (reschedule-fn)
1823 (with-replaced-entry-text
1824 (format "\nAnswer:\n\n %s\n" drill-answer
)
1826 (funcall reschedule-fn
)
1827 (setq drill-answer nil
))))
1829 (org-drill-hide-subheadings-if 'org-drill-entry-p
)
1830 (org-drill-unhide-clozed-text)
1831 (org-drill--show-latex-fragments)
1833 (org-display-inline-images t
))
1834 (org-cycle-hide-drawers 'all
)
1835 (with-hidden-cloze-hints
1836 (funcall reschedule-fn
)))))
1839 (defun org-drill--show-latex-fragments ()
1840 (org-remove-latex-fragment-image-overlays)
1841 (if (fboundp 'org-toggle-latex-fragment
)
1842 (org-toggle-latex-fragment '(4))
1843 (org-preview-latex-fragment '(4))))
1846 (defun org-drill-present-two-sided-card ()
1847 (with-hidden-comments
1848 (with-hidden-cloze-hints
1849 (with-hidden-cloze-text
1850 (let ((drill-sections (org-drill-hide-all-subheadings-except nil
)))
1851 (when drill-sections
1853 (goto-char (nth (random* (min 2 (length drill-sections
)))
1855 (org-show-subtree)))
1856 (org-drill--show-latex-fragments)
1858 (org-display-inline-images t
))
1859 (org-cycle-hide-drawers 'all
)
1860 (prog1 (org-drill-presentation-prompt)
1861 (org-drill-hide-subheadings-if 'org-drill-entry-p
)))))))
1865 (defun org-drill-present-multi-sided-card ()
1866 (with-hidden-comments
1867 (with-hidden-cloze-hints
1868 (with-hidden-cloze-text
1869 (let ((drill-sections (org-drill-hide-all-subheadings-except nil
)))
1870 (when drill-sections
1872 (goto-char (nth (random* (length drill-sections
)) drill-sections
))
1873 (org-show-subtree)))
1874 (org-drill--show-latex-fragments)
1876 (org-display-inline-images t
))
1877 (org-cycle-hide-drawers 'all
)
1878 (prog1 (org-drill-presentation-prompt)
1879 (org-drill-hide-subheadings-if 'org-drill-entry-p
)))))))
1882 (defun org-drill-present-multicloze-hide-n (number-to-hide
1887 "Hides NUMBER-TO-HIDE pieces of text that are marked for cloze deletion,
1889 If NUMBER-TO-HIDE is negative, show only (ABS NUMBER-TO-HIDE) pieces,
1890 hiding all the rest.
1891 If FORCE-HIDE-FIRST is non-nil, force the first piece of text to be one of
1893 If FORCE-SHOW-FIRST is non-nil, never hide the first piece of text.
1894 If FORCE-SHOW-LAST is non-nil, never hide the last piece of text.
1895 If the number of text pieces in the item is less than
1896 NUMBER-TO-HIDE, then all text pieces will be hidden (except the first or last
1897 items if FORCE-SHOW-FIRST or FORCE-SHOW-LAST is non-nil)."
1898 (with-hidden-comments
1899 (with-hidden-cloze-hints
1900 (let ((item-end nil
)
1902 (body-start (or (cdr (org-get-property-block))
1904 (if (and force-hide-first force-show-first
)
1905 (error "FORCE-HIDE-FIRST and FORCE-SHOW-FIRST are mutually exclusive"))
1906 (org-drill-hide-all-subheadings-except nil
)
1908 (outline-next-heading)
1909 (setq item-end
(point)))
1911 (goto-char body-start
)
1912 (while (re-search-forward org-drill-cloze-regexp item-end t
)
1913 (let ((in-regexp?
(save-match-data
1914 (org-pos-in-regexp (match-beginning 0)
1915 org-bracket-link-regexp
1))))
1916 (unless (or in-regexp?
1917 (org-inside-LaTeX-fragment-p))
1918 (incf match-count
)))))
1919 (if (minusp number-to-hide
)
1920 (setq number-to-hide
(+ match-count number-to-hide
)))
1921 (when (plusp match-count
)
1922 (let* ((positions (shuffle-list (loop for i from
1
1927 (if force-hide-first
1928 ;; Force '1' to be in the list, and to be the first item
1930 (setq positions
(cons 1 (remove 1 positions
))))
1931 (if force-show-first
1932 (setq positions
(remove 1 positions
)))
1934 (setq positions
(remove match-count positions
)))
1937 0 (min number-to-hide
(length positions
))))
1938 ;; (dolist (pos-to-hide match-nums)
1940 (goto-char body-start
)
1942 (while (re-search-forward org-drill-cloze-regexp item-end t
)
1943 (unless (save-match-data
1944 (or (org-pos-in-regexp (match-beginning 0)
1945 org-bracket-link-regexp
1)
1946 (org-inside-LaTeX-fragment-p)))
1948 (if (memq cnt match-nums
)
1949 (org-drill-hide-matched-cloze-text)))))))
1951 ;; do (re-search-forward org-drill-cloze-regexp
1952 ;; item-end t pos-to-hide)
1953 ;; while (org-pos-in-regexp (match-beginning 0)
1954 ;; org-bracket-link-regexp 1))
1955 ;; (org-drill-hide-matched-cloze-text)))))
1956 (org-drill--show-latex-fragments)
1958 (org-display-inline-images t
))
1959 (org-cycle-hide-drawers 'all
)
1960 (prog1 (org-drill-presentation-prompt)
1961 (org-drill-hide-subheadings-if 'org-drill-entry-p
)
1962 (org-drill-unhide-clozed-text))))))
1965 (defun org-drill-present-multicloze-hide-nth (to-hide)
1966 "Hide the TO-HIDE'th piece of clozed text. 1 is the first piece. If
1967 TO-HIDE is negative, count backwards, so -1 means the last item, -2
1968 the second to last, etc."
1969 (with-hidden-comments
1970 (with-hidden-cloze-hints
1971 (let ((item-end nil
)
1973 (body-start (or (cdr (org-get-property-block))
1976 (org-drill-hide-all-subheadings-except nil
)
1978 (outline-next-heading)
1979 (setq item-end
(point)))
1981 (goto-char body-start
)
1982 (while (re-search-forward org-drill-cloze-regexp item-end t
)
1983 (let ((in-regexp?
(save-match-data
1984 (org-pos-in-regexp (match-beginning 0)
1985 org-bracket-link-regexp
1))))
1986 (unless (or in-regexp?
1987 (org-inside-LaTeX-fragment-p))
1988 (incf match-count
)))))
1989 (if (minusp to-hide
)
1990 (setq to-hide
(+ 1 to-hide match-count
)))
1992 ((or (not (plusp match-count
))
1993 (> to-hide match-count
))
1997 (goto-char body-start
)
1999 (while (re-search-forward org-drill-cloze-regexp item-end t
)
2000 (unless (save-match-data
2001 ;; Don't consider this a cloze region if it is part of an
2002 ;; org link, or if it occurs inside a LaTeX math
2004 (or (org-pos-in-regexp (match-beginning 0)
2005 org-bracket-link-regexp
1)
2006 (org-inside-LaTeX-fragment-p)))
2009 (org-drill-hide-matched-cloze-text)))))))
2010 (org-drill--show-latex-fragments)
2012 (org-display-inline-images t
))
2013 (org-cycle-hide-drawers 'all
)
2014 (prog1 (org-drill-presentation-prompt)
2015 (org-drill-hide-subheadings-if 'org-drill-entry-p
)
2016 (org-drill-unhide-clozed-text))))))
2019 (defun org-drill-present-multicloze-hide1 ()
2020 "Hides one of the pieces of text that are marked for cloze deletion,
2022 (org-drill-present-multicloze-hide-n 1))
2025 (defun org-drill-present-multicloze-hide2 ()
2026 "Hides two of the pieces of text that are marked for cloze deletion,
2028 (org-drill-present-multicloze-hide-n 2))
2031 (defun org-drill-present-multicloze-hide-first ()
2032 "Hides the first piece of text that is marked for cloze deletion."
2033 (org-drill-present-multicloze-hide-nth 1))
2036 (defun org-drill-present-multicloze-hide-last ()
2037 "Hides the last piece of text that is marked for cloze deletion."
2038 (org-drill-present-multicloze-hide-nth -
1))
2041 (defun org-drill-present-multicloze-hide1-firstmore ()
2042 "Commonly, hides the FIRST piece of text that is marked for
2043 cloze deletion. Uncommonly, hide one of the other pieces of text,
2046 The definitions of 'commonly' and 'uncommonly' are determined by
2047 the value of `org-drill-cloze-text-weight'."
2048 ;; The 'firstmore' and 'lastmore' functions used to randomly choose whether
2049 ;; to hide the 'favoured' piece of text. However even when the chance of
2050 ;; hiding it was set quite high (80%), the outcome was too unpredictable over
2051 ;; the small number of repetitions where most learning takes place for each
2052 ;; item. In other words, the actual frequency during the first 10 repetitions
2053 ;; was often very different from 80%. Hence we use modulo instead.
2055 ((null org-drill-cloze-text-weight
)
2056 ;; Behave as hide1cloze
2057 (org-drill-present-multicloze-hide1))
2058 ((not (and (integerp org-drill-cloze-text-weight
)
2059 (plusp org-drill-cloze-text-weight
)))
2060 (error "Illegal value for org-drill-cloze-text-weight: %S"
2061 org-drill-cloze-text-weight
))
2062 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
2063 org-drill-cloze-text-weight
))
2064 ;; Uncommonly, hide any item except the first
2065 (org-drill-present-multicloze-hide-n 1 t
))
2067 ;; Commonly, hide first item
2068 (org-drill-present-multicloze-hide-first))))
2071 (defun org-drill-present-multicloze-show1-lastmore ()
2072 "Commonly, hides all pieces except the last. Uncommonly, shows
2073 any random piece. The effect is similar to 'show1cloze' except
2074 that the last item is much less likely to be the item that is
2077 The definitions of 'commonly' and 'uncommonly' are determined by
2078 the value of `org-drill-cloze-text-weight'."
2080 ((null org-drill-cloze-text-weight
)
2081 ;; Behave as show1cloze
2082 (org-drill-present-multicloze-show1))
2083 ((not (and (integerp org-drill-cloze-text-weight
)
2084 (plusp org-drill-cloze-text-weight
)))
2085 (error "Illegal value for org-drill-cloze-text-weight: %S"
2086 org-drill-cloze-text-weight
))
2087 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
2088 org-drill-cloze-text-weight
))
2089 ;; Uncommonly, show any item except the last
2090 (org-drill-present-multicloze-hide-n -
1 nil nil t
))
2092 ;; Commonly, show the LAST item
2093 (org-drill-present-multicloze-hide-n -
1 nil t
))))
2096 (defun org-drill-present-multicloze-show1-firstless ()
2097 "Commonly, hides all pieces except one, where the shown piece
2098 is guaranteed NOT to be the first piece. Uncommonly, shows any
2099 random piece. The effect is similar to 'show1cloze' except that
2100 the first item is much less likely to be the item that is
2103 The definitions of 'commonly' and 'uncommonly' are determined by
2104 the value of `org-drill-cloze-text-weight'."
2106 ((null org-drill-cloze-text-weight
)
2107 ;; Behave as show1cloze
2108 (org-drill-present-multicloze-show1))
2109 ((not (and (integerp org-drill-cloze-text-weight
)
2110 (plusp org-drill-cloze-text-weight
)))
2111 (error "Illegal value for org-drill-cloze-text-weight: %S"
2112 org-drill-cloze-text-weight
))
2113 ((zerop (mod (1+ (org-drill-entry-total-repeats 0))
2114 org-drill-cloze-text-weight
))
2115 ;; Uncommonly, show the first item
2116 (org-drill-present-multicloze-hide-n -
1 t
))
2118 ;; Commonly, show any item, except the first
2119 (org-drill-present-multicloze-hide-n -
1 nil nil t
))))
2122 (defun org-drill-present-multicloze-show1 ()
2123 "Similar to `org-drill-present-multicloze-hide1', but hides all
2124 the pieces of text that are marked for cloze deletion, except for one
2125 piece which is chosen at random."
2126 (org-drill-present-multicloze-hide-n -
1))
2129 (defun org-drill-present-multicloze-show2 ()
2130 "Similar to `org-drill-present-multicloze-show1', but reveals two
2131 pieces rather than one."
2132 (org-drill-present-multicloze-hide-n -
2))
2135 (defun org-drill-present-card-using-text (question &optional answer
)
2136 "Present the string QUESTION as the only visible content of the card.
2137 If ANSWER is supplied, set the global variable `drill-answer' to its value."
2138 (if answer
(setq drill-answer answer
))
2139 (with-hidden-comments
2140 (with-replaced-entry-text
2141 (concat "\n" question
)
2142 (org-drill-hide-all-subheadings-except nil
)
2143 (org-cycle-hide-drawers 'all
)
2145 (org-display-inline-images t
))
2146 (prog1 (org-drill-presentation-prompt)
2147 (org-drill-hide-subheadings-if 'org-drill-entry-p
)))))
2150 (defun org-drill-present-card-using-multiple-overlays (replacements &optional answer
)
2151 "TEXTS is a list of valid values for the 'display' text property.
2152 Present these overlays, in sequence, as the only
2153 visible content of the card.
2154 If ANSWER is supplied, set the global variable `drill-answer' to its value."
2155 (if answer
(setq drill-answer answer
))
2156 (with-hidden-comments
2157 (with-replaced-entry-text-multi
2159 (org-drill-hide-all-subheadings-except nil
)
2160 (org-cycle-hide-drawers 'all
)
2162 (org-display-inline-images t
))
2163 (prog1 (org-drill-presentation-prompt)
2164 (org-drill-hide-subheadings-if 'org-drill-entry-p
)))))
2167 (defun org-drill-entry ()
2168 "Present the current topic for interactive review, as in `org-drill'.
2169 Review will occur regardless of whether the topic is due for review or whether
2170 it meets the definition of a 'review topic' used by `org-drill'.
2172 Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
2173 EDIT if the user chose to exit the drill and edit the current item. Choosing
2174 the latter option leaves the drill session suspended; it can be resumed
2175 later using `org-drill-resume'.
2177 See `org-drill' for more details."
2179 (org-drill-goto-drill-entry-heading)
2180 ;;(unless (org-part-of-drill-entry-p)
2181 ;; (error "Point is not inside a drill entry"))
2182 ;;(unless (org-at-heading-p)
2183 ;; (org-back-to-heading))
2184 (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE" t
))
2185 (answer-fn 'org-drill-present-default-answer
)
2186 (present-empty-cards nil
)
2188 ;; fontification functions in `outline-view-change-hook' can cause big
2189 ;; slowdowns, so we temporarily bind this variable to nil here.
2190 (outline-view-change-hook nil
))
2191 (setq drill-answer nil
)
2192 (org-save-outline-visibility t
2194 (org-narrow-to-subtree)
2196 (org-cycle-hide-drawers 'all
)
2198 (let ((presentation-fn
2199 (cdr (assoc card-type org-drill-card-type-alist
))))
2200 (if (listp presentation-fn
)
2201 (psetq answer-fn
(or (second presentation-fn
)
2202 'org-drill-present-default-answer
)
2203 present-empty-cards
(third presentation-fn
)
2204 presentation-fn
(first presentation-fn
)))
2207 ((null presentation-fn
)
2208 (message "%s:%d: Unrecognised card type '%s', skipping..."
2209 (buffer-name) (point) card-type
)
2213 (setq cont
(funcall presentation-fn
))
2225 (lambda () (org-drill-reschedule))))))))
2226 (org-remove-latex-fragment-image-overlays)))))))
2229 (defun org-drill-entries-pending-p ()
2230 (or *org-drill-again-entries
*
2231 *org-drill-current-item
*
2232 (and (not (org-drill-maximum-item-count-reached-p))
2233 (not (org-drill-maximum-duration-reached-p))
2234 (or *org-drill-new-entries
*
2235 *org-drill-failed-entries
*
2236 *org-drill-young-mature-entries
*
2237 *org-drill-old-mature-entries
*
2238 *org-drill-overdue-entries
*
2239 *org-drill-again-entries
*))))
2242 (defun org-drill-pending-entry-count ()
2243 (+ (if (markerp *org-drill-current-item
*) 1 0)
2244 (length *org-drill-new-entries
*)
2245 (length *org-drill-failed-entries
*)
2246 (length *org-drill-young-mature-entries
*)
2247 (length *org-drill-old-mature-entries
*)
2248 (length *org-drill-overdue-entries
*)
2249 (length *org-drill-again-entries
*)))
2252 (defun org-drill-maximum-duration-reached-p ()
2253 "Returns true if the current drill session has continued past its
2255 (and org-drill-maximum-duration
2256 (not *org-drill-cram-mode
*)
2257 *org-drill-start-time
*
2258 (> (- (float-time (current-time)) *org-drill-start-time
*)
2259 (* org-drill-maximum-duration
60))))
2262 (defun org-drill-maximum-item-count-reached-p ()
2263 "Returns true if the current drill session has reached the
2264 maximum number of items."
2265 (and org-drill-maximum-items-per-session
2266 (not *org-drill-cram-mode
*)
2267 (>= (length *org-drill-done-entries
*)
2268 org-drill-maximum-items-per-session
)))
2271 (defun org-drill-pop-next-pending-entry ()
2272 (block org-drill-pop-next-pending-entry
2275 (not (org-drill-entry-p m
)))
2279 ;; First priority is items we failed in a prior session.
2280 ((and *org-drill-failed-entries
*
2281 (not (org-drill-maximum-item-count-reached-p))
2282 (not (org-drill-maximum-duration-reached-p)))
2283 (pop-random *org-drill-failed-entries
*))
2284 ;; Next priority is overdue items.
2285 ((and *org-drill-overdue-entries
*
2286 (not (org-drill-maximum-item-count-reached-p))
2287 (not (org-drill-maximum-duration-reached-p)))
2288 ;; We use `pop', not `pop-random', because we have already
2289 ;; sorted overdue items into a random order which takes
2290 ;; number of days overdue into account.
2291 (pop *org-drill-overdue-entries
*))
2292 ;; Next priority is 'young' items.
2293 ((and *org-drill-young-mature-entries
*
2294 (not (org-drill-maximum-item-count-reached-p))
2295 (not (org-drill-maximum-duration-reached-p)))
2296 (pop-random *org-drill-young-mature-entries
*))
2297 ;; Next priority is newly added items, and older entries.
2298 ;; We pool these into a single group.
2299 ((and (or *org-drill-new-entries
*
2300 *org-drill-old-mature-entries
*)
2301 (not (org-drill-maximum-item-count-reached-p))
2302 (not (org-drill-maximum-duration-reached-p)))
2304 ((< (random* (+ (length *org-drill-new-entries
*)
2305 (length *org-drill-old-mature-entries
*)))
2306 (length *org-drill-new-entries
*))
2307 (pop-random *org-drill-new-entries
*))
2309 (pop-random *org-drill-old-mature-entries
*))))
2310 ;; After all the above are done, last priority is items
2311 ;; that were failed earlier THIS SESSION.
2312 (*org-drill-again-entries
*
2313 (pop *org-drill-again-entries
*))
2314 (t ; nothing left -- return nil
2315 (return-from org-drill-pop-next-pending-entry nil
)))))
2319 (defun org-drill-entries (&optional resuming-p
)
2320 "Returns nil, t, or a list of markers representing entries that were
2321 'failed' and need to be presented again before the session ends.
2323 RESUMING-P is true if we are resuming a suspended drill session."
2324 (block org-drill-entries
2325 (while (org-drill-entries-pending-p)
2327 ((or (not resuming-p
)
2328 (null *org-drill-current-item
*)
2329 (not (org-drill-entry-p *org-drill-current-item
*)))
2330 (org-drill-pop-next-pending-entry))
2331 (t ; resuming a suspended session.
2332 (setq resuming-p nil
)
2333 *org-drill-current-item
*))))
2334 (setq *org-drill-current-item
* m
)
2336 (error "Unexpectedly ran out of pending drill items"))
2338 (org-drill-goto-entry m
)
2340 ((not (org-drill-entry-due-p))
2341 ;; The entry is not due anymore. This could arise if the user
2342 ;; suspends a drill session, then drills an individual entry,
2343 ;; then resumes the session.
2344 (message "Entry no longer due, skipping...")
2348 (setq result
(org-drill-entry))
2352 (setq end-pos
:quit
)
2353 (return-from org-drill-entries nil
))
2355 (setq end-pos
(point-marker))
2356 (return-from org-drill-entries nil
))
2358 (setq *org-drill-current-item
* nil
)
2359 nil
) ; skip this item
2362 ((<= result org-drill-failure-quality
)
2363 (if *org-drill-again-entries
*
2364 (setq *org-drill-again-entries
*
2365 (shuffle-list *org-drill-again-entries
*)))
2366 (push-end m
*org-drill-again-entries
*))
2368 (push m
*org-drill-done-entries
*)))
2369 (setq *org-drill-current-item
* nil
))))))))))
2373 (defun org-drill-final-report ()
2375 (round (* 100 (count-if (lambda (qual)
2376 (> qual org-drill-failure-quality
))
2377 *org-drill-session-qualities
*))
2378 (max 1 (length *org-drill-session-qualities
*))))
2380 (max-mini-window-height 0.6))
2383 "%d items reviewed. Session duration %s.
2384 Recall of reviewed items:
2385 Excellent (5): %3d%% | Near miss (2): %3d%%
2386 Good (4): %3d%% | Failure (1): %3d%%
2387 Hard (3): %3d%% | Abject failure (0): %3d%%
2389 You successfully recalled %d%% of reviewed items (quality > %s)
2390 %d/%d items still await review (%s, %s, %s, %s, %s).
2391 Tomorrow, %d more items will become due for review.
2392 Session finished. Press a key to continue..."
2393 (length *org-drill-done-entries
*)
2394 (format-seconds "%h:%.2m:%.2s"
2395 (- (float-time (current-time)) *org-drill-start-time
*))
2396 (round (* 100 (count 5 *org-drill-session-qualities
*))
2397 (max 1 (length *org-drill-session-qualities
*)))
2398 (round (* 100 (count 2 *org-drill-session-qualities
*))
2399 (max 1 (length *org-drill-session-qualities
*)))
2400 (round (* 100 (count 4 *org-drill-session-qualities
*))
2401 (max 1 (length *org-drill-session-qualities
*)))
2402 (round (* 100 (count 1 *org-drill-session-qualities
*))
2403 (max 1 (length *org-drill-session-qualities
*)))
2404 (round (* 100 (count 3 *org-drill-session-qualities
*))
2405 (max 1 (length *org-drill-session-qualities
*)))
2406 (round (* 100 (count 0 *org-drill-session-qualities
*))
2407 (max 1 (length *org-drill-session-qualities
*)))
2409 org-drill-failure-quality
2410 (org-drill-pending-entry-count)
2411 (+ (org-drill-pending-entry-count)
2412 *org-drill-dormant-entry-count
*)
2415 (+ (length *org-drill-failed-entries
*)
2416 (length *org-drill-again-entries
*)))
2417 'face
`(:foreground
,org-drill-failed-count-color
))
2419 (format "%d overdue"
2420 (length *org-drill-overdue-entries
*))
2421 'face
`(:foreground
,org-drill-failed-count-color
))
2424 (length *org-drill-new-entries
*))
2425 'face
`(:foreground
,org-drill-new-count-color
))
2428 (length *org-drill-young-mature-entries
*))
2429 'face
`(:foreground
,org-drill-mature-count-color
))
2432 (length *org-drill-old-mature-entries
*))
2433 'face
`(:foreground
,org-drill-mature-count-color
))
2434 *org-drill-due-tomorrow-count
*
2437 (while (not (input-pending-p))
2438 (message "%s" prompt
)
2440 (read-char-exclusive)
2442 (if (and *org-drill-session-qualities
*
2443 (< pass-percent
(- 100 org-drill-forgetting-index
)))
2444 (read-char-exclusive
2447 You failed %d%% of the items you reviewed during this session.
2448 %d (%d%%) of all items scanned were overdue.
2450 Are you keeping up with your items, and reviewing them
2451 when they are scheduled? If so, you may want to consider
2452 lowering the value of `org-drill-learn-fraction' slightly in
2453 order to make items appear more frequently over time."
2454 (propertize "WARNING!" 'face
'org-warning
)
2455 (- 100 pass-percent
)
2456 *org-drill-overdue-entry-count
*
2457 (round (* 100 *org-drill-overdue-entry-count
*)
2458 (+ *org-drill-dormant-entry-count
*
2459 *org-drill-due-entry-count
*)))
2464 (defun org-drill-free-markers (markers)
2465 "MARKERS is a list of markers, all of which will be freed (set to
2466 point nowhere). Alternatively, MARKERS can be 't', in which case
2467 all the markers used by Org-Drill will be freed."
2468 (dolist (m (if (eql t markers
)
2469 (append *org-drill-done-entries
*
2470 *org-drill-new-entries
*
2471 *org-drill-failed-entries
*
2472 *org-drill-again-entries
*
2473 *org-drill-overdue-entries
*
2474 *org-drill-young-mature-entries
*
2475 *org-drill-old-mature-entries
*)
2480 ;;; overdue-data is a list of entries, each entry has the form (POS DUE AGE)
2481 ;;; where POS is a marker pointing to the start of the entry, and
2482 ;;; DUE is a number indicating how many days ago the entry was due.
2483 ;;; AGE is the number of days elapsed since item creation (nil if unknown).
2484 ;;; if age > lapse threshold (default 90), sort by age (oldest first)
2485 ;;; if age < lapse threshold, sort by due (biggest first)
2488 (defun org-drill-order-overdue-entries (overdue-data)
2489 (let* ((lapsed-days (if org-drill--lapse-very-overdue-entries-p
2490 90 most-positive-fixnum
))
2491 (not-lapsed (remove-if (lambda (a) (> (or (second a
) 0) lapsed-days
))
2493 (lapsed (remove-if-not (lambda (a) (> (or (second a
) 0)
2494 lapsed-days
)) overdue-data
)))
2495 (setq *org-drill-overdue-entries
*
2498 (sort (shuffle-list not-lapsed
)
2499 (lambda (a b
) (> (second a
) (second b
))))
2501 (lambda (a b
) (> (third a
) (third b
)))))))))
2504 (defun org-drill--entry-lapsed-p ()
2505 (let ((lapsed-days 90))
2506 (and org-drill--lapse-very-overdue-entries-p
2507 (> (or (org-drill-entry-days-overdue) 0) lapsed-days
))))
2512 (defun org-drill-entry-days-since-creation (&optional use-last-interval-p
)
2513 "If USE-LAST-INTERVAL-P is non-nil, and DATE_ADDED is missing, use the
2514 value of DRILL_LAST_INTERVAL instead (as the item's age must be at least
2516 (let ((timestamp (org-entry-get (point) "DATE_ADDED")))
2519 (- (org-time-stamp-to-now timestamp
)))
2520 (use-last-interval-p
2521 (+ (or (org-drill-entry-days-overdue) 0)
2522 (read (or (org-entry-get (point) "DRILL_LAST_INTERVAL") "0"))))
2526 (defun org-drill-entry-status ()
2527 "Returns a list (STATUS DUE AGE) where DUE is the number of days overdue,
2528 zero being due today, -1 being scheduled 1 day in the future.
2529 AGE is the number of days elapsed since the item was created (nil if unknown).
2530 STATUS is one of the following values:
2531 - nil, if the item is not a drill entry, or has an empty body
2541 (unless (org-at-heading-p)
2542 (org-back-to-heading))
2543 (let ((due (org-drill-entry-days-overdue))
2544 (age (org-drill-entry-days-since-creation t
))
2545 (last-int (org-drill-entry-last-interval 1)))
2548 ((not (org-drill-entry-p))
2550 ((and (org-entry-empty-p)
2551 (let* ((card-type (org-entry-get (point) "DRILL_CARD_TYPE" nil
))
2552 (dat (cdr (assoc card-type org-drill-card-type-alist
))))
2553 (or (null card-type
)
2554 (not (third dat
)))))
2555 ;; body is empty, and this is not a card type where empty bodies are
2556 ;; meaningful, so skip it.
2558 ((null due
) ; unscheduled - usually a skipped leech
2562 ((minusp due
) ; scheduled in the future
2564 ;; The rest of the stati all denote 'due' items ==========================
2565 ((<= (org-drill-entry-last-quality 9999)
2566 org-drill-failure-quality
)
2567 ;; Mature entries that were failed last time are
2568 ;; FAILED, regardless of how young, old or overdue
2571 ((org-drill-entry-new-p)
2573 ((org-drill-entry-overdue-p due last-int
)
2574 ;; Overdue status overrides young versus old
2576 ;; Store marker + due, for sorting of overdue entries
2578 ((<= (org-drill-entry-last-interval 9999)
2579 org-drill-days-before-old
)
2586 (defun org-drill-progress-message (collected scanned
)
2587 (when (zerop (% scanned
50))
2588 (let* ((meter-width 40)
2589 (sym1 (if (oddp (floor scanned
(* 50 meter-width
))) ?| ?.
))
2590 (sym2 (if (eql sym1 ?.
) ?| ?.
)))
2591 (message "Collecting due drill items:%4d %s%s"
2593 (make-string (%
(ceiling scanned
50) meter-width
)
2595 (make-string (- meter-width
(%
(ceiling scanned
50) meter-width
))
2599 (defun org-map-drill-entry-function ()
2600 (org-drill-progress-message
2601 (+ (length *org-drill-new-entries
*)
2602 (length *org-drill-overdue-entries
*)
2603 (length *org-drill-young-mature-entries
*)
2604 (length *org-drill-old-mature-entries
*)
2605 (length *org-drill-failed-entries
*))
2608 ((not (org-drill-entry-p))
2611 (when (and (not warned-about-id-creation
)
2612 (null (org-id-get)))
2613 (message (concat "Creating unique IDs for items "
2614 "(slow, but only happens once)"))
2616 (setq warned-about-id-creation t
))
2617 (org-id-get-create) ; ensure drill entry has unique ID
2618 (destructuring-bind (status due age
)
2619 (org-drill-entry-status)
2622 (incf *org-drill-dormant-entry-count
*))
2624 ;; (incf *org-drill-dormant-entry-count*)
2625 ;; (incf *org-drill-due-tomorrow-count*))
2627 (incf *org-drill-dormant-entry-count
*)
2629 (incf *org-drill-due-tomorrow-count
*)))
2631 (push (point-marker) *org-drill-new-entries
*))
2633 (push (point-marker) *org-drill-failed-entries
*))
2635 (push (point-marker) *org-drill-young-mature-entries
*))
2637 (push (list (point-marker) due age
) overdue-data
))
2639 (push (point-marker) *org-drill-old-mature-entries
*))
2643 (defun org-drill (&optional scope drill-match resume-p
)
2644 "Begin an interactive 'drill session'. The user is asked to
2645 review a series of topics (headers). Each topic is initially
2646 presented as a 'question', often with part of the topic content
2647 hidden. The user attempts to recall the hidden information or
2648 answer the question, then presses a key to reveal the answer. The
2649 user then rates his or her recall or performance on that
2650 topic. This rating information is used to reschedule the topic
2653 Org-drill proceeds by:
2655 - Finding all topics (headings) in SCOPE which have either been
2656 used and rescheduled before, or which have a tag that matches
2657 `org-drill-question-tag'.
2659 - All matching topics which are either unscheduled, or are
2660 scheduled for the current date or a date in the past, are
2661 considered to be candidates for the drill session.
2663 - If `org-drill-maximum-items-per-session' is set, a random
2664 subset of these topics is presented. Otherwise, all of the
2665 eligible topics will be presented.
2667 SCOPE determines the scope in which to search for
2668 questions. It accepts the same values as `org-drill-scope',
2671 DRILL-MATCH, if supplied, is a string specifying a tags/property/
2672 todo query. Only items matching the query will be considered.
2673 It accepts the same values as `org-drill-match', which see.
2675 If RESUME-P is non-nil, resume a suspended drill session rather
2676 than starting a new one."
2679 ;; Check org version. Org 7.9.3f introduced a backwards-incompatible change
2680 ;; to the arguments accepted by `org-schedule'. At the time of writing there
2681 ;; are still lots of people using versions of org older than this.
2682 (let ((majorv (first (mapcar 'string-to-number
(split-string (org-release) "[.]")))))
2683 (if (and (< majorv
8)
2684 (not (string-match-p "universal prefix argument" (documentation 'org-schedule
))))
2685 (read-char-exclusive
2686 (format "Warning: org-drill requires org mode 7.9.3f or newer. Scheduling of failed cards will not
2687 work correctly with older versions of org mode. Your org mode version (%s) appears to be older than
2688 7.9.3f. Please consider installing a more recent version of org mode." (org-release)))))
2694 (org-drill-free-markers t
)
2695 (setq *org-drill-current-item
* nil
2696 *org-drill-done-entries
* nil
2697 *org-drill-dormant-entry-count
* 0
2698 *org-drill-due-entry-count
* 0
2699 *org-drill-due-tomorrow-count
* 0
2700 *org-drill-overdue-entry-count
* 0
2701 *org-drill-new-entries
* nil
2702 *org-drill-overdue-entries
* nil
2703 *org-drill-young-mature-entries
* nil
2704 *org-drill-old-mature-entries
* nil
2705 *org-drill-failed-entries
* nil
2706 *org-drill-again-entries
* nil
)
2707 (setq *org-drill-session-qualities
* nil
)
2708 (setq *org-drill-start-time
* (float-time (current-time))))
2709 (setq *random-state
* (make-random-state t
)) ; reseed RNG
2713 (let ((org-trust-scanner-tags t
)
2714 (warned-about-id-creation nil
))
2715 (org-map-drill-entries
2716 'org-map-drill-entry-function
2718 (org-drill-order-overdue-entries overdue-data
)
2719 (setq *org-drill-overdue-entry-count
*
2720 (length *org-drill-overdue-entries
*))))
2721 (setq *org-drill-due-entry-count
* (org-drill-pending-entry-count))
2723 ((and (null *org-drill-current-item
*)
2724 (null *org-drill-new-entries
*)
2725 (null *org-drill-failed-entries
*)
2726 (null *org-drill-overdue-entries
*)
2727 (null *org-drill-young-mature-entries
*)
2728 (null *org-drill-old-mature-entries
*))
2729 (message "I did not find any pending drill items."))
2731 (org-drill-entries resume-p
)
2732 (message "Drill session finished!"))))
2735 (setq *org-drill-cram-mode
* nil
)
2736 (org-drill-free-markers *org-drill-done-entries
*)))))
2739 (when (markerp end-pos
)
2740 (org-drill-goto-entry end-pos
)
2743 (let ((keystr (command-keybinding-to-string 'org-drill-resume
)))
2745 "You can continue the drill session with the command `org-drill-resume'.%s"
2746 (if keystr
(format "\nYou can run this command by pressing %s." keystr
)
2749 (org-drill-final-report)
2750 (if (eql 'sm5 org-drill-spaced-repetition-algorithm
)
2751 (org-drill-save-optimal-factor-matrix))
2752 (if org-drill-save-buffers-after-drill-sessions-p
2753 (save-some-buffers))
2754 (message "Drill session finished!")
2758 (defun org-drill-save-optimal-factor-matrix ()
2759 (savehist-autosave))
2762 (defun org-drill-cram (&optional scope drill-match
)
2763 "Run an interactive drill session in 'cram mode'. In cram mode,
2764 all drill items are considered to be due for review, unless they
2765 have been reviewed within the last `org-drill-cram-hours'
2768 (setq *org-drill-cram-mode
* t
)
2769 (org-drill scope drill-match
))
2772 (defun org-drill-tree ()
2773 "Run an interactive drill session using drill items within the
2779 (defun org-drill-directory ()
2780 "Run an interactive drill session using drill items from all org
2781 files in the same directory as the current file."
2783 (org-drill 'directory
))
2786 (defun org-drill-again (&optional scope drill-match
)
2787 "Run a new drill session, but try to use leftover due items that
2788 were not reviewed during the last session, rather than scanning for
2789 unreviewed items. If there are no leftover items in memory, a full
2790 scan will be performed."
2792 (setq *org-drill-cram-mode
* nil
)
2794 ((plusp (org-drill-pending-entry-count))
2795 (org-drill-free-markers *org-drill-done-entries
*)
2796 (if (markerp *org-drill-current-item
*)
2797 (free-marker *org-drill-current-item
*))
2798 (setq *org-drill-start-time
* (float-time (current-time))
2799 *org-drill-done-entries
* nil
2800 *org-drill-current-item
* nil
)
2801 (org-drill scope drill-match t
))
2803 (org-drill scope drill-match
))))
2807 (defun org-drill-resume ()
2808 "Resume a suspended drill session. Sessions are suspended by
2809 exiting them with the `edit' or `quit' options."
2812 ((org-drill-entries-pending-p)
2813 (org-drill nil nil t
))
2814 ((and (plusp (org-drill-pending-entry-count))
2815 ;; Current drill session is finished, but there are still
2816 ;; more items which need to be reviewed.
2818 "You have finished the drill session. However, %d items still
2819 need reviewing. Start a new drill session? "
2820 (org-drill-pending-entry-count))))
2823 (message "You have finished the drill session."))))
2826 (defun org-drill-relearn-item ()
2827 "Make the current item due for revision, and set its last interval to 0.
2828 Makes the item behave as if it has been failed, without actually recording a
2829 failure. This command can be used to 'reset' repetitions for an item."
2831 (org-drill-smart-reschedule 4 0))
2834 (defun org-drill-strip-entry-data ()
2835 (dolist (prop org-drill-scheduling-properties
)
2836 (org-delete-property prop
))
2837 (org-schedule '(4)))
2840 (defun org-drill-strip-all-data (&optional scope
)
2841 "Delete scheduling data from every drill entry in scope. This
2842 function may be useful if you want to give your collection of
2843 entries to someone else. Scope defaults to the current buffer,
2844 and is specified by the argument SCOPE, which accepts the same
2845 values as `org-drill-scope'."
2848 "Delete scheduling data from ALL items in scope: are you sure?")
2851 ;; Scope is the current buffer. This means we can use
2852 ;; `org-delete-property-globally', which is faster.
2853 (dolist (prop org-drill-scheduling-properties
)
2854 (org-delete-property-globally prop
))
2855 (org-map-drill-entries (lambda () (org-schedule '(4))) scope
))
2857 (org-map-drill-entries 'org-drill-strip-entry-data scope
)))
2861 (defun org-drill-add-cloze-fontification ()
2862 ;; Compute local versions of the regexp for cloze deletions, in case
2863 ;; the left and right delimiters are redefined locally.
2864 (setq-local org-drill-cloze-regexp
(org-drill--compute-cloze-regexp))
2865 (setq-local org-drill-cloze-keywords
(org-drill--compute-cloze-keywords))
2866 (when org-drill-use-visible-cloze-face-p
2867 (add-to-list 'org-font-lock-extra-keywords
2868 (first org-drill-cloze-keywords
))))
2871 ;; Can't add to org-mode-hook, because local variables won't have been loaded
2874 ;; (defun org-drill-add-cloze-fontification ()
2875 ;; (when (eql major-mode 'org-mode)
2876 ;; ;; Compute local versions of the regexp for cloze deletions, in case
2877 ;; ;; the left and right delimiters are redefined locally.
2878 ;; (setq-local org-drill-cloze-regexp (org-drill--compute-cloze-regexp))
2879 ;; (setq-local org-drill-cloze-keywords (org-drill--compute-cloze-keywords))
2880 ;; (when org-drill-use-visible-cloze-face-p
2881 ;; (font-lock-add-keywords nil ;'org-mode
2882 ;; org-drill-cloze-keywords
2886 ;; (add-hook 'hack-local-variables-hook
2887 ;; 'org-drill-add-cloze-fontification)
2889 ;; (org-drill-add-cloze-fontification)
2892 ;;; Synching card collections =================================================
2895 (defvar *org-drill-dest-id-table
* (make-hash-table :test
'equal
))
2898 (defun org-drill-copy-entry-to-other-buffer (dest &optional path
)
2899 "Copy the subtree at point to the buffer DEST. The copy will receive
2900 the tag 'imported'."
2901 (block org-drill-copy-entry-to-other-buffer
2903 (let ((src (current-buffer))
2905 (cl-flet ((paste-tree-here (&optional level
)
2906 (org-paste-subtree level
)
2907 (org-drill-strip-entry-data)
2908 (org-toggle-tag "imported" 'on
)
2909 (org-map-drill-entries
2911 (let ((id (org-id-get)))
2912 (org-drill-strip-entry-data)
2913 (unless (gethash id
*org-drill-dest-id-table
*)
2914 (puthash id
(point-marker)
2915 *org-drill-dest-id-table
*))))
2918 (setq path
(org-get-outline-path)))
2920 (switch-to-buffer dest
)
2923 (org-find-olp path t
)
2924 (error ; path does not exist in DEST
2925 (return-from org-drill-copy-entry-to-other-buffer
2928 (org-drill-copy-entry-to-other-buffer
2929 dest
(butlast path
)))
2931 ;; We've looked all the way up the path
2932 ;; Default to appending to the end of DEST
2933 (goto-char (point-max))
2935 (paste-tree-here)))))))
2937 (outline-next-heading)
2940 (paste-tree-here (1+ (or (org-current-level) 0)))
2945 (defun org-drill-merge-buffers (src &optional dest ignore-new-items-p
)
2946 "SRC and DEST are two org mode buffers containing drill items.
2947 For each drill item in DEST that shares an ID with an item in SRC,
2948 overwrite scheduling data in DEST with data taken from the item in SRC.
2949 This is intended for use when two people are sharing a set of drill items,
2950 one person has made some updates to the item set, and the other person
2951 wants to migrate to the updated set without losing their scheduling data.
2953 By default, any drill items in SRC which do not exist in DEST are
2954 copied into DEST. We attempt to place the copied item in the
2955 equivalent location in DEST to its location in SRC, by matching
2956 the heading hierarchy. However if IGNORE-NEW-ITEMS-P is non-nil,
2957 we simply ignore any items that do not exist in DEST, and do not
2959 (interactive "bImport scheduling info from which buffer?")
2961 (setq dest
(current-buffer)))
2962 (setq src
(get-buffer src
)
2963 dest
(get-buffer dest
))
2966 (concat "About to overwrite all scheduling data for drill items in `%s' "
2967 "with information taken from matching items in `%s'. Proceed? ")
2968 (buffer-name dest
) (buffer-name src
)))
2969 ;; Compile list of all IDs in the destination buffer.
2970 (clrhash *org-drill-dest-id-table
*)
2971 (with-current-buffer dest
2972 (org-map-drill-entries
2974 (let ((this-id (org-id-get)))
2976 (puthash this-id
(point-marker) *org-drill-dest-id-table
*))))
2978 ;; Look through all entries in source buffer.
2979 (with-current-buffer src
2980 (org-map-drill-entries
2982 (let ((id (org-id-get))
2983 (last-quality nil
) (last-reviewed nil
)
2984 (scheduled-time nil
))
2987 (not (org-drill-entry-p)))
2989 ((gethash id
*org-drill-dest-id-table
*)
2990 ;; This entry matches an entry in dest. Retrieve all its
2991 ;; scheduling data, then go to the matching location in dest
2992 ;; and write the data.
2993 (let ((marker (gethash id
*org-drill-dest-id-table
*)))
2994 (destructuring-bind (last-interval repetitions failures
2995 total-repeats meanq ease
)
2996 (org-drill-get-item-data)
2997 (setq last-reviewed
(org-entry-get (point) "DRILL_LAST_REVIEWED")
2998 last-quality
(org-entry-get (point) "DRILL_LAST_QUALITY")
2999 scheduled-time
(org-get-scheduled-time (point)))
3001 ;; go to matching entry in destination buffer
3002 (switch-to-buffer (marker-buffer marker
))
3004 (org-drill-strip-entry-data)
3005 (unless (zerop total-repeats
)
3006 (org-drill-store-item-data last-interval repetitions failures
3007 total-repeats meanq ease
)
3009 (org-set-property "LAST_QUALITY" last-quality
)
3010 (org-delete-property "LAST_QUALITY"))
3012 (org-set-property "LAST_REVIEWED" last-reviewed
)
3013 (org-delete-property "LAST_REVIEWED"))
3015 (org-schedule nil scheduled-time
)))))
3016 (remhash id
*org-drill-dest-id-table
*)
3017 (free-marker marker
)))
3019 ;; item in SRC has ID, but no matching ID in DEST.
3020 ;; It must be a new item that does not exist in DEST.
3021 ;; Copy the entire item to the *end* of DEST.
3022 (unless ignore-new-items-p
3023 (org-drill-copy-entry-to-other-buffer dest
))))))
3025 ;; Finally: there may be some items in DEST which are not in SRC, and
3026 ;; which have been scheduled by another user of DEST. Clear out the
3027 ;; scheduling info from all the unmatched items in DEST.
3028 (with-current-buffer dest
3029 (maphash (lambda (id m
)
3031 (org-drill-strip-entry-data)
3033 *org-drill-dest-id-table
*))))
3037 ;;; Card types for learning languages =========================================
3039 ;;; Get spell-number.el from:
3040 ;;; http://www.emacswiki.org/emacs/spell-number.el
3041 (autoload 'spelln-integer-in-words
"spell-number")
3044 ;;; `conjugate' card type =====================================================
3045 ;;; See spanish.org for usage
3047 (defvar org-drill-verb-tense-alist
3048 '(("present" "tomato")
3049 ("simple present" "tomato")
3050 ("present indicative" "tomato")
3053 ("simple past" "purple")
3054 ("preterite" "purple")
3055 ("imperfect" "darkturquoise")
3056 ("present perfect" "royalblue")
3059 ;; moods (backgrounds).
3060 ("indicative" nil
) ; default
3061 ("subjunctive" "medium blue")
3062 ("conditional" "grey30")
3063 ("negative imperative" "red4")
3064 ("positive imperative" "darkgreen")
3066 "Alist where each entry has the form (TENSE COLOUR), where
3067 TENSE is a string naming a tense in which verbs can be
3068 conjugated, and COLOUR is a string specifying a foreground colour
3069 which will be used by `org-drill-present-verb-conjugation' and
3070 `org-drill-show-answer-verb-conjugation' to fontify the verb and
3071 the name of the tense.")
3074 (defun org-drill-get-verb-conjugation-info ()
3075 "Auxiliary function used by `org-drill-present-verb-conjugation' and
3076 `org-drill-show-answer-verb-conjugation'."
3077 (let ((infinitive (org-entry-get (point) "VERB_INFINITIVE" t
))
3078 (inf-hint (org-entry-get (point) "VERB_INFINITIVE_HINT" t
))
3079 (translation (org-entry-get (point) "VERB_TRANSLATION" t
))
3080 (tense (org-entry-get (point) "VERB_TENSE" nil
))
3081 (mood (org-entry-get (point) "VERB_MOOD" nil
))
3082 (highlight-face nil
))
3083 (unless (and infinitive translation
(or tense mood
))
3084 (error "Missing information for verb conjugation card (%s, %s, %s, %s) at %s"
3085 infinitive translation tense mood
(point)))
3086 (setq tense
(if tense
(downcase (car (read-from-string tense
))))
3087 mood
(if mood
(downcase (car (read-from-string mood
))))
3088 infinitive
(car (read-from-string infinitive
))
3089 inf-hint
(if inf-hint
(car (read-from-string inf-hint
)))
3090 translation
(car (read-from-string translation
)))
3091 (setq highlight-face
3093 (or (second (assoc-string tense org-drill-verb-tense-alist t
))
3096 (second (assoc-string mood org-drill-verb-tense-alist t
))))
3097 (setq infinitive
(propertize infinitive
'face highlight-face
))
3098 (setq translation
(propertize translation
'face highlight-face
))
3099 (if tense
(setq tense
(propertize tense
'face highlight-face
)))
3100 (if mood
(setq mood
(propertize mood
'face highlight-face
)))
3101 (list infinitive inf-hint translation tense mood
)))
3104 (defun org-drill-present-verb-conjugation ()
3105 "Present a drill entry whose card type is 'conjugate'."
3106 (cl-flet ((tense-and-mood-to-string
3110 (format "%s tense, %s mood" tense mood
))
3112 (format "%s tense" tense
))
3114 (format "%s mood" mood
)))))
3115 (destructuring-bind (infinitive inf-hint translation tense mood
)
3116 (org-drill-get-verb-conjugation-info)
3117 (org-drill-present-card-using-text
3119 ((zerop (random* 2))
3120 (format "\nTranslate the verb\n\n%s\n\nand conjugate for the %s.\n\n"
3121 infinitive
(tense-and-mood-to-string tense mood
)))
3124 (format "\nGive the verb that means\n\n%s %s\n
3125 and conjugate for the %s.\n\n"
3127 (if inf-hint
(format " [HINT: %s]" inf-hint
) "")
3128 (tense-and-mood-to-string tense mood
))))))))
3131 (defun org-drill-show-answer-verb-conjugation (reschedule-fn)
3132 "Show the answer for a drill item whose card type is 'conjugate'.
3133 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
3134 returns its return value."
3135 (destructuring-bind (infinitive inf-hint translation tense mood
)
3136 (org-drill-get-verb-conjugation-info)
3137 (with-replaced-entry-heading
3138 (format "%s of %s ==> %s\n\n"
3142 (format "%s tense, %s mood" tense mood
))
3144 (format "%s tense" tense
))
3146 (format "%s mood" mood
))))
3147 infinitive translation
)
3148 (org-cycle-hide-drawers 'all
)
3149 (funcall reschedule-fn
))))
3152 ;;; `decline_noun' card type ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
3155 (defvar org-drill-noun-gender-alist
3156 '(("masculine" "dodgerblue")
3157 ("masc" "dodgerblue")
3158 ("male" "dodgerblue")
3160 ("feminine" "orchid")
3171 (defun org-drill-get-noun-info ()
3172 "Auxiliary function used by `org-drill-present-noun-declension' and
3173 `org-drill-show-answer-noun-declension'."
3174 (let ((noun (org-entry-get (point) "NOUN" t
))
3175 (noun-hint (org-entry-get (point) "NOUN_HINT" t
))
3176 (noun-root (org-entry-get (point) "NOUN_ROOT" t
))
3177 (noun-gender (org-entry-get (point) "NOUN_GENDER" t
))
3178 (translation (org-entry-get (point) "NOUN_TRANSLATION" t
))
3179 (highlight-face nil
))
3180 (unless (and noun translation
)
3181 (error "Missing information for `decline_noun' card (%s, %s, %s, %s) at %s"
3182 noun translation noun-hint noun-root
(point)))
3183 (setq noun-root
(if noun-root
(car (read-from-string noun-root
)))
3184 noun
(car (read-from-string noun
))
3185 noun-gender
(downcase (car (read-from-string noun-gender
)))
3186 noun-hint
(if noun-hint
(car (read-from-string noun-hint
)))
3187 translation
(car (read-from-string translation
)))
3188 (setq highlight-face
3190 (or (second (assoc-string noun-gender
3191 org-drill-noun-gender-alist t
))
3193 (setq noun
(propertize noun
'face highlight-face
))
3194 (setq translation
(propertize translation
'face highlight-face
))
3195 (list noun noun-root noun-gender noun-hint translation
)))
3198 (defun org-drill-present-noun-declension ()
3199 "Present a drill entry whose card type is 'decline_noun'."
3200 (destructuring-bind (noun noun-root noun-gender noun-hint translation
)
3201 (org-drill-get-noun-info)
3202 (let* ((props (org-entry-properties (point)))
3205 ((assoc "DECLINE_DEFINITE" props
)
3206 (propertize (if (org-entry-get (point) "DECLINE_DEFINITE")
3207 "definite" "indefinite")
3212 ((assoc "DECLINE_PLURAL" props
)
3213 (propertize (if (org-entry-get (point) "DECLINE_PLURAL")
3214 "plural" "singular")
3217 (org-drill-present-card-using-text
3219 ((zerop (random* 2))
3220 (format "\nTranslate the noun\n\n%s (%s)\n\nand list its declensions%s.\n\n"
3222 (if (or plural definite
)
3223 (format " for the %s %s form" definite plural
)
3226 (format "\nGive the noun that means\n\n%s %s\n
3227 and list its declensions%s.\n\n"
3229 (if noun-hint
(format " [HINT: %s]" noun-hint
) "")
3230 (if (or plural definite
)
3231 (format " for the %s %s form" definite plural
)
3235 (defun org-drill-show-answer-noun-declension (reschedule-fn)
3236 "Show the answer for a drill item whose card type is 'decline_noun'.
3237 RESCHEDULE-FN must be a function that calls `org-drill-reschedule' and
3238 returns its return value."
3239 (destructuring-bind (noun noun-root noun-gender noun-hint translation
)
3240 (org-drill-get-noun-info)
3241 (with-replaced-entry-heading
3242 (format "Declensions of %s (%s) ==> %s\n\n"
3243 noun noun-gender translation
)
3244 (org-cycle-hide-drawers 'all
)
3245 (funcall reschedule-fn
))))
3248 ;;; `translate_number' card type ==============================================
3249 ;;; See spanish.org for usage
3252 (defun spelln-integer-in-language (n lang
)
3253 (let ((spelln-language lang
))
3254 (spelln-integer-in-words n
)))
3256 (defun org-drill-present-translate-number ()
3257 (let ((num-min (read (org-entry-get (point) "DRILL_NUMBER_MIN")))
3258 (num-max (read (org-entry-get (point) "DRILL_NUMBER_MAX")))
3259 (language (read (org-entry-get (point) "DRILL_LANGUAGE" t
)))
3261 (drilled-number-direction 'to-english
)
3262 (highlight-face 'font-lock-warning-face
))
3264 ((not (fboundp 'spelln-integer-in-words
))
3265 (message "`spell-number.el' not loaded, skipping 'translate_number' card...")
3268 ((not (and (numberp num-min
) (numberp num-max
) language
))
3269 (error "Missing language or minimum or maximum numbers for number card"))
3271 (if (> num-min num-max
)
3272 (psetf num-min num-max
3274 (setq drilled-number
3275 (+ num-min
(random* (abs (1+ (- num-max num-min
))))))
3276 (setq drilled-number-direction
3277 (if (zerop (random* 2)) 'from-english
'to-english
))
3279 ((eql 'to-english drilled-number-direction
)
3280 (org-drill-present-card-using-text
3281 (format "\nTranslate into English:\n\n%s\n"
3283 (spelln-integer-in-language drilled-number language
)
3284 'face highlight-face
))
3285 (spelln-integer-in-language drilled-number
'english-gb
)))
3287 (org-drill-present-card-using-text
3288 (format "\nTranslate into %s:\n\n%s\n"
3289 (capitalize (format "%s" language
))
3291 (spelln-integer-in-language drilled-number
'english-gb
)
3292 'face highlight-face
))
3293 (spelln-integer-in-language drilled-number language
))))))))
3296 ;; (defun org-drill-show-answer-translate-number (reschedule-fn)
3297 ;; (let* ((language (read (org-entry-get (point) "DRILL_LANGUAGE" t)))
3298 ;; (highlight-face 'font-lock-warning-face)
3300 ;; (let ((spelln-language language))
3301 ;; (propertize (spelln-integer-in-words *drilled-number*)
3302 ;; 'face highlight-face)))
3304 ;; (let ((spelln-language 'english-gb))
3305 ;; (propertize (spelln-integer-in-words *drilled-number*)
3306 ;; 'face 'highlight-face))))
3307 ;; (with-replaced-entry-text
3309 ;; ((eql 'to-english *drilled-number-direction*)
3310 ;; (format "\nThe English translation of %s is:\n\n%s\n"
3311 ;; non-english english))
3313 ;; (format "\nThe %s translation of %s is:\n\n%s\n"
3314 ;; (capitalize (format "%s" language))
3315 ;; english non-english)))
3316 ;; (funcall reschedule-fn))))
3319 ;;; `spanish_verb' card type ==================================================
3320 ;;; Not very interesting, but included to demonstrate how a presentation
3321 ;;; function can manipulate which subheading are hidden versus shown.
3324 (defun org-drill-present-spanish-verb ()
3326 (reveal-headings nil
))
3327 (with-hidden-comments
3328 (with-hidden-cloze-hints
3329 (with-hidden-cloze-text
3332 (org-drill-hide-all-subheadings-except '("Infinitive"))
3334 (concat "Translate this Spanish verb, and conjugate it "
3335 "for the *present* tense.")
3336 reveal-headings
'("English" "Present Tense" "Notes")))
3338 (org-drill-hide-all-subheadings-except '("English"))
3339 (setq prompt
(concat "For the *present* tense, conjugate the "
3340 "Spanish translation of this English verb.")
3341 reveal-headings
'("Infinitive" "Present Tense" "Notes")))
3343 (org-drill-hide-all-subheadings-except '("Infinitive"))
3344 (setq prompt
(concat "Translate this Spanish verb, and "
3345 "conjugate it for the *past* tense.")
3346 reveal-headings
'("English" "Past Tense" "Notes")))
3348 (org-drill-hide-all-subheadings-except '("English"))
3349 (setq prompt
(concat "For the *past* tense, conjugate the "
3350 "Spanish translation of this English verb.")
3351 reveal-headings
'("Infinitive" "Past Tense" "Notes")))
3353 (org-drill-hide-all-subheadings-except '("Infinitive"))
3354 (setq prompt
(concat "Translate this Spanish verb, and "
3355 "conjugate it for the *future perfect* tense.")
3356 reveal-headings
'("English" "Future Perfect Tense" "Notes")))
3358 (org-drill-hide-all-subheadings-except '("English"))
3359 (setq prompt
(concat "For the *future perfect* tense, conjugate the "
3360 "Spanish translation of this English verb.")
3361 reveal-headings
'("Infinitive" "Future Perfect Tense" "Notes"))))
3362 (org-cycle-hide-drawers 'all
)
3363 (prog1 (org-drill-presentation-prompt)
3364 (org-drill-hide-subheadings-if 'org-drill-entry-p
)))))))
3367 (provide 'org-drill
)