org-drill.el 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. ;;; org-drill.el - Self-testing with org-learn
  2. ;;;
  3. ;;; Author: Paul Sexton <eeeickythump@gmail.com>
  4. ;;; Version: 1.4
  5. ;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
  6. ;;;
  7. ;;;
  8. ;;; Synopsis
  9. ;;; ========
  10. ;;;
  11. ;;; Uses the spaced repetition algorithm in `org-learn' to conduct interactive
  12. ;;; "drill sessions", where the material to be remembered is presented to the
  13. ;;; student in random order. The student rates his or her recall of each item,
  14. ;;; and this information is fed back to `org-learn' to schedule the item for
  15. ;;; later revision.
  16. ;;;
  17. ;;; Each drill session can be restricted to topics in the current buffer
  18. ;;; (default), one or several files, all agenda files, or a subtree. A single
  19. ;;; topic can also be drilled.
  20. ;;;
  21. ;;; Different "card types" can be defined, which present their information to
  22. ;;; the student in different ways.
  23. ;;;
  24. ;;; See the file README.org for more detailed documentation.
  25. (eval-when-compile (require 'cl))
  26. (eval-when-compile (require 'hi-lock))
  27. (require 'org)
  28. (require 'org-learn)
  29. (defgroup org-drill nil
  30. "Options concerning interactive drill sessions in Org mode (org-drill)."
  31. :tag "Org-Drill"
  32. :group 'org-link)
  33. (defcustom org-drill-question-tag
  34. "drill"
  35. "Tag which topics must possess in order to be identified as review topics
  36. by `org-drill'."
  37. :group 'org-drill
  38. :type 'string)
  39. (defcustom org-drill-maximum-items-per-session
  40. 30
  41. "Each drill session will present at most this many topics for review.
  42. Nil means unlimited."
  43. :group 'org-drill
  44. :type '(choice integer (const nil)))
  45. (defcustom org-drill-maximum-duration
  46. 20
  47. "Maximum duration of a drill session, in minutes.
  48. Nil means unlimited."
  49. :group 'org-drill
  50. :type '(choice integer (const nil)))
  51. (defcustom org-drill-failure-quality
  52. 2
  53. "If the quality of recall for an item is this number or lower,
  54. it is regarded as an unambiguous failure, and the repetition
  55. interval for the card is reset to 0 days. By default this is
  56. 2. For Mnemosyne-like behaviour, set it to 1. Other values are
  57. not really sensible."
  58. :group 'org-drill
  59. :type '(choice (const 2) (const 1)))
  60. (defcustom org-drill-leech-failure-threshold
  61. 15
  62. "If an item is forgotten more than this many times, it is tagged
  63. as a 'leech' item."
  64. :group 'org-drill
  65. :type '(choice integer (const nil)))
  66. (defcustom org-drill-leech-method
  67. 'skip
  68. "How should 'leech items' be handled during drill sessions?
  69. Possible values:
  70. - nil :: Leech items are treated the same as normal items.
  71. - skip :: Leech items are not included in drill sessions.
  72. - warn :: Leech items are still included in drill sessions,
  73. but a warning message is printed when each leech item is
  74. presented."
  75. :group 'org-drill
  76. :type '(choice (const 'warn) (const 'skip) (const nil)))
  77. (defface org-drill-visible-cloze-face
  78. '((t (:foreground "darkseagreen")))
  79. "The face used to hide the contents of cloze phrases."
  80. :group 'org-drill)
  81. (defface org-drill-visible-cloze-hint-face
  82. '((t (:foreground "dark slate blue")))
  83. "The face used to hide the contents of cloze phrases."
  84. :group 'org-drill)
  85. (defcustom org-drill-use-visible-cloze-face-p
  86. nil
  87. "Use a special face to highlight cloze-deleted text in org mode
  88. buffers?"
  89. :group 'org-drill
  90. :type 'boolean)
  91. (defface org-drill-hidden-cloze-face
  92. '((t (:foreground "deep sky blue" :background "blue")))
  93. "The face used to hide the contents of cloze phrases."
  94. :group 'org-drill)
  95. (defcustom org-drill-new-count-color
  96. "royal blue"
  97. "Foreground colour used to display the count of remaining new items
  98. during a drill session."
  99. :group 'org-drill
  100. :type 'color)
  101. (defcustom org-drill-mature-count-color
  102. "green"
  103. "Foreground colour used to display the count of remaining mature items
  104. during a drill session. Mature items are due for review, but are not new."
  105. :group 'org-drill
  106. :type 'color)
  107. (defcustom org-drill-failed-count-color
  108. "red"
  109. "Foreground colour used to display the count of remaining failed items
  110. during a drill session."
  111. :group 'org-drill
  112. :type 'color)
  113. (defcustom org-drill-done-count-color
  114. "sienna"
  115. "Foreground colour used to display the count of reviewed items
  116. during a drill session."
  117. :group 'org-drill
  118. :type 'color)
  119. (setplist 'org-drill-cloze-overlay-defaults
  120. '(display "[...]"
  121. face org-drill-hidden-cloze-face
  122. window t))
  123. (defvar org-drill-cloze-regexp
  124. ;; ver 1 "[^][]\\(\\[[^][][^]]*\\]\\)"
  125. ;; ver 2 "\\(\\[.*?\\]\\|^[^[[:cntrl:]]*?\\]\\|\\[.*?$\\)"
  126. ;; ver 3! "\\(\\[.*?\\]\\|\\[.*?[[:cntrl:]]+.*?\\]\\)"
  127. "\\(\\[[[:cntrl:][:graph:][:space:]]*?\\)\\(\\||.+?\\)\\(\\]\\)")
  128. (defvar org-drill-cloze-keywords
  129. `((,org-drill-cloze-regexp
  130. (1 'org-drill-visible-cloze-face nil)
  131. (2 'org-drill-visible-cloze-hint-face t)
  132. (3 'org-drill-visible-cloze-face nil)
  133. )))
  134. (defcustom org-drill-card-type-alist
  135. '((nil . org-drill-present-simple-card)
  136. ("simple" . org-drill-present-simple-card)
  137. ("twosided" . org-drill-present-two-sided-card)
  138. ("multisided" . org-drill-present-multi-sided-card)
  139. ("multicloze" . org-drill-present-multicloze)
  140. ("spanish_verb" . org-drill-present-spanish-verb))
  141. "Alist associating card types with presentation functions. Each entry in the
  142. alist takes the form (CARDTYPE . FUNCTION), where CARDTYPE is a string
  143. or nil, and FUNCTION is a function which takes no arguments and returns a
  144. boolean value."
  145. :group 'org-drill
  146. :type '(alist :key-type (choice string (const nil)) :value-type function))
  147. (defcustom org-drill-spaced-repetition-algorithm
  148. 'sm5
  149. "Which SuperMemo spaced repetition algorithm to use for scheduling items.
  150. Available choices are SM2 and SM5."
  151. :group 'org-drill
  152. :type '(choice (const 'sm2) (const 'sm5)))
  153. (defcustom org-drill-add-random-noise-to-intervals-p
  154. nil
  155. "If true, the number of days until an item's next repetition
  156. will vary slightly from the interval calculated by the SM2
  157. algorithm. The variation is very small when the interval is
  158. small, and scales up with the interval. The code for calculating
  159. random noise is adapted from Mnemosyne."
  160. :group 'org-drill
  161. :type 'boolean)
  162. (defcustom org-drill-cram-hours
  163. 12
  164. "When in cram mode, items are considered due for review if
  165. they were reviewed at least this many hours ago."
  166. :group 'org-drill
  167. :type 'integer)
  168. (defvar *org-drill-session-qualities* nil)
  169. (defvar *org-drill-start-time* 0)
  170. (defvar *org-drill-new-entries* nil)
  171. (defvar *org-drill-mature-entries* nil)
  172. (defvar *org-drill-failed-entries* nil)
  173. (defvar *org-drill-again-entries* nil)
  174. (defvar *org-drill-done-entries* nil)
  175. (defvar *org-drill-cram-mode* nil
  176. "Are we in 'cram mode', where all items are considered due
  177. for review unless they were already reviewed in the recent past?")
  178. ;;;; Utilities ================================================================
  179. (defun free-marker (m)
  180. (set-marker m nil))
  181. (defmacro pop-random (place)
  182. (let ((elt (gensym)))
  183. `(if (null ,place)
  184. nil
  185. (let ((,elt (nth (random (length ,place)) ,place)))
  186. (setq ,place (remove ,elt ,place))
  187. ,elt))))
  188. (defun shuffle-list (list)
  189. "Randomly permute the elements of LIST (all permutations equally likely)."
  190. ;; Adapted from 'shuffle-vector' in cookie1.el
  191. (let ((i 0)
  192. j
  193. temp
  194. (len (length list)))
  195. (while (< i len)
  196. (setq j (+ i (random (- len i))))
  197. (setq temp (nth i list))
  198. (setf (nth i list) (nth j list))
  199. (setf (nth j list) temp)
  200. (setq i (1+ i))))
  201. list)
  202. (defun time-to-inactive-org-timestamp (time)
  203. (format-time-string
  204. (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")
  205. time))
  206. (defmacro with-hidden-cloze-text (&rest body)
  207. `(progn
  208. (org-drill-hide-clozed-text)
  209. (unwind-protect
  210. (progn
  211. ,@body)
  212. (org-drill-unhide-clozed-text))))
  213. (defun org-drill-days-since-last-review ()
  214. "Nil means a last review date has not yet been stored for
  215. the item.
  216. Zero means it was reviewed today.
  217. A positive number means it was reviewed that many days ago.
  218. A negative number means the date of last review is in the future --
  219. this should never happen."
  220. (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
  221. (when datestr
  222. (- (time-to-days (current-time))
  223. (time-to-days (apply 'encode-time
  224. (org-parse-time-string datestr)))))))
  225. (defun org-drill-hours-since-last-review ()
  226. "Like `org-drill-days-since-last-review', but return value is
  227. in hours rather than days."
  228. (let ((datestr (org-entry-get (point) "DRILL_LAST_REVIEWED")))
  229. (when datestr
  230. (floor
  231. (/ (- (time-to-seconds (current-time))
  232. (time-to-seconds (apply 'encode-time
  233. (org-parse-time-string datestr))))
  234. (* 60 60))))))
  235. (defun org-drill-entry-p ()
  236. "Is the current entry a 'drill item'?"
  237. (or (org-entry-get (point) "LEARN_DATA")
  238. ;;(assoc "LEARN_DATA" (org-entry-properties nil))
  239. (member org-drill-question-tag (org-get-local-tags))))
  240. (defun org-part-of-drill-entry-p ()
  241. "Is the current entry either the main heading of a 'drill item',
  242. or a subheading within a drill item?"
  243. (or (org-drill-entry-p)
  244. ;; Does this heading INHERIT the drill tag
  245. (member org-drill-question-tag (org-get-tags-at))))
  246. (defun org-drill-goto-drill-entry-heading ()
  247. "Move the point to the heading which hold the :drill: tag for this
  248. drill entry."
  249. (unless (org-at-heading-p)
  250. (org-back-to-heading))
  251. (unless (org-part-of-drill-entry-p)
  252. (error "Point is not inside a drill entry"))
  253. (while (not (org-drill-entry-p))
  254. (unless (org-up-heading-safe)
  255. (error "Cannot find a parent heading that is marked as a drill entry"))))
  256. (defun org-drill-entry-leech-p ()
  257. "Is the current entry a 'leech item'?"
  258. (and (org-drill-entry-p)
  259. (member "leech" (org-get-local-tags))))
  260. (defun org-drill-entry-due-p ()
  261. (cond
  262. (*org-drill-cram-mode*
  263. (let ((hours (org-drill-hours-since-last-review)))
  264. (and (org-drill-entry-p)
  265. (or (null hours)
  266. (>= hours org-drill-cram-hours)))))
  267. (t
  268. (let ((item-time (org-get-scheduled-time (point))))
  269. (and (org-drill-entry-p)
  270. (or (not (eql 'skip org-drill-leech-method))
  271. (not (org-drill-entry-leech-p)))
  272. (or (null item-time)
  273. (not (minusp ; scheduled for today/in future
  274. (- (time-to-days (current-time))
  275. (time-to-days item-time))))))))))
  276. (defun org-drill-entry-new-p ()
  277. (and (org-drill-entry-p)
  278. (let ((item-time (org-get-scheduled-time (point))))
  279. (null item-time))))
  280. (defun org-drill-entry-last-quality ()
  281. (let ((quality (org-entry-get (point) "DRILL_LAST_QUALITY")))
  282. (if quality
  283. (string-to-number quality)
  284. nil)))
  285. ;;; SM2 Algorithm =============================================================
  286. (defun determine-next-interval-sm2 (last-interval n ef quality of-matrix)
  287. "Arguments:
  288. - LAST-INTERVAL -- the number of days since the item was last reviewed.
  289. - N -- the number of times the item has been successfully reviewed
  290. - EF -- the 'easiness factor'
  291. - QUALITY -- 0 to 5
  292. - OF-MATRIX -- a matrix of values, used by SM5 but not by SM2.
  293. Returns a list: (INTERVAL N EF OFMATRIX), where:
  294. - INTERVAL is the number of days until the item should next be reviewed
  295. - N is incremented by 1.
  296. - EF is modified based on the recall quality for the item.
  297. - OF-MATRIX is not modified."
  298. (assert (> n 0))
  299. (assert (and (>= quality 0) (<= quality 5)))
  300. (if (<= quality org-drill-failure-quality)
  301. ;; When an item is failed, its interval is reset to 0,
  302. ;; but its EF is unchanged
  303. (list -1 1 ef of-matrix)
  304. ;; else:
  305. (let* ((next-ef (modify-e-factor ef quality))
  306. (interval
  307. (cond
  308. ((<= n 1) 1)
  309. ((= n 2)
  310. (cond
  311. (org-drill-add-random-noise-to-intervals-p
  312. (case quality
  313. (5 6)
  314. (4 4)
  315. (3 3)
  316. (2 1)
  317. (t -1)))
  318. (t 6)))
  319. (t (ceiling (* last-interval next-ef))))))
  320. (list (round
  321. (if org-drill-add-random-noise-to-intervals-p
  322. (+ last-interval (* (- interval last-interval)
  323. (org-drill-random-dispersal-factor)))
  324. interval))
  325. (1+ n) next-ef of-matrix))))
  326. ;;; SM5 Algorithm =============================================================
  327. ;;; From http://www.supermemo.com/english/ol/sm5.htm
  328. (defun org-drill-random-dispersal-factor ()
  329. (let ((a 0.047)
  330. (b 0.092)
  331. (p (- (random* 1.0) 0.5)))
  332. (flet ((sign (n)
  333. (cond ((zerop n) 0)
  334. ((plusp n) 1)
  335. (t -1))))
  336. (/ (+ 100 (* (* (/ -1 b) (log (- 1 (* (/ b a ) (abs p)))))
  337. (sign p)))
  338. 100))))
  339. (defun inter-repetition-interval-sm5 (last-interval n ef &optional of-matrix)
  340. (let ((of (get-optimal-factor n ef of-matrix)))
  341. (if (= 1 n)
  342. of
  343. (* of last-interval))))
  344. (defun determine-next-interval-sm5 (last-interval n ef quality of-matrix)
  345. (assert (> n 0))
  346. (assert (and (>= quality 0) (<= quality 5)))
  347. (let ((next-ef (modify-e-factor ef quality))
  348. (interval nil))
  349. (setq of-matrix
  350. (set-optimal-factor n next-ef of-matrix
  351. (modify-of (get-optimal-factor n ef of-matrix)
  352. quality org-learn-fraction))
  353. ef next-ef)
  354. (cond
  355. ;; "Failed" -- reset repetitions to 0,
  356. ((<= quality org-drill-failure-quality)
  357. (list -1 1 ef of-matrix)) ; Not clear if OF matrix is supposed to be
  358. ; preserved
  359. ;; For a zero-based quality of 4 or 5, don't repeat
  360. ((and (>= quality 4)
  361. (not org-learn-always-reschedule))
  362. (list 0 (1+ n) ef of-matrix)) ; 0 interval = unschedule
  363. (t
  364. (setq interval (inter-repetition-interval-sm5
  365. last-interval n ef of-matrix))
  366. (if org-drill-add-random-noise-to-intervals-p
  367. (setq interval (+ last-interval
  368. (* (- interval last-interval)
  369. (org-drill-random-dispersal-factor)))))
  370. (list (round interval) (1+ n) ef of-matrix)))))
  371. ;;; Essentially copied from `org-learn.el', but modified to
  372. ;;; optionally call the SM2 function above.
  373. (defun org-drill-smart-reschedule (quality)
  374. (interactive "nHow well did you remember the information (on a scale of 0-5)? ")
  375. (let* ((learn-str (org-entry-get (point) "LEARN_DATA"))
  376. (learn-data (or (and learn-str
  377. (read learn-str))
  378. (copy-list initial-repetition-state)))
  379. closed-dates)
  380. (setq learn-data
  381. (case org-drill-spaced-repetition-algorithm
  382. (sm5 (determine-next-interval-sm5 (nth 0 learn-data)
  383. (nth 1 learn-data)
  384. (nth 2 learn-data)
  385. quality
  386. (nth 3 learn-data)))
  387. (sm2 (determine-next-interval-sm2 (nth 0 learn-data)
  388. (nth 1 learn-data)
  389. (nth 2 learn-data)
  390. quality
  391. (nth 3 learn-data)))))
  392. (org-entry-put (point) "LEARN_DATA" (prin1-to-string learn-data))
  393. (cond
  394. ((= 0 (nth 0 learn-data))
  395. (org-schedule t))
  396. ((minusp (first learn-data))
  397. (org-schedule nil (current-time)))
  398. (t
  399. (org-schedule nil (time-add (current-time)
  400. (days-to-time (nth 0 learn-data))))))))
  401. (defun org-drill-reschedule ()
  402. "Returns quality rating (0-5), or nil if the user quit."
  403. (let ((ch nil))
  404. (while (not (memq ch '(?q ?e ?0 ?1 ?2 ?3 ?4 ?5)))
  405. (setq ch (read-char-exclusive
  406. (if (eq ch ??)
  407. "0-2 Means you have forgotten the item.
  408. 3-5 Means you have remembered the item.
  409. 0 - Completely forgot.
  410. 1 - Even after seeing the answer, it still took a bit to sink in.
  411. 2 - After seeing the answer, you remembered it.
  412. 3 - It took you awhile, but you finally remembered.
  413. 4 - After a little bit of thought you remembered.
  414. 5 - You remembered the item really easily.
  415. How well did you do? (0-5, ?=help, e=edit, t=tags, q=quit)"
  416. "How well did you do? (0-5, ?=help, e=edit, q=quit)")))
  417. (if (eql ch ?t)
  418. (org-set-tags-command)))
  419. (cond
  420. ((and (>= ch ?0) (<= ch ?5))
  421. (let ((quality (- ch ?0))
  422. (failures (org-entry-get (point) "DRILL_FAILURE_COUNT")))
  423. (save-excursion
  424. (org-drill-smart-reschedule quality))
  425. (push quality *org-drill-session-qualities*)
  426. (cond
  427. ((<= quality org-drill-failure-quality)
  428. (when org-drill-leech-failure-threshold
  429. (setq failures (if failures (string-to-number failures) 0))
  430. (org-set-property "DRILL_FAILURE_COUNT"
  431. (format "%d" (1+ failures)))
  432. (if (> (1+ failures) org-drill-leech-failure-threshold)
  433. (org-toggle-tag "leech" 'on))))
  434. (t
  435. (let ((scheduled-time (org-get-scheduled-time (point))))
  436. (when scheduled-time
  437. (message "Next review in %d days"
  438. (- (time-to-days scheduled-time)
  439. (time-to-days (current-time))))
  440. (sit-for 0.5)))))
  441. (org-set-property "DRILL_LAST_QUALITY" (format "%d" quality))
  442. (org-set-property "DRILL_LAST_REVIEWED"
  443. (time-to-inactive-org-timestamp (current-time)))
  444. quality))
  445. ((= ch ?e)
  446. 'edit)
  447. (t
  448. nil))))
  449. (defun org-drill-hide-all-subheadings-except (heading-list)
  450. "Returns a list containing the position of each immediate subheading of
  451. the current topic."
  452. (let ((drill-entry-level (org-current-level))
  453. (drill-sections nil)
  454. (drill-heading nil))
  455. (org-show-subtree)
  456. (save-excursion
  457. (org-map-entries
  458. (lambda ()
  459. (when (= (org-current-level) (1+ drill-entry-level))
  460. (setq drill-heading (org-get-heading t))
  461. (unless (member drill-heading heading-list)
  462. (hide-subtree))
  463. (push (point) drill-sections)))
  464. "" 'tree))
  465. (reverse drill-sections)))
  466. (defun org-drill-presentation-prompt (&rest fmt-and-args)
  467. (let* ((item-start-time (current-time))
  468. (ch nil)
  469. (last-second 0)
  470. (prompt
  471. (if fmt-and-args
  472. (apply 'format
  473. (first fmt-and-args)
  474. (rest fmt-and-args))
  475. (concat "Press key for answer, "
  476. "e=edit, t=tags, s=skip, q=quit."))))
  477. (setq prompt
  478. (format "%s %s %s %s %s"
  479. (propertize
  480. (number-to-string (length *org-drill-done-entries*))
  481. 'face `(:foreground ,org-drill-done-count-color)
  482. 'help-echo "The number of items you have reviewed this session.")
  483. (propertize
  484. (number-to-string (+ (length *org-drill-again-entries*)
  485. (length *org-drill-failed-entries*)))
  486. 'face `(:foreground ,org-drill-failed-count-color)
  487. 'help-echo (concat "The number of items that you failed, "
  488. "and need to review again."))
  489. (propertize
  490. (number-to-string (length *org-drill-mature-entries*))
  491. 'face `(:foreground ,org-drill-mature-count-color)
  492. 'help-echo "The number of old items due for review.")
  493. (propertize
  494. (number-to-string (length *org-drill-new-entries*))
  495. 'face `(:foreground ,org-drill-new-count-color)
  496. 'help-echo (concat "The number of new items that you "
  497. "have never reviewed."))
  498. prompt))
  499. (if (and (eql 'warn org-drill-leech-method)
  500. (org-drill-entry-leech-p))
  501. (setq prompt (concat
  502. (propertize "!!! LEECH ITEM !!!
  503. You seem to be having a lot of trouble memorising this item.
  504. Consider reformulating the item to make it easier to remember.\n"
  505. 'face '(:foreground "red"))
  506. prompt)))
  507. (while (memq ch '(nil ?t))
  508. (while (not (input-pending-p))
  509. (message (concat (format-time-string
  510. "%M:%S " (time-subtract
  511. (current-time) item-start-time))
  512. prompt))
  513. (sit-for 1))
  514. (setq ch (read-char-exclusive))
  515. (if (eql ch ?t)
  516. (org-set-tags-command)))
  517. (case ch
  518. (?q nil)
  519. (?e 'edit)
  520. (?s 'skip)
  521. (otherwise t))))
  522. (defun org-pos-in-regexp (pos regexp &optional nlines)
  523. (save-excursion
  524. (goto-char pos)
  525. (org-in-regexp regexp nlines)))
  526. (defun org-drill-hide-clozed-text ()
  527. (save-excursion
  528. (while (re-search-forward org-drill-cloze-regexp nil t)
  529. ;; Don't hide org links, partly because they might contain inline
  530. ;; images which we want to keep visible
  531. (unless (org-pos-in-regexp (match-beginning 0)
  532. org-bracket-link-regexp 1)
  533. (org-drill-hide-matched-cloze-text)))))
  534. (defun org-drill-hide-matched-cloze-text ()
  535. "Hide the current match with a 'cloze' visual overlay."
  536. (let ((ovl (make-overlay (match-beginning 0) (match-end 0))))
  537. (overlay-put ovl 'category
  538. 'org-drill-cloze-overlay-defaults)
  539. (when (find ?| (match-string 0))
  540. (overlay-put ovl
  541. 'display
  542. (format "[...%s]"
  543. (substring-no-properties
  544. (match-string 0)
  545. (1+ (position ?| (match-string 0)))
  546. (1- (length (match-string 0)))))))))
  547. (defun org-drill-unhide-clozed-text ()
  548. (save-excursion
  549. (dolist (ovl (overlays-in (point-min) (point-max)))
  550. (when (eql 'org-drill-cloze-overlay-defaults (overlay-get ovl 'category))
  551. (delete-overlay ovl)))))
  552. ;;; Presentation functions ====================================================
  553. ;; Each of these is called with point on topic heading. Each needs to show the
  554. ;; topic in the form of a 'question' or with some information 'hidden', as
  555. ;; appropriate for the card type. The user should then be prompted to press a
  556. ;; key. The function should then reveal either the 'answer' or the entire
  557. ;; topic, and should return t if the user chose to see the answer and rate their
  558. ;; recall, nil if they chose to quit.
  559. (defun org-drill-present-simple-card ()
  560. (with-hidden-cloze-text
  561. (org-drill-hide-all-subheadings-except nil)
  562. (org-display-inline-images t)
  563. (org-cycle-hide-drawers 'all)
  564. (prog1 (org-drill-presentation-prompt)
  565. (org-show-subtree))))
  566. (defun org-drill-present-two-sided-card ()
  567. (with-hidden-cloze-text
  568. (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
  569. (when drill-sections
  570. (save-excursion
  571. (goto-char (nth (random (min 2 (length drill-sections)))
  572. drill-sections))
  573. (org-show-subtree)))
  574. (org-display-inline-images t)
  575. (org-cycle-hide-drawers 'all)
  576. (prog1
  577. (org-drill-presentation-prompt)
  578. (org-show-subtree)))))
  579. (defun org-drill-present-multi-sided-card ()
  580. (with-hidden-cloze-text
  581. (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
  582. (when drill-sections
  583. (save-excursion
  584. (goto-char (nth (random (length drill-sections)) drill-sections))
  585. (org-show-subtree)))
  586. (org-display-inline-images t)
  587. (org-cycle-hide-drawers 'all)
  588. (prog1
  589. (org-drill-presentation-prompt)
  590. (org-show-subtree)))))
  591. (defun org-drill-present-multicloze ()
  592. (let ((item-end nil)
  593. (match-count 0)
  594. (body-start (or (cdr (org-get-property-block))
  595. (point))))
  596. (org-drill-hide-all-subheadings-except nil)
  597. (save-excursion
  598. (outline-next-heading)
  599. (setq item-end (point)))
  600. (save-excursion
  601. (goto-char body-start)
  602. (while (re-search-forward org-drill-cloze-regexp item-end t)
  603. (incf match-count)))
  604. (when (plusp match-count)
  605. (save-excursion
  606. (goto-char body-start)
  607. (re-search-forward org-drill-cloze-regexp
  608. item-end t (1+ (random match-count)))
  609. (org-drill-hide-matched-cloze-text)))
  610. (org-display-inline-images t)
  611. (org-cycle-hide-drawers 'all)
  612. (prog1 (org-drill-presentation-prompt)
  613. (org-show-subtree)
  614. (org-drill-unhide-clozed-text))))
  615. (defun org-drill-present-spanish-verb ()
  616. (let ((prompt nil)
  617. (reveal-headings nil))
  618. (with-hidden-cloze-text
  619. (case (random 6)
  620. (0
  621. (org-drill-hide-all-subheadings-except '("Infinitive"))
  622. (setq prompt
  623. (concat "Translate this Spanish verb, and conjugate it "
  624. "for the *present* tense.")
  625. reveal-headings '("English" "Present Tense" "Notes")))
  626. (1
  627. (org-drill-hide-all-subheadings-except '("English"))
  628. (setq prompt (concat "For the *present* tense, conjugate the "
  629. "Spanish translation of this English verb.")
  630. reveal-headings '("Infinitive" "Present Tense" "Notes")))
  631. (2
  632. (org-drill-hide-all-subheadings-except '("Infinitive"))
  633. (setq prompt (concat "Translate this Spanish verb, and "
  634. "conjugate it for the *past* tense.")
  635. reveal-headings '("English" "Past Tense" "Notes")))
  636. (3
  637. (org-drill-hide-all-subheadings-except '("English"))
  638. (setq prompt (concat "For the *past* tense, conjugate the "
  639. "Spanish translation of this English verb.")
  640. reveal-headings '("Infinitive" "Past Tense" "Notes")))
  641. (4
  642. (org-drill-hide-all-subheadings-except '("Infinitive"))
  643. (setq prompt (concat "Translate this Spanish verb, and "
  644. "conjugate it for the *future perfect* tense.")
  645. reveal-headings '("English" "Future Perfect Tense" "Notes")))
  646. (5
  647. (org-drill-hide-all-subheadings-except '("English"))
  648. (setq prompt (concat "For the *future perfect* tense, conjugate the "
  649. "Spanish translation of this English verb.")
  650. reveal-headings '("Infinitive" "Future Perfect Tense" "Notes"))))
  651. (org-cycle-hide-drawers 'all)
  652. (prog1
  653. (org-drill-presentation-prompt prompt)
  654. (org-drill-hide-all-subheadings-except reveal-headings)))))
  655. (defun org-drill-entry ()
  656. "Present the current topic for interactive review, as in `org-drill'.
  657. Review will occur regardless of whether the topic is due for review or whether
  658. it meets the definition of a 'review topic' used by `org-drill'.
  659. Returns a quality rating from 0 to 5, or nil if the user quit, or the symbol
  660. EDIT if the user chose to exit the drill and edit the current item.
  661. See `org-drill' for more details."
  662. (interactive)
  663. (org-drill-goto-drill-entry-heading)
  664. ;;(unless (org-part-of-drill-entry-p)
  665. ;; (error "Point is not inside a drill entry"))
  666. ;;(unless (org-at-heading-p)
  667. ;; (org-back-to-heading))
  668. (let ((card-type (org-entry-get (point) "DRILL_CARD_TYPE"))
  669. (cont nil))
  670. (save-restriction
  671. (org-narrow-to-subtree)
  672. (org-show-subtree)
  673. (org-cycle-hide-drawers 'all)
  674. (let ((presentation-fn (cdr (assoc card-type org-drill-card-type-alist))))
  675. (cond
  676. (presentation-fn
  677. (setq cont (funcall presentation-fn)))
  678. (t
  679. (error "Unknown card type: '%s'" card-type))))
  680. (cond
  681. ((not cont)
  682. (message "Quit")
  683. nil)
  684. ((eql cont 'edit)
  685. 'edit)
  686. ((eql cont 'skip)
  687. 'skip)
  688. (t
  689. (save-excursion
  690. (org-drill-reschedule)))))))
  691. ;; (defun org-drill-entries (entries)
  692. ;; "Returns nil, t, or a list of markers representing entries that were
  693. ;; 'failed' and need to be presented again before the session ends."
  694. ;; (let ((again-entries nil))
  695. ;; (setq *org-drill-done-entry-count* 0
  696. ;; *org-drill-pending-entry-count* (length entries))
  697. ;; (if (and org-drill-maximum-items-per-session
  698. ;; (> (length entries)
  699. ;; org-drill-maximum-items-per-session))
  700. ;; (setq entries (subseq entries 0
  701. ;; org-drill-maximum-items-per-session)))
  702. ;; (block org-drill-entries
  703. ;; (dolist (m entries)
  704. ;; (save-restriction
  705. ;; (switch-to-buffer (marker-buffer m))
  706. ;; (goto-char (marker-position m))
  707. ;; (setq result (org-drill-entry))
  708. ;; (cond
  709. ;; ((null result)
  710. ;; (message "Quit")
  711. ;; (return-from org-drill-entries nil))
  712. ;; ((eql result 'edit)
  713. ;; (setq end-pos (point-marker))
  714. ;; (return-from org-drill-entries nil))
  715. ;; (t
  716. ;; (cond
  717. ;; ((< result 3)
  718. ;; (push m again-entries))
  719. ;; (t
  720. ;; (decf *org-drill-pending-entry-count*)
  721. ;; (incf *org-drill-done-entry-count*)))
  722. ;; (when (and org-drill-maximum-duration
  723. ;; (> (- (float-time (current-time)) *org-drill-start-time*)
  724. ;; (* org-drill-maximum-duration 60)))
  725. ;; (message "This drill session has reached its maximum duration.")
  726. ;; (return-from org-drill-entries nil))))))
  727. ;; (or again-entries
  728. ;; t))))
  729. (defun org-drill-entries-pending-p ()
  730. (or *org-drill-again-entries*
  731. (and (not (org-drill-maximum-item-count-reached-p))
  732. (not (org-drill-maximum-duration-reached-p))
  733. (or *org-drill-new-entries*
  734. *org-drill-failed-entries*
  735. *org-drill-mature-entries*
  736. *org-drill-again-entries*))))
  737. (defun org-drill-pending-entry-count ()
  738. (+ (length *org-drill-new-entries*)
  739. (length *org-drill-failed-entries*)
  740. (length *org-drill-mature-entries*)
  741. (length *org-drill-again-entries*)))
  742. (defun org-drill-maximum-duration-reached-p ()
  743. "Returns true if the current drill session has continued past its
  744. maximum duration."
  745. (and org-drill-maximum-duration
  746. *org-drill-start-time*
  747. (> (- (float-time (current-time)) *org-drill-start-time*)
  748. (* org-drill-maximum-duration 60))))
  749. (defun org-drill-maximum-item-count-reached-p ()
  750. "Returns true if the current drill session has reached the
  751. maximum number of items."
  752. (and org-drill-maximum-items-per-session
  753. (>= (length *org-drill-done-entries*)
  754. org-drill-maximum-items-per-session)))
  755. (defun org-drill-pop-next-pending-entry ()
  756. (cond
  757. ;; First priority is items we failed in a prior session.
  758. ((and *org-drill-failed-entries*
  759. (not (org-drill-maximum-item-count-reached-p))
  760. (not (org-drill-maximum-duration-reached-p)))
  761. (pop-random *org-drill-failed-entries*))
  762. ;; Next priority is newly added items, and items which
  763. ;; are not new and were not failed when they were last
  764. ;; reviewed.
  765. ((and (or *org-drill-new-entries*
  766. *org-drill-mature-entries*)
  767. (not (org-drill-maximum-item-count-reached-p))
  768. (not (org-drill-maximum-duration-reached-p)))
  769. (if (< (random (+ (length *org-drill-new-entries*)
  770. (length *org-drill-mature-entries*)))
  771. (length *org-drill-new-entries*))
  772. (pop-random *org-drill-new-entries*)
  773. ;; else
  774. (pop-random *org-drill-mature-entries*)))
  775. ;; After all the above are done, last priority is items
  776. ;; that were failed earlier THIS SESSION.
  777. (*org-drill-again-entries*
  778. (pop-random *org-drill-again-entries*))
  779. (t
  780. nil)))
  781. (defun org-drill-entries ()
  782. "Returns nil, t, or a list of markers representing entries that were
  783. 'failed' and need to be presented again before the session ends."
  784. (block org-drill-entries
  785. (while (org-drill-entries-pending-p)
  786. (setq m (org-drill-pop-next-pending-entry))
  787. (unless m
  788. (error "Unexpectedly ran out of pending drill items"))
  789. (save-excursion
  790. (set-buffer (marker-buffer m))
  791. (goto-char m)
  792. (setq result (org-drill-entry))
  793. (cond
  794. ((null result)
  795. (message "Quit")
  796. (return-from org-drill-entries nil))
  797. ((eql result 'edit)
  798. (setq end-pos (point-marker))
  799. (return-from org-drill-entries nil))
  800. ((eql result 'skip)
  801. nil) ; skip this item
  802. (t
  803. (cond
  804. ((<= result org-drill-failure-quality)
  805. (push m *org-drill-again-entries*))
  806. (t
  807. (push m *org-drill-done-entries*)))))))))
  808. (defun org-drill-final-report ()
  809. (read-char-exclusive
  810. (format
  811. "%d items reviewed
  812. %d items awaiting review (%s, %s, %s)
  813. Session duration %s
  814. Recall of reviewed items:
  815. Excellent (5): %3d%% | Near miss (2): %3d%%
  816. Good (4): %3d%% | Failure (1): %3d%%
  817. Hard (3): %3d%% | Total failure (0): %3d%%
  818. Session finished. Press a key to continue..."
  819. (length *org-drill-done-entries*)
  820. (org-drill-pending-entry-count)
  821. (propertize
  822. (format "%d failed"
  823. (+ (length *org-drill-failed-entries*)
  824. (length *org-drill-again-entries*)))
  825. 'face `(:foreground ,org-drill-failed-count-color))
  826. (propertize
  827. (format "%d old"
  828. (length *org-drill-mature-entries*))
  829. 'face `(:foreground ,org-drill-mature-count-color))
  830. (propertize
  831. (format "%d new"
  832. (length *org-drill-new-entries*))
  833. 'face `(:foreground ,org-drill-new-count-color))
  834. (format-seconds "%h:%.2m:%.2s"
  835. (- (float-time (current-time)) *org-drill-start-time*))
  836. (round (* 100 (count 5 *org-drill-session-qualities*))
  837. (max 1 (length *org-drill-session-qualities*)))
  838. (round (* 100 (count 2 *org-drill-session-qualities*))
  839. (max 1 (length *org-drill-session-qualities*)))
  840. (round (* 100 (count 4 *org-drill-session-qualities*))
  841. (max 1 (length *org-drill-session-qualities*)))
  842. (round (* 100 (count 1 *org-drill-session-qualities*))
  843. (max 1 (length *org-drill-session-qualities*)))
  844. (round (* 100 (count 3 *org-drill-session-qualities*))
  845. (max 1 (length *org-drill-session-qualities*)))
  846. (round (* 100 (count 0 *org-drill-session-qualities*))
  847. (max 1 (length *org-drill-session-qualities*)))
  848. )))
  849. (defun org-drill (&optional scope)
  850. "Begin an interactive 'drill session'. The user is asked to
  851. review a series of topics (headers). Each topic is initially
  852. presented as a 'question', often with part of the topic content
  853. hidden. The user attempts to recall the hidden information or
  854. answer the question, then presses a key to reveal the answer. The
  855. user then rates his or her recall or performance on that
  856. topic. This rating information is used to reschedule the topic
  857. for future review using the `org-learn' library.
  858. Org-drill proceeds by:
  859. - Finding all topics (headings) in SCOPE which have either been
  860. used and rescheduled by org-learn before (i.e. the LEARN_DATA
  861. property is set), or which have a tag that matches
  862. `org-drill-question-tag'.
  863. - All matching topics which are either unscheduled, or are
  864. scheduled for the current date or a date in the past, are
  865. considered to be candidates for the drill session.
  866. - If `org-drill-maximum-items-per-session' is set, a random
  867. subset of these topics is presented. Otherwise, all of the
  868. eligible topics will be presented.
  869. SCOPE determines the scope in which to search for
  870. questions. It is passed to `org-map-entries', and can be any of:
  871. nil The current buffer, respecting the restriction if any.
  872. This is the default.
  873. tree The subtree started with the entry at point
  874. file The current buffer, without restriction
  875. file-with-archives
  876. The current buffer, and any archives associated with it
  877. agenda All agenda files
  878. agenda-with-archives
  879. All agenda files with any archive files associated with them
  880. (file1 file2 ...)
  881. If this is a list, all files in the list will be scanned."
  882. (interactive)
  883. (let ((entries nil)
  884. (failed-entries nil)
  885. (result nil)
  886. (results nil)
  887. (end-pos nil)
  888. (cnt 0))
  889. (block org-drill
  890. (setq *org-drill-done-entries* nil
  891. *org-drill-new-entries* nil
  892. *org-drill-mature-entries* nil
  893. *org-drill-failed-entries* nil
  894. *org-drill-again-entries* nil)
  895. (setq *org-drill-session-qualities* nil)
  896. (setq *org-drill-start-time* (float-time (current-time)))
  897. (unwind-protect
  898. (save-excursion
  899. (let ((org-trust-scanner-tags t))
  900. (org-map-entries
  901. (lambda ()
  902. (when (zerop (% (incf cnt) 50))
  903. (message "Processing drill items: %4d%s"
  904. (+ (length *org-drill-new-entries*)
  905. (length *org-drill-mature-entries*)
  906. (length *org-drill-failed-entries*))
  907. (make-string (ceiling cnt 50) ?.)))
  908. (when (org-drill-entry-due-p)
  909. (cond
  910. ((org-drill-entry-new-p)
  911. (push (point-marker) *org-drill-new-entries*))
  912. ((and (org-drill-entry-last-quality)
  913. (<= (org-drill-entry-last-quality)
  914. org-drill-failure-quality))
  915. (push (point-marker) *org-drill-failed-entries*))
  916. (t
  917. (push (point-marker) *org-drill-mature-entries*)))))
  918. (concat "+" org-drill-question-tag) scope))
  919. ;; Failed first, then random mix of old + new
  920. (setq entries (append (shuffle-list *org-drill-failed-entries*)
  921. (shuffle-list (append *org-drill-mature-entries*
  922. *org-drill-new-entries*))))
  923. (cond
  924. ((and (null *org-drill-new-entries*)
  925. (null *org-drill-failed-entries*)
  926. (null *org-drill-mature-entries*))
  927. (message "I did not find any pending drill items."))
  928. (t
  929. (org-drill-entries)
  930. (message "Drill session finished!"))))
  931. ;; (cond
  932. ;; ((null entries)
  933. ;; (message "I did not find any pending drill items."))
  934. ;; (t
  935. ;; (let ((again t))
  936. ;; (while again
  937. ;; (when (listp again)
  938. ;; (setq entries (shuffle-list again)))
  939. ;; (setq again (org-drill-entries entries))
  940. ;; (cond
  941. ;; ((null again)
  942. ;; (return-from org-drill nil))
  943. ;; ((eql t again)
  944. ;; (setq again nil))))
  945. ;; (message "Drill session finished!")
  946. ;; ))))
  947. (progn
  948. (dolist (m (append *org-drill-new-entries*
  949. *org-drill-failed-entries*
  950. *org-drill-again-entries*
  951. *org-drill-mature-entries*))
  952. (free-marker m)))))
  953. (cond
  954. (end-pos
  955. (switch-to-buffer (marker-buffer end-pos))
  956. (goto-char (marker-position end-pos))
  957. (message "Edit topic."))
  958. (t
  959. (org-drill-final-report)))))
  960. (defun org-drill-cram (&optional scope)
  961. "Run an interactive drill session in 'cram mode'. In cram mode,
  962. all drill items are considered to be due for review, unless they
  963. have been reviewed within the last `org-drill-cram-hours'
  964. hours."
  965. (interactive)
  966. (let ((*org-drill-cram-mode* t))
  967. (org-drill scope)))
  968. (add-hook 'org-mode-hook
  969. (lambda ()
  970. (if org-drill-use-visible-cloze-face-p
  971. (font-lock-add-keywords
  972. 'org-mode
  973. org-drill-cloze-keywords
  974. t))))
  975. (provide 'org-drill)