Browse Source

Add org-drill.el

Carsten Dominik 14 years ago
parent
commit
4ea5658a1a
2 changed files with 448 additions and 0 deletions
  1. 2 0
      contrib/README
  2. 446 0
      contrib/lisp/org-drill.el

+ 2 - 0
contrib/README

@@ -18,6 +18,8 @@ org-choose.el            --- Use TODO keywords to mark decision states
 org-collector.el         --- Collect properties into tables
 org-contribdir.el        --- Dummy file to mark the org contrib Lisp directory
 org-depend.el            --- TODO dependencies for Org-mode
+org-drill.el             --- Self-testing with org-learn
+org-depend.el            --- TODO dependencies for Org-mode
 org-elisp-symbol.el      --- Org links to emacs-lisp symbols
 org-eval.el              --- The <lisp> tag, adapted from Muse
 org-eval-light.el        --- Evaluate in-buffer code on demand

+ 446 - 0
contrib/lisp/org-drill.el

@@ -0,0 +1,446 @@
+;;; org-drill.el - Self-testing with org-learn
+;;;
+;;; Author: Paul Sexton <eeeickythump@gmail.com>
+;;; Version: 1.0
+;;; Repository at http://bitbucket.org/eeeickythump/org-drill/
+;;;
+;;;
+;;; Synopsis
+;;; ========
+;;;
+;;; Uses the spaced repetition algorithm in `org-learn' to conduct interactive
+;;; "drill sessions", where the material to be remembered is presented to the
+;;; student in random order. The student rates his or her recall of each item,
+;;; and this information is fed back to `org-learn' to schedule the item for
+;;; later revision.
+;;;
+;;; Each drill session can be restricted to topics in the current buffer
+;;; (default), one or several files, all agenda files, or a subtree. A single
+;;; topic can also be drilled.
+;;;
+;;; Different "card types" can be defined, which present their information to
+;;; the student in different ways.
+;;;
+;;;
+;;; Installation
+;;; ============
+;;;
+;;; Put the following in your .emacs:
+;;;
+;;; (add-to-list 'load-path "/path/to/org-drill/")
+;;; (require 'org-drill)
+;;;
+;;;
+;;; Writing the questions
+;;; =====================
+;;;
+;;; See the file "spanish.org" for an example set of material.
+;;;
+;;; Tag all items you want to be asked about with a tag that matches
+;;; `org-drill-question-tag'. This is :drill: by default.
+;;;
+;;; You don't need to schedule the topics initially.  However org-drill *will*
+;;; recognise items that have been scheduled previously with `org-learn'.
+;;;
+;;; Within each question, the answer can be included in the following ways:
+;;; 
+;;; - Question in the main body text, answer in subtopics. This is the
+;;;   default. All subtopics will be shown collapsed, while the text under
+;;;   the main heading will stay visible.
+;;;
+;;; - Each subtopic contains a piece of information related to the topic. ONE
+;;;   of these will revealed at random, and the others hidden. To define a
+;;;   topic of this type, give the topic a property `DRILL_CARD_TYPE' with
+;;;   value `multisided'.
+;;;
+;;; - Cloze deletion -- any pieces of text in the body of the card that are
+;;;   surrounded with [SINGLE square brackets] will be hidden when the card is
+;;;   presented to the user, and revealed once they press a key. Cloze deletion
+;;;   is automatically applied to all topics.
+;;; 
+;;; - No explicit answer -- the user judges whether they recalled the
+;;;   fact adequately.
+;;;
+;;; - Other methods of your own devising, provided you write a function to
+;;;   handle selective display of the topic. See the function
+;;;   `org-drill-present-spanish-verb', which handles topics of type "spanish_verb",
+;;;   for an example.
+;;;
+;;;
+;;; Running the drill session
+;;; =========================
+;;;
+;;; Start a drill session with `M-x org-drill'. This will include all eligible
+;;; topics in the current buffer. `org-drill' can also be targeted at a particular
+;;; subtree or particular files or sets of files; see the documentation of 
+;;; the function `org-drill' for details.
+;;;
+;;; During the drill session, you will be presented with each item, then asked
+;;; to rate your recall of it by pressing a key between 0 and 5. At any time you
+;;; can press 'q' to finish the drill early (your progress will be saved), or
+;;; 'e' to finish the drill and jump to the current topic for editing.
+;;;
+;;; 
+;;; TODO
+;;; ====
+;;;
+;;; - encourage org-learn to reschedule "4" and "5" items.
+;;; - nicer "cloze face" which does not hide the space preceding the cloze,
+;;;   and behaves more nicely across line breaks
+;;; - hide drawers.
+;;; - org-drill-question-tag should use a tag match string, rather than a
+;;;   single tag
+;;; - when finished, display a message showing how many items reviewed,
+;;;   how many still pending, numbers in each recall category
+
+(eval-when-compile (require 'cl))
+(eval-when-compile (require 'hi-lock))
+(require 'org)
+(require 'org-learn)
+
+
+(defgroup org-drill nil
+  "Options concerning interactive drill sessions in Org mode (org-drill)."
+  :tag "Org-Drill"
+  :group 'org-link)
+
+
+
+(defcustom org-drill-question-tag 
+  "drill"
+  "Tag which topics must possess in order to be identified as review topics
+by `org-drill'."
+  :group 'org-drill
+  :type 'string)
+
+
+
+(defcustom org-drill-maximum-items-per-session
+  30
+  "Each drill session will present at most this many topics for review.
+Nil means unlimited."
+  :group 'org-drill
+  :type '(choice integer (const nil)))
+
+
+
+(defcustom org-drill-maximum-duration
+  20
+  "Maximum duration of a drill session, in minutes.
+Nil means unlimited."
+  :group 'org-drill
+  :type '(choice integer (const nil)))
+
+
+
+(defface org-drill-hidden-cloze-face
+  '((t (:foreground "blue" :background "blue")))
+  "The face used to hide the contents of cloze phrases."
+  :group 'org-drill)
+
+
+(defvar org-drill-cloze-regexp
+  "[^][]\\(\\[[^][][^]]*\\]\\)")
+
+
+(defcustom org-drill-card-type-alist
+  '((nil . org-drill-present-simple-card)
+    ("simple" . org-drill-present-simple-card)
+    ("multisided" . org-drill-present-multi-sided-card)
+    ("spanish_verb" . org-drill-present-spanish-verb))
+  "Alist associating card types with presentation functions. Each entry in the
+alist takes the form (CARDTYPE . FUNCTION), where CARDTYPE is a string
+or nil, and FUNCTION is a function which takes no arguments and returns a
+boolean value."
+  :group 'org-drill
+  :type '(alist :key-type (choice string (const nil)) :value-type function))
+
+
+
+(defun shuffle-list (list)
+  "Randomly permute the elements of LIST (all permutations equally likely)."
+  ;; Adapted from 'shuffle-vector' in cookie1.el
+  (let ((i 0)
+	j
+	temp
+	(len (length list)))
+    (while (< i len)
+      (setq j (+ i (random (- len i))))
+      (setq temp (nth i list))
+      (setf (nth i list) (nth j list))
+      (setf (nth j list) temp)
+      (setq i (1+ i))))
+  list)
+    
+
+
+(defun org-drill-entry-due-p ()
+  (let ((item-time (org-get-scheduled-time (point))))
+    (and (or (assoc "LEARN_DATA" (org-entry-properties nil))
+             (member org-drill-question-tag (org-get-local-tags)))
+         (or (null item-time)
+             (not (minusp               ; scheduled for today/in
+                                        ; future
+                   (- (time-to-days (current-time))
+                      (time-to-days item-time))))))))
+
+
+
+(defun org-drill-reschedule ()
+  (let ((ch nil))
+    (while (not (memq ch '(?q ?0 ?1 ?2 ?3 ?4 ?5)))
+      (setq ch (read-char
+                (if (eq ch ??)
+                    "0-2 Means you have forgotten the item.
+3-5 Means you have remembered the item.
+ 
+0 - Completely forgot. 
+1 - Even after seeing the answer, it still took a bit to sink in. 
+2 - After seeing the answer, you remembered it. 
+3 - It took you awhile, but you finally remembered.
+4 - After a little bit of thought you remembered.
+5 - You remembered the item really easily.
+
+How well did you do? (0-5, ?=help, q=quit)"
+                  "How well did you do? (0-5, ?=help, q=quit)"))))
+    (cond
+     ((and (>= ch ?0) (<= ch ?5))
+      (save-excursion
+        (org-smart-reschedule (- ch 48)))
+      ch)
+     (t
+      nil))))
+
+
+(defun org-drill-hide-all-subheadings-except (heading-list)
+  "Returns a list containing the position of each immediate subheading of
+the current topic."
+  (let ((drill-entry-level (org-current-level))
+        (drill-sections nil)
+        (drill-heading nil))
+    (org-show-subtree)
+    (save-excursion
+      (org-map-entries
+       (lambda ()
+         (when (= (org-current-level) (1+ drill-entry-level))
+           (setq drill-heading (org-get-heading t))
+           (unless (member drill-heading heading-list)
+             (hide-subtree))
+           (push (point) drill-sections)))
+       "" 'tree))
+    (reverse drill-sections)))
+
+
+(defun org-drill-presentation-prompt (&rest fmt-and-args)
+  (let ((ch (read-char (if fmt-and-args
+                           (apply 'format
+                                  (first fmt-and-args)
+                                  (rest fmt-and-args))
+                         "Press any key to see the answer, 'e' to edit, 'q' to quit."))))
+    (case ch
+      (?q nil)
+      (?e 'edit)
+      (otherwise t))))
+
+
+;;; Presentation functions ====================================================
+
+;; Each of these is called with point on topic heading.  Each needs to show the
+;; topic in the form of a 'question' or with some information 'hidden', as
+;; appropriate for the card type. The user should then be prompted to press a
+;; key. The function should then reveal either the 'answer' or the entire
+;; topic, and should return t if the user chose to see the answer and rate their
+;; recall, nil if they chose to quit.
+
+(defun org-drill-present-simple-card ()
+  (org-drill-hide-all-subheadings-except nil)
+  (prog1 (org-drill-presentation-prompt)
+    (org-show-subtree)))
+
+
+(defun org-drill-present-multi-sided-card ()
+  (let ((drill-sections (org-drill-hide-all-subheadings-except nil)))
+    (when drill-sections
+      (save-excursion
+        (goto-char (nth (random (length drill-sections)) drill-sections))
+        (org-show-subtree)))
+    (prog1
+        (org-drill-presentation-prompt)
+      (org-show-subtree))))
+
+
+
+(defun org-drill-present-spanish-verb ()
+  (case (random 6)
+    (0
+     (org-drill-hide-all-subheadings-except '("Infinitive"))
+     (prog1
+         (org-drill-presentation-prompt
+          "Translate this Spanish verb, and conjugate it for the *present* tense.")
+       (org-drill-hide-all-subheadings-except '("English" "Present Tense"
+                                                "Notes"))))
+    (1
+     (org-drill-hide-all-subheadings-except '("English"))
+     (prog1
+         (org-drill-presentation-prompt
+          "For the *present* tense, conjugate the Spanish translation of this English verb.")
+       (org-drill-hide-all-subheadings-except '("Infinitive" "Present Tense"
+                                                "Notes"))))
+    (2
+     (org-drill-hide-all-subheadings-except '("Infinitive"))
+     (prog1
+         (org-drill-presentation-prompt
+          "Translate this Spanish verb, and conjugate it for the *past* tense.")
+       (org-drill-hide-all-subheadings-except '("English" "Past Tense"
+                                                "Notes"))))
+    (3
+     (org-drill-hide-all-subheadings-except '("English"))
+     (prog1
+         (org-drill-presentation-prompt
+          "For the *past* tense, conjugate the Spanish translation of this English verb.")
+       (org-drill-hide-all-subheadings-except '("Infinitive" "Past Tense"
+                                                "Notes"))))
+    (4
+     (org-drill-hide-all-subheadings-except '("Infinitive"))
+     (prog1
+         (org-drill-presentation-prompt
+          "Translate this Spanish verb, and conjugate it for the *future perfect* tense.")
+       (org-drill-hide-all-subheadings-except '("English" "Future Perfect Tense"
+                                                "Notes"))))
+    (5
+     (org-drill-hide-all-subheadings-except '("English"))
+     (prog1
+         (org-drill-presentation-prompt
+          "For the *future perfect* tense, conjugate the Spanish translation of this English verb.")
+       (org-drill-hide-all-subheadings-except '("Infinitive" "Future Perfect Tense"
+                                                "Notes"))))))
+    
+
+
+
+(defun org-drill-entry ()
+  "Present the current topic for interactive review, as in `org-drill'.
+Review will occur regardless of whether the topic is due for review or whether
+it meets the definition of a 'review topic' used by `org-drill'.
+
+See `org-drill' for more details."
+  (interactive)
+  (unless (org-at-heading-p)
+    (org-back-to-heading))
+  (let ((card-type (cdr (assoc "DRILL_CARD_TYPE" (org-entry-properties nil))))
+        (cont nil))
+    (save-restriction
+      (org-narrow-to-subtree) 
+      (org-show-subtree)
+      (org-cycle-hide-drawers 'overview)
+      
+      (let ((presentation-fn (cdr (assoc card-type org-drill-card-type-alist))))
+        (cond
+         (presentation-fn
+          (highlight-regexp org-drill-cloze-regexp
+                            'org-drill-hidden-cloze-face)
+          (setq cont (funcall presentation-fn))
+          (unhighlight-regexp org-drill-cloze-regexp))
+         (t
+          (error "Unknown card type: '%s'" card-type))))
+      
+      (cond
+       ((not cont)
+        (message "Quit")
+        nil)
+       ((eql cont 'edit)
+        'edit)
+       (t
+        (save-excursion
+          (org-drill-reschedule)))))))
+
+
+
+(defun org-drill (&optional scope)
+  "Begin an interactive 'drill session'. The user is asked to
+review a series of topics (headers). Each topic is initially
+presented as a 'question', often with part of the topic content
+hidden. The user attempts to recall the hidden information or
+answer the question, then presses a key to reveal the answer. The
+user then rates his or her recall or performance on that
+topic. This rating information is used to reschedule the topic
+for future review using the `org-learn' library.
+
+Org-drill proceeds by:
+
+- Finding all topics (headings) in SCOPE which have either been
+  used and rescheduled by org-learn before (i.e. the LEARN_DATA
+  property is set), or which have a tag that matches
+  `org-drill-question-tag'.
+
+- All matching topics which are either unscheduled, or are
+  scheduled for the current date or a date in the past, are
+  considered to be candidates for the drill session.
+
+- If `org-drill-maximum-items-per-session' is set, a random
+  subset of these topics is presented. Otherwise, all of the
+  eligible topics will be presented.
+
+SCOPE determines the scope in which to search for
+questions.  It is passed to `org-map-entries', and can be any of:
+
+nil     The current buffer, respecting the restriction if any.
+        This is the default.
+tree    The subtree started with the entry at point
+file    The current buffer, without restriction
+file-with-archives
+        The current buffer, and any archives associated with it
+agenda  All agenda files
+agenda-with-archives
+        All agenda files with any archive files associated with them
+ (file1 file2 ...)
+        If this is a list, all files in the list will be scanned."
+
+  (interactive)
+  (let ((entries nil)
+        (result nil)
+        (results nil)
+        (end-pos nil))
+    (block org-drill
+      (save-excursion
+        (org-map-entries
+         (lambda () (if (org-drill-entry-due-p)
+                   (push (point-marker) entries)))
+         "" scope)
+        (cond
+         ((null entries)
+          (message "I did not find any pending drill items."))
+         (t
+          (let ((start-time (float-time (current-time))))
+            (dolist (m (if (and org-drill-maximum-items-per-session
+                                (> (length entries)
+                                   org-drill-maximum-items-per-session))
+                           (subseq (shuffle-list entries) 0
+                                   org-drill-maximum-items-per-session)
+                         (shuffle-list entries)))
+              (save-restriction
+                (switch-to-buffer (marker-buffer m))
+                (goto-char (marker-position m))
+                (setq result (org-drill-entry))
+                (cond
+                 ((null result)
+                  (message "Quit")
+                  (return-from org-drill nil))
+                 ((eql result 'edit)
+                  (setq end-pos (point-marker))
+                  (return-from org-drill nil))
+                 ((and org-drill-maximum-duration
+                       (> (- (float-time (current-time)) start-time)
+                          (* org-drill-maximum-duration 60)))
+                  (message "This drill session has reached its maximum duration.")
+                  (return-from org-drill nil)))))
+            (message "Drill session finished!")
+            )))))
+    (when end-pos
+      (switch-to-buffer (marker-buffer end-pos))
+      (goto-char (marker-position end-pos))
+      (message "Edit topic."))))
+
+
+
+(provide 'org-drill)