org-drill.el 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. ;;; org-drill.el - Self-testing with org-learn
  2. ;;;
  3. ;;; Author: Paul Sexton <eeeickythump@gmail.com>
  4. ;;; Version: 1.0
  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. ;;;
  25. ;;; Installation
  26. ;;; ============
  27. ;;;
  28. ;;; Put the following in your .emacs:
  29. ;;;
  30. ;;; (add-to-list 'load-path "/path/to/org-drill/")
  31. ;;; (require 'org-drill)
  32. ;;;
  33. ;;;
  34. ;;; Writing the questions
  35. ;;; =====================
  36. ;;;
  37. ;;; See the file "spanish.org" for an example set of material.
  38. ;;;
  39. ;;; Tag all items you want to be asked about with a tag that matches
  40. ;;; `org-drill-question-tag'. This is :drill: by default.
  41. ;;;
  42. ;;; You don't need to schedule the topics initially. However org-drill *will*
  43. ;;; recognise items that have been scheduled previously with `org-learn'.
  44. ;;;
  45. ;;; Within each question, the answer can be included in the following ways:
  46. ;;;
  47. ;;; - Question in the main body text, answer in subtopics. This is the
  48. ;;; default. All subtopics will be shown collapsed, while the text under
  49. ;;; the main heading will stay visible.
  50. ;;;
  51. ;;; - Each subtopic contains a piece of information related to the topic. ONE
  52. ;;; of these will revealed at random, and the others hidden. To define a
  53. ;;; topic of this type, give the topic a property `DRILL_CARD_TYPE' with
  54. ;;; value `multisided'.
  55. ;;;
  56. ;;; - Cloze deletion -- any pieces of text in the body of the card that are
  57. ;;; surrounded with [SINGLE square brackets] will be hidden when the card is
  58. ;;; presented to the user, and revealed once they press a key. Cloze deletion
  59. ;;; is automatically applied to all topics.
  60. ;;;
  61. ;;; - No explicit answer -- the user judges whether they recalled the
  62. ;;; fact adequately.
  63. ;;;
  64. ;;; - Other methods of your own devising, provided you write a function to
  65. ;;; handle selective display of the topic. See the function
  66. ;;; `org-drill-present-spanish-verb', which handles topics of type "spanish_verb",
  67. ;;; for an example.
  68. ;;;
  69. ;;;
  70. ;;; Running the drill session
  71. ;;; =========================
  72. ;;;
  73. ;;; Start a drill session with `M-x org-drill'. This will include all eligible
  74. ;;; topics in the current buffer. `org-drill' can also be targeted at a particular
  75. ;;; subtree or particular files or sets of files; see the documentation of
  76. ;;; the function `org-drill' for details.
  77. ;;;
  78. ;;; During the drill session, you will be presented with each item, then asked
  79. ;;; to rate your recall of it by pressing a key between 0 and 5. At any time you
  80. ;;; can press 'q' to finish the drill early (your progress will be saved), or
  81. ;;; 'e' to finish the drill and jump to the current topic for editing.
  82. ;;;
  83. ;;;
  84. ;;; TODO
  85. ;;; ====
  86. ;;;
  87. ;;; - encourage org-learn to reschedule "4" and "5" items.
  88. ;;; - nicer "cloze face" which does not hide the space preceding the cloze,
  89. ;;; and behaves more nicely across line breaks
  90. ;;; - hide drawers.
  91. ;;; - org-drill-question-tag should use a tag match string, rather than a
  92. ;;; single tag
  93. ;;; - when finished, display a message showing how many items reviewed,
  94. ;;; how many still pending, numbers in each recall category
  95. (eval-when-compile (require 'cl))
  96. (eval-when-compile (require 'hi-lock))
  97. (require 'org)
  98. (require 'org-learn)
  99. (defgroup org-drill nil
  100. "Options concerning interactive drill sessions in Org mode (org-drill)."
  101. :tag "Org-Drill"
  102. :group 'org-link)
  103. (defcustom org-drill-question-tag
  104. "drill"
  105. "Tag which topics must possess in order to be identified as review topics
  106. by `org-drill'."
  107. :group 'org-drill
  108. :type 'string)
  109. (defcustom org-drill-maximum-items-per-session
  110. 30
  111. "Each drill session will present at most this many topics for review.
  112. Nil means unlimited."
  113. :group 'org-drill
  114. :type '(choice integer (const nil)))
  115. (defcustom org-drill-maximum-duration
  116. 20
  117. "Maximum duration of a drill session, in minutes.
  118. Nil means unlimited."
  119. :group 'org-drill
  120. :type '(choice integer (const nil)))
  121. (defface org-drill-hidden-cloze-face
  122. '((t (:foreground "blue" :background "blue")))
  123. "The face used to hide the contents of cloze phrases."
  124. :group 'org-drill)
  125. (defvar org-drill-cloze-regexp
  126. "[^][]\\(\\[[^][][^]]*\\]\\)")
  127. (defcustom org-drill-card-type-alist
  128. '((nil . org-drill-present-simple-card)
  129. ("simple" . org-drill-present-simple-card)
  130. ("multisided" . org-drill-present-multi-sided-card)
  131. ("spanish_verb" . org-drill-present-spanish-verb))
  132. "Alist associating card types with presentation functions. Each entry in the
  133. alist takes the form (CARDTYPE . FUNCTION), where CARDTYPE is a string
  134. or nil, and FUNCTION is a function which takes no arguments and returns a
  135. boolean value."
  136. :group 'org-drill
  137. :type '(alist :key-type (choice string (const nil)) :value-type function))
  138. (defun shuffle-list (list)
  139. "Randomly permute the elements of LIST (all permutations equally likely)."
  140. ;; Adapted from 'shuffle-vector' in cookie1.el
  141. (let ((i 0)
  142. j
  143. temp
  144. (len (length list)))
  145. (while (< i len)
  146. (setq j (+ i (random (- len i))))
  147. (setq temp (nth i list))
  148. (setf (nth i list) (nth j list))
  149. (setf (nth j list) temp)
  150. (setq i (1+ i))))
  151. list)
  152. (defun org-drill-entry-due-p ()
  153. (let ((item-time (org-get-scheduled-time (point))))
  154. (and (or (assoc "LEARN_DATA" (org-entry-properties nil))
  155. (member org-drill-question-tag (org-get-local-tags)))
  156. (or (null item-time)
  157. (not (minusp ; scheduled for today/in
  158. ; future
  159. (- (time-to-days (current-time))
  160. (time-to-days item-time))))))))
  161. (defun org-drill-reschedule ()
  162. (let ((ch nil))
  163. (while (not (memq ch '(?q ?0 ?1 ?2 ?3 ?4 ?5)))
  164. (setq ch (read-char
  165. (if (eq ch ??)
  166. "0-2 Means you have forgotten the item.
  167. 3-5 Means you have remembered the item.
  168. 0 - Completely forgot.
  169. 1 - Even after seeing the answer, it still took a bit to sink in.
  170. 2 - After seeing the answer, you remembered it.
  171. 3 - It took you awhile, but you finally remembered.
  172. 4 - After a little bit of thought you remembered.
  173. 5 - You remembered the item really easily.
  174. How well did you do? (0-5, ?=help, q=quit)"
  175. "How well did you do? (0-5, ?=help, q=quit)"))))
  176. (cond
  177. ((and (>= ch ?0) (<= ch ?5))
  178. (save-excursion
  179. (org-smart-reschedule (- ch 48)))
  180. ch)
  181. (t
  182. nil))))
  183. (defun org-drill-hide-all-subheadings-except (heading-list)
  184. "Returns a list containing the position of each immediate subheading of
  185. the current topic."
  186. (let ((drill-entry-level (org-current-level))
  187. (drill-sections nil)
  188. (drill-heading nil))
  189. (org-show-subtree)
  190. (save-excursion
  191. (org-map-entries
  192. (lambda ()
  193. (when (= (org-current-level) (1+ drill-entry-level))
  194. (setq drill-heading (org-get-heading t))
  195. (unless (member drill-heading heading-list)
  196. (hide-subtree))
  197. (push (point) drill-sections)))
  198. "" 'tree))
  199. (reverse drill-sections)))
  200. (defun org-drill-presentation-prompt (&rest fmt-and-args)
  201. (let ((ch (read-char (if fmt-and-args
  202. (apply 'format
  203. (first fmt-and-args)
  204. (rest fmt-and-args))
  205. "Press any key to see the answer, 'e' to edit, 'q' to quit."))))
  206. (case ch
  207. (?q nil)
  208. (?e 'edit)
  209. (otherwise t))))
  210. ;;; Presentation functions ====================================================
  211. ;; Each of these is called with point on topic heading. Each needs to show the
  212. ;; topic in the form of a 'question' or with some information 'hidden', as
  213. ;; appropriate for the card type. The user should then be prompted to press a
  214. ;; key. The function should then reveal either the 'answer' or the entire
  215. ;; topic, and should return t if the user chose to see the answer and rate their
  216. ;; recall, nil if they chose to quit.
  217. (defun org-drill-present-simple-card ()
  218. (org-drill-hide-all-subheadings-except nil)
  219. (prog1 (org-drill-presentation-prompt)
  220. (org-show-subtree)))
  221. (defun org-drill-present-multi-sided-card ()
  222. (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
  223. (when drill-sections
  224. (save-excursion
  225. (goto-char (nth (random (length drill-sections)) drill-sections))
  226. (org-show-subtree)))
  227. (prog1
  228. (org-drill-presentation-prompt)
  229. (org-show-subtree))))
  230. (defun org-drill-present-spanish-verb ()
  231. (case (random 6)
  232. (0
  233. (org-drill-hide-all-subheadings-except '("Infinitive"))
  234. (prog1
  235. (org-drill-presentation-prompt
  236. "Translate this Spanish verb, and conjugate it for the *present* tense.")
  237. (org-drill-hide-all-subheadings-except '("English" "Present Tense"
  238. "Notes"))))
  239. (1
  240. (org-drill-hide-all-subheadings-except '("English"))
  241. (prog1
  242. (org-drill-presentation-prompt
  243. "For the *present* tense, conjugate the Spanish translation of this English verb.")
  244. (org-drill-hide-all-subheadings-except '("Infinitive" "Present Tense"
  245. "Notes"))))
  246. (2
  247. (org-drill-hide-all-subheadings-except '("Infinitive"))
  248. (prog1
  249. (org-drill-presentation-prompt
  250. "Translate this Spanish verb, and conjugate it for the *past* tense.")
  251. (org-drill-hide-all-subheadings-except '("English" "Past Tense"
  252. "Notes"))))
  253. (3
  254. (org-drill-hide-all-subheadings-except '("English"))
  255. (prog1
  256. (org-drill-presentation-prompt
  257. "For the *past* tense, conjugate the Spanish translation of this English verb.")
  258. (org-drill-hide-all-subheadings-except '("Infinitive" "Past Tense"
  259. "Notes"))))
  260. (4
  261. (org-drill-hide-all-subheadings-except '("Infinitive"))
  262. (prog1
  263. (org-drill-presentation-prompt
  264. "Translate this Spanish verb, and conjugate it for the *future perfect* tense.")
  265. (org-drill-hide-all-subheadings-except '("English" "Future Perfect Tense"
  266. "Notes"))))
  267. (5
  268. (org-drill-hide-all-subheadings-except '("English"))
  269. (prog1
  270. (org-drill-presentation-prompt
  271. "For the *future perfect* tense, conjugate the Spanish translation of this English verb.")
  272. (org-drill-hide-all-subheadings-except '("Infinitive" "Future Perfect Tense"
  273. "Notes"))))))
  274. (defun org-drill-entry ()
  275. "Present the current topic for interactive review, as in `org-drill'.
  276. Review will occur regardless of whether the topic is due for review or whether
  277. it meets the definition of a 'review topic' used by `org-drill'.
  278. See `org-drill' for more details."
  279. (interactive)
  280. (unless (org-at-heading-p)
  281. (org-back-to-heading))
  282. (let ((card-type (cdr (assoc "DRILL_CARD_TYPE" (org-entry-properties nil))))
  283. (cont nil))
  284. (save-restriction
  285. (org-narrow-to-subtree)
  286. (org-show-subtree)
  287. (org-cycle-hide-drawers 'overview)
  288. (let ((presentation-fn (cdr (assoc card-type org-drill-card-type-alist))))
  289. (cond
  290. (presentation-fn
  291. (highlight-regexp org-drill-cloze-regexp
  292. 'org-drill-hidden-cloze-face)
  293. (setq cont (funcall presentation-fn))
  294. (unhighlight-regexp org-drill-cloze-regexp))
  295. (t
  296. (error "Unknown card type: '%s'" card-type))))
  297. (cond
  298. ((not cont)
  299. (message "Quit")
  300. nil)
  301. ((eql cont 'edit)
  302. 'edit)
  303. (t
  304. (save-excursion
  305. (org-drill-reschedule)))))))
  306. (defun org-drill (&optional scope)
  307. "Begin an interactive 'drill session'. The user is asked to
  308. review a series of topics (headers). Each topic is initially
  309. presented as a 'question', often with part of the topic content
  310. hidden. The user attempts to recall the hidden information or
  311. answer the question, then presses a key to reveal the answer. The
  312. user then rates his or her recall or performance on that
  313. topic. This rating information is used to reschedule the topic
  314. for future review using the `org-learn' library.
  315. Org-drill proceeds by:
  316. - Finding all topics (headings) in SCOPE which have either been
  317. used and rescheduled by org-learn before (i.e. the LEARN_DATA
  318. property is set), or which have a tag that matches
  319. `org-drill-question-tag'.
  320. - All matching topics which are either unscheduled, or are
  321. scheduled for the current date or a date in the past, are
  322. considered to be candidates for the drill session.
  323. - If `org-drill-maximum-items-per-session' is set, a random
  324. subset of these topics is presented. Otherwise, all of the
  325. eligible topics will be presented.
  326. SCOPE determines the scope in which to search for
  327. questions. It is passed to `org-map-entries', and can be any of:
  328. nil The current buffer, respecting the restriction if any.
  329. This is the default.
  330. tree The subtree started with the entry at point
  331. file The current buffer, without restriction
  332. file-with-archives
  333. The current buffer, and any archives associated with it
  334. agenda All agenda files
  335. agenda-with-archives
  336. All agenda files with any archive files associated with them
  337. (file1 file2 ...)
  338. If this is a list, all files in the list will be scanned."
  339. (interactive)
  340. (let ((entries nil)
  341. (result nil)
  342. (results nil)
  343. (end-pos nil))
  344. (block org-drill
  345. (save-excursion
  346. (org-map-entries
  347. (lambda () (if (org-drill-entry-due-p)
  348. (push (point-marker) entries)))
  349. "" scope)
  350. (cond
  351. ((null entries)
  352. (message "I did not find any pending drill items."))
  353. (t
  354. (let ((start-time (float-time (current-time))))
  355. (dolist (m (if (and org-drill-maximum-items-per-session
  356. (> (length entries)
  357. org-drill-maximum-items-per-session))
  358. (subseq (shuffle-list entries) 0
  359. org-drill-maximum-items-per-session)
  360. (shuffle-list entries)))
  361. (save-restriction
  362. (switch-to-buffer (marker-buffer m))
  363. (goto-char (marker-position m))
  364. (setq result (org-drill-entry))
  365. (cond
  366. ((null result)
  367. (message "Quit")
  368. (return-from org-drill nil))
  369. ((eql result 'edit)
  370. (setq end-pos (point-marker))
  371. (return-from org-drill nil))
  372. ((and org-drill-maximum-duration
  373. (> (- (float-time (current-time)) start-time)
  374. (* org-drill-maximum-duration 60)))
  375. (message "This drill session has reached its maximum duration.")
  376. (return-from org-drill nil)))))
  377. (message "Drill session finished!")
  378. )))))
  379. (when end-pos
  380. (switch-to-buffer (marker-buffer end-pos))
  381. (goto-char (marker-position end-pos))
  382. (message "Edit topic."))))
  383. (provide 'org-drill)