aboutsummaryrefslogtreecommitdiff
path: root/org-drill.el
Commit message (Collapse)AuthorAgeFilesLines
* refactor: add org-drill-card-state struct + ADRCraig Jennings2026-05-271-0/+22
| | | | | | First step of #147. I added a cl-defstruct bundling the recall fields the schedulers and the item-data round-trip shuttle around (last-interval, repetitions, ease, failures, meanq, total-repeats), with an ADR comment recording why: the same fields were passed as positional lists in three different orderings, and three call sites re-ordered between them by hand. The struct is inert here. The item-data round-trip and the schedulers adopt it in the following commits.
* refactor: sharpen opaque local variable names across org-drill.elCraig Jennings2026-05-271-39/+39
| | | | | | I renamed several cryptic locals to say what they hold: idx to index-var in the pop-random gensym, val to raw-value in the five DRILL_* property getters, q to quality in hypothetical-next-review-dates, m to marker in free-markers, the a/b sort and filter lambdas in order-overdue-entries to entry/other, and dat to card-def in the empty-card-type check. These are pure renames with no behavior change. Byte-compile and the full unit suite stay green.
* refactor: name the progress-meter chars in org-drill-progress-messageCraig Jennings2026-05-271-4/+4
| | | | sym1/sym2 gave no hint of their role. current-meter-char toggles per meter wrap and alternate-meter-char is its inverse, which is what the two make-string calls actually consume.
* feat: undo last rating, customizable keys, and configurable text limitCraig Jennings2026-05-271-49/+142
| | | | | | | | | | | | | | A batch of self-contained user-facing improvements, squashed from the feat/org-drill-solo-features branch. I added an undo for the last rating (issue #2 follow-up). The rating prompt now takes an undo key (org-drill--undo-key, default u): it restores the previous card's scheduling snapshot, drops the recorded quality, and re-queues that card, then returns to the current prompt. Each rating snapshots the scheduling properties and SCHEDULED line onto a per-session stack capped at org-drill-undo-limit (default 3). org-drill-reschedule loops on the rating read so undo doesn't rate the current card. I made the five session-control keys (quit, edit, help, skip, tags) defcustoms so they can be rebound from customize-group (issue #35), keeping their defaults. The 0-5 rating keys stay as-is, since they're tied to the quality scale rather than being variables. I lifted the hardcoded 100-line entry-text limit in org-drill-get-entry-text into the org-drill-entry-text-max-lines defcustom, defaulting to 100. I also deleted a commented-out old org-entry-empty-p that the real definition had already replaced. Existing tests stay green and each change added its own, including snapshot/restore and prompt-loop tests for undo.
* refactor: dedupe presenters, group defcustoms, and fill in docstringsCraig Jennings2026-05-261-232/+303
| | | | | | | | | | | | | | A cleanup pass over org-drill internals, squashed from the refactor/wave3-cleanup branch. No behavior change. Each step kept the existing tests green and added its own. I shared two duplicated helpers across the language card getters: org-drill--read-property-string and org-drill--face-from-alist. I factored the cloze body-scan out of the two multicloze presenters into org-drill--cloze-body-bounds, org-drill--count-cloze-matches, and org-drill--hide-cloze-by-index, so each presenter just picks which indices to hide. I pulled the presenter resolution and the four-way result classification out of org-drill-entry-f into org-drill--resolve-presenter and org-drill--classify-presentation-result, untangling the pivot of every drill iteration. I split the 37 defcustoms (and the three cloze faces) into four customize sub-groups (display, algorithm, session, leech) so customize-group org-drill is navigable. There's no leitner group because the Leitner settings are defvars. I documented the 22 defuns that had no docstring, rewrote the corrupted org-drill-presentation-prompt-in-mini-buffer docstring, and switched eleven docstrings to the imperative "Return" (issue #2).
* fix: scope cloze fontification to drill buffers via org-drill-modeCraig Jennings2026-05-261-10/+73
| | | | | | | | org-drill-add-cloze-fontification ran on org-font-lock-set-keywords-hook, which fires in every org buffer, and pushed the cloze rule into org's global org-font-lock-extra-keywords. The cloze regexp is built from the [ and ] delimiters, so an org priority cookie like [#A] matched the cloze pattern and got fontified as a cloze in every org buffer, colliding with org's headline fontification and stripping the heading's org-level-N face. I replaced the global install with org-drill-mode, a buffer-local minor mode that adds the cloze keywords only to its own buffer via font-lock-add-keywords. org-drill-auto-enable-mode (default on) turns the mode on from org-mode-hook in buffers that hold drill cards, so existing drill files keep their cloze highlighting while plain org buffers stay clean. Highlighting still respects org-drill-use-visible-cloze-face-p. The cloze regexp itself is unchanged, so the single-line cloze constraint from #38 is preserved.
* chore: coverage, autoload fix, and internal cleanup for org-drillCraig Jennings2026-05-261-37/+68
| | | | | | | | | | | | | | | | A batch of test-coverage and hardening work, squashed from the test-work branch. Tests: deduplicated a colliding leitner-capture test name so make test-name loads again. Added SM2 assert-failure cases, the six basic multicloze variant delegations, the three English-side spanish-verb branches, and org-drill-current-scope branch coverage. Fix: the entry-point commands (org-drill itself, cram-tree, tree, directory, resume, relearn-item, strip-all-data, merge-buffers) carried no autoload cookies, so M-x failed from a fresh install until something pulled the file in. They're autoloaded now. Perf: org-drill-shuffle was quadratic because it indexed a list with elt on every swap. It runs a linear Fisher-Yates pass over a vector now, and it checks its argument is a list. Feat: added org-drill-version, a constant plus an interactive command, so a bug reporter doesn't have to open the file header. Refactor: extracted org-drill--format-tense-mood, shared by the two verb-conjugation presenters that each carried a copy. Docs: explained the SM8 magic numbers in the simple8 helpers as empirical fits rather than tunable knobs.
* build: migrate from Cask to EaskCraig Jennings2026-05-101-1/+1
| | | | | | | | | | Cask's upstream has slowed. Eask is the actively maintained successor. Eask's `package-file` directive doesn't auto-install the deps from the .el header's Package-Requires, so the Eask file mirrors emacs/seq/org/persist explicitly. `eask install-deps` also doesn't pull transitive deps, so dash, m-buffer, and shut-up needed their own `depends-on` lines for undercover and elisp-lint to activate. The Makefile swaps are mechanical: $(CASK) → $(EASK), `cask install` → `eask install-deps --dev`, `cask build` → `eask compile`. The URL in org-drill.el's header pointed at the abandoned upstream's GitLab issues page. Eask cross-validates that against website-url, so I updated it to the GitHub mirror — where users file issues now.
* refactor: split org-drill-entry-status classifier from its predicatesCraig Jennings2026-05-051-38/+34
| | | | | | | | | | | | | | | | | Two extractions out of the 58-line cond: - org-drill--entry-empty-and-not-empty-friendly-p: encapsulates the ugly nested predicate that means 'body is empty AND the card type doesn't opt in to empty bodies'. Used to be 5 inlined lines including a let* and an alist lookup. - org-drill--classify-status: takes the precomputed DUE and LAST-INT and walks the cond. The decision tree is now a flat sequence of one-line clauses. org-drill-entry-status itself drops to 8 lines and reads as 'compute the inputs, classify, return triple'. The :failed branch also uses org-drill--quality-failed-p instead of inlining the threshold check.
* refactor: split org-drill-merge-buffers into named phasesCraig Jennings2026-05-051-64/+74
| | | | | | | | | | | | | | | | | | | | | | merge-buffers was 89 lines of dense cross-buffer marker work mixing hash-table population, source-buffer iteration with embedded property reads, scheduling-data writes, and cleanup of unmatched DEST entries. Extracted four helpers: - org-drill--build-dest-id-table: scan DEST and populate the id→ marker table. - org-drill--copy-scheduling-to-marker: read the current entry's scheduling state and write it at MARKER (skipping never-rated items via total-repeats=0 guard). - org-drill--migrate-from-source: walk SRC, dispatch to the copy helper or org-drill-copy-entry-to-other-buffer for new items. - org-drill--strip-unmatched-dest-entries: clean up DEST entries that have no SRC match. merge-buffers itself drops from 89 lines to 19 and reads as a sequence of named phases.
* refactor: split org-drill main entry into named phasesCraig Jennings2026-05-051-99/+103
| | | | | | | | | | | | | | | | | | | | | org-drill was 137 lines mixing five distinct concerns: an org-version warning (now dead under the org>=9.6 floor), session reset, entry collection, drill execution, and post-session messaging. Extracted four helpers, each with a single responsibility: - org-drill--prepare-fresh-session: zero out queues + counters - org-drill--collect-entries: scan + sort overdue - org-drill--queues-empty-p: predicate for the no-pending branch - org-drill--show-end-message: dispatch resume-hint vs final-report Plus org-drill--show-resume-hint for the keystr-aware suspended message. Removed the dead org<7.9.3f warning block (the org>=9.6 floor makes it unreachable). org-drill itself drops from 137 lines to 36 and the cl-block wrapper goes away — the cl-return-from inside org-drill-entries returns through the normal control flow now.
* refactor: split org-drill-final-report into format helpersCraig Jennings2026-05-051-73/+71
| | | | | | | | | | | | | | | | | | | | org-drill-final-report was 95 lines, dominated by two big format calls (the main summary and the low-pass-rate WARNING) plus an inlined queue-tag pattern that propertized 5 different counts in the same shape. Extracted three helpers: - org-drill--queue-tag: builds a propertized 'N label' string for one queue (failed / overdue / new / young / old). Replaces 5 inlined propertize calls. - org-drill--build-final-report-summary: takes the session, and returns the formatted main summary string. - org-drill--build-low-pass-warning: takes the session and pass- percent, returns the formatted warning string. org-drill-final-report becomes a 12-line orchestrator that wires the helpers together with the wait-and-read loop.
* refactor: flatten nesting in org-drill-entriesCraig Jennings2026-05-051-35/+50
| | | | | | | | | | | | | | | | | | | The main loop body reached 7 levels of indentation in the success path: while > destructuring-bind > save-excursion > cond > t-clause > let > cond > inner-cond. Extracted two helpers: - org-drill--pick-next-marker: chooses between resuming the current-item slot and popping a fresh marker, returning a (marker . next-resuming-p) cons so the caller updates resuming-p in lock-step. - org-drill--route-rating-result: routes the rating result into the session's again/done queues and returns a symbol (quit/edit/ skip/next) telling the caller whether to break the loop. org-drill-entries shrinks from 54 lines to 19, and the deepest nesting drops from 7 levels to 4.
* refactor: extract org-drill--read-rating-key shared by reschedule and ↵Craig Jennings2026-05-051-66/+48
| | | | | | | | | | | | | | | | | | | | | | leitner-rebox The two interactive rating loops (reschedule and leitner-rebox) shared roughly 60 lines of identical code: the same key-prompt string, the same memq-based exit-on-rating loop, the same arrow/ scroll/wheel cond, the same tags-key dispatch, and the same help-key toggle. Only two things differ — the rating-explanation help text and the per-rating action that follows — and the comment in leitner-rebox literally said "All this is shared with drill- reschedule. And what does it do?" Extracted org-drill--read-rating-key with two args: typed-answer (for the typed-answer flow's 'Your answer: ...' line) and rating-help-block (the multi-line ratings explanation specific to the scheduler). Both call sites collapse from ~50 lines of inlined loop to a single call. reschedule and leitner-rebox now consist of just the rating- specific actions plus this read-key call.
* refactor: introduce org-drill-with-card-display macroCraig Jennings2026-05-051-60/+60
| | | | | | | | | | | | | | | | | | | Five presenters opened with the same three-deep wrap: (org-drill-with-hidden-comments (org-drill-with-hidden-cloze-hints (org-drill-with-hidden-cloze-text ...body...))) Combined into org-drill-with-card-display. Five sites (present-simple-card, present-simple-card-with-typed-answer, present-two-sided-card, present-multi-sided-card, present-spanish-verb) lose 2-3 lines of nesting each. Multicloze-hide-n / hide-nth use a different envelope (only two of the three wraps; they hide specific clozes by index, not all of them) so they keep their explicit nesting. Same for present-card-using-text, which substitutes with-replaced-entry-text for with-hidden-cloze-text.
* refactor: extract org-drill--quality-failed-p predicateCraig Jennings2026-05-051-5/+11
| | | | | | | | | | | | The check (<= quality org-drill-failure-quality) appeared in five places: SM2/SM5/Simple8 schedulers, smart-reschedule, and the main org-drill-entries loop body. Each call site does different things on failure (reset interval, push to again-entries, etc.) so only the predicate was duplicated. Extracted as org-drill--quality-failed-p with a docstring naming the threshold and the role. Five inlined comparisons collapse to five named predicate calls.
* refactor: extract LEECH-warning preamble helperCraig Jennings2026-05-051-24/+19
| | | | | | | | | | | The seven-line propertize+concat block that prepends a red leech warning to the prompt was inlined in three prompt builders: presentation-prompt-in-mini-buffer, presentation-prompt-in-buffer, and presentation-prompt-for-string. Extracted org-drill--maybe-prepend-leech-warning as a single helper the three call. 21 lines duplicated → one definition + three one-line call sites.
* refactor: remove smaller commented-out alternative branchesCraig Jennings2026-05-051-18/+0
| | | | | | | | | | | | | | | | Four dead-code blocks deleted, all of them commented-out alternative implementations next to the live versions: - 5-line commented (and (>= quality 4) ...) cond branch in the SM5 scheduler - 6-line commented (loop do (re-search-forward ...)) alternative in present-multicloze-hide-n - 4-line commented unless-error guards at the top of entry-f - 3-line commented (:tomorrow ...) case branch in map-entry-function Prose commentary kept (license, package overview, recent-fix explanations). Pure deletion, no behavior change.
* refactor: convert spanish-verb 6-way cl-case to alist dispatchCraig Jennings2026-05-051-47/+19
| | | | | | | | | | | | | org-drill-present-spanish-verb had a 6-branch cl-case where each branch differed only in two values: which subheading to reveal (Infinitive or English) and which prompt string to show. Inlined into a 50-line block of nearly-identical setq calls. Extracted the (reveal . prompt) pairs into a defconst alist and reduced the dispatcher to a random-pick + apply-pair pattern. Function drops from 51 lines to 14. Existing branch tests still pass — they mock cl-random to a specific index, and the alist's order matches the previous case order.
* refactor: extract org-drill--quality-percent helper for final-reportCraig Jennings2026-05-051-12/+11
| | | | | | | | | | | | The quality-percentage formula (round (* 100 (cl-count Q qualities)) (max 1 (length qualities))) appeared six times in org-drill-final-report (once per recall quality 0..5), each time inlined verbatim. Extracted to a single helper. Six call sites collapse from three lines each to one. Behavior identical (the helper documents the (max 1 ...) divisor as an empty-list guard). Final-report drops from 95 lines to ~80.
* refactor: drop Org <9.6 fallback in time-to-inactive-org-timestampCraig Jennings2026-05-051-6/+1
| | | | | | | | | | | The function had an Org-version branch with a legacy <9.6 path that used (substring (cdr org-time-stamp-formats) 1 -1). Modern Org's format-strings dropped the angle brackets that the substring slice assumed, so the legacy path was both dead-code (unreachable under the org>=9.6 dep floor we just declared) and silently buggy if it ever did run. Function is now a one-liner around the modern primitive.
* refactor: drop Org <9.6 compat shimsCraig Jennings2026-05-051-23/+0
| | | | | | | | | | | | | Two dead branches removed: - (when (version< org-version "9.2") ...) advice on org-get-tags, plus the org-drill-get-tags-advice helper that backed it. Org 9.2 shipped the new arity in 2018 — well below our org>=9.6 floor. - (when (= 8 (car ...)) ...) Org 8.x defalias shim that wrapped org-latex-preview around org-preview-latex-fragment. Org 8 hasn't been a target for years and is below the org>=9.6 floor. Both are now genuinely dead after the dep bump in 75b1601.
* refactor: delete commented-out function bodiesCraig Jennings2026-05-051-58/+0
| | | | | | | | | | | | | | Three large dead-code blocks removed: - 16-line commented-out org-drill-entry-due-p (replaced by current implementation that takes a session arg) - 17-line commented-out org-drill-hide-all-subheadings-except (the body was a placeholder docstring + commented body; the real function lives elsewhere now) - 25-line block of commented-out alternative org-drill-add-cloze-fontification + add-hook XXX commentary Pure deletion, no behavior change. Version control has the history.
* build: bump Org dep to 9.6 to match unguarded org-fold-* callsCraig Jennings2026-05-051-1/+1
| | | | | | | | | | | | | | | | | org-drill calls org-fold-show-entry and org-fold-show-subtree from seven sites without fboundp guards. Both functions arrived in Org 9.6. But the package declared org 9.3 (Package-Requires) / org 9.2 (Cask), so users on older Org would silently void-function at runtime instead of getting a clear install-time mismatch error. Bumped both declarations to org 9.6. Wrapping each of the seven call sites with fboundp would be the alternative, but Org 9.6 was released October 2022 — three-and-a-half years ago — and we already have a follow-up TODO to drop the legacy time-to-inactive fallback that this version bump unblocks. Two tests verify the declared dep and that the org-fold APIs are actually bound on the running Org version.
* fix: keep collection scan alive when one entry errors (upstream #53)Craig Jennings2026-05-051-24/+34
| | | | | | | | | | | | | | | | | | | | | | User reported that running org-drill on a buffer with a new (no-ID) entry threw 'Wrong Type Argument: hash-table-p, nil' and stopped the scan — every subsequent entry was silently skipped, so the user had to re-run org-drill once per item (10 items meant 10 invocations). The exact source of the hash-table error is environment-dependent (Emacs version, Org version, lazy org-id-locations init, Doom overrides), so this fix targets the user-visible failure mode instead of the underlying triggering condition. Wrapped the per-entry body of org-drill-map-entry-function in condition-case. An error on one entry now logs a 'skipping' message and the scan continues to the next entry. The session collects all the well-formed items, and the user can re-run drill once total to process them — no more once-per-item. Two regression tests: one verifies the resilience behavior directly (fail entry 1, scan continues to entry 2), the other documents the ID-creation-with-uninitialized-locations scenario as a smoke check.
* fix: keep cloze regex within a single line (upstream #38)Craig Jennings2026-05-051-2/+9
| | | | | | | | | | | | The inner match was [[:cntrl:][:graph:][:space:]]+?, which silently includes newline. A stray [ could match all the way to a ] several lines later, covering org headings in between with the visible-cloze face. Reporter saw lines 4 and 5 of test.org lose their org-level-N face and use default instead. Switched the inner class to [^\n]+?. Clozes now stay within a single line, which matches the design intent and stops the face bleed. Three new tests cover the regression.
* fix: skip LaTeX preview on TTY frames (upstream #44)Craig Jennings2026-05-051-10/+18
| | | | | | | | | | | | | | | | | Issue #44 (2021): running org-drill in a TTY emacsclient (the reporter mentioned tmux) raised "Window system frame should be used" because LaTeX preview helpers (org-latex-preview, org--latex-preview-region) require a window system and weren't guarded. Wrapped both call sites with (when (display-graphic-p) ...). - org-drill--show-latex-fragments: now a silent no-op on TTY - present-default-answer's clear-and-preview block: same guard LaTeX previews are inherently graphical. The right behavior on TTY is to skip the preview rather than crash the session — TTY users still see the underlying source text just fine.
* fix: clear stale end-pos on resume so final-report fires (upstream #33)Craig Jennings2026-05-051-0/+7
| | | | | | | | | | | | | | | | When a user interrupted a drill session to edit or capture, the session's end-pos slot got set to a marker (or :quit). The end-of- org-drill cond branched on end-pos: if set, show resume message and skip org-drill-final-report. That worked for the first interruption. But on org-drill-resume, the session was reused with end-pos still carrying the prior marker. Even when the resumed session completed normally, the same cond branch fired again — silently skipping final-report. Clear end-pos at the top of org-drill when resume-p is non-nil, per Markus's proposed patch on the upstream issue. The resumed session can now reach the final-report branch.
* fix: restore display state in the buffer setup ran inCraig Jennings2026-05-051-20/+40
| | | | | | | | | | | | | | | | | | org-drill--setup-display saved buffer-local state (mode-line, variable-pitch-mode) into global defvars and called setq-local on the current buffer. org-drill--restore-display read those globals and ran setq-local against whatever buffer happened to be current at restore time. If the user switched buffers mid-session, the restore wrote to the wrong buffer — leaving the original drill buffer's mode-line still hidden and trampling the destination buffer's mode-line with whatever was saved from elsewhere. Captured the buffer at setup in org-drill--saved-display-buffer. Restore now wraps mode-line and variable-pitch restoration in with-current-buffer against that saved buffer. Text-scale stays global (the underlying face attribute is process-wide).
* fix: recover from corrupted persist file at package load (upstream #45)Craig Jennings2026-05-051-3/+14
| | | | | | | | | | | | | Issue #45 (2021): persist-load raised End of file during parsing at persist.el:413 in some configurations, likely from a corrupted persist data file. Pre-fix, this propagated up through the top-level (persist-defvar org-drill-sm5-optimal-factor-matrix ...) form at file-load time and broke the entire package's load. Wrapped the persist-defvar form in condition-case. On failure, the matrix falls back to a fresh nil binding via plain defvar, and a message tells the user what happened. org-drill continues to load normally.
* fix: guard org-drill-again and org-drill-resume against nil last-sessionCraig Jennings2026-05-051-0/+4
| | | | | | | | | | | | Both functions bound session to org-drill-last-session and immediately called setf / org-drill-entries-pending-p on it without checking for nil. First-time invocation (or after Emacs restart with no active session) threw an obscure eieio-oset / nil-slot type error instead of a clear message. Added (unless session (user-error ...)) at the top of each function. A user running M-x org-drill-resume cold now sees a sensible message telling them to run org-drill first.
* fix: hide-drawers ignores drawers with no :END:Craig Jennings2026-05-051-3/+10
| | | | | | | | | | | | | drawer-end was captured as (save-excursion (re-search-forward ':END:' end t) (point)) which always returns a number — (point) is always defined. The subsequent (when drawer-end ...) guard was dead, so a malformed drawer (typo in :END:, mid-edit truncation) ended up with a junk overlay covering whatever range point happened to land in. Captured the search result itself and gate on it. Malformed drawers are now skipped silently; well-formed drawers still get their normal overlay.
* fix: default DRILL_LEITNER_BOX to 0 in leitner-reboxCraig Jennings2026-05-051-1/+6
| | | | | | | | | | | When the property is absent, org-entry-get returns nil and string-to-number errors with wrong-type-argument. Reachable when a user removes the property mid-session, or when a Leitner-tagged entry is rebox'd before its DRILL_LEITNER_BOX has been set. Wrapped the org-entry-get with (or ... "0"). Box 0 makes the rating semantics still sensible: a downgrade stays at 0, a promotion goes to 1.
* fix: drop dead translate_number entry from card-type alist (upstream #43)Craig Jennings2026-05-051-2/+1
| | | | | | | | | | | | | | | | | | | The card-type alist mapped translate_number to a function that no longer exists in the file. Cards with DRILL_CARD_TYPE: translate_number crashed with void-function during drill instead of being skipped. Reporter (issue #43, 2021) said they had old decks using the documented translate_number type and were getting the crash on restore. The function was apparently removed at some point without clearing the alist entry. Removed the alist entry so entry-f's no-presentation-fn branch fires and returns skip after messaging the user. Legacy decks now degrade gracefully instead of crashing the session. Tests in tests/test-org-drill-translate-number-regression.el lock the behavior in (entry-f returns skip on translate_number, alist no longer carries the entry).
* fix: guard zero-divisor in org-drill-final-report overdue percentageCraig Jennings2026-05-051-2/+7
| | | | | | | | | | | | The warning branch divided 100*overdue by (dormant+due) without guarding the denominator. When both counts are zero — degenerate scopes (cram with no items collected, pure-failure session on empty queues) — the call hit arith-error before the warning even rendered. Wrapped the divisor with (max 1 ...). In the zero case the percentage reads as 0% rather than crashing the session wrap-up. Resolves a long-standing pre-existing TODO entry.
* fix: remove stray [debug] message in org-drill-entriesCraig Jennings2026-05-051-1/+0
| | | | | | | Every drilled card was logging "[debug] org-drill: at marker position N" to *Messages* and flashing it in the minibuffer. Pure noise — the print statement was clearly a leftover from diagnostic work that never got cleaned up. Delete it.
* fix: guard org-drill-smart-reschedule cond against nil days-aheadCraig Jennings2026-05-051-5/+10
| | | | | | | | | | | | | | | | The function takes `days-ahead' as &optional, but the schedule cond called `(= 0 days-ahead)' and `(cl-minusp days-ahead)' before any type-guard, so passing nil crashed with a wrong-type-argument error. Today's two callers (the rating-confirmation flow and the org-drill-relearn-item helper) always pass a number, so this was latent — but a third caller relying on the documented &optional shape would hit it immediately. Switched the cond to require numberp before the value comparisons, and the default branch now falls back to the algorithm-computed next-interval when days-ahead is nil. That matches the intent implied by the optional signature and the docstring.
* test: add entry-status, days-since-creation, and overdue ordering coverageCraig Jennings2026-05-051-2/+7
| | | | | | | | | | | | | | | | | | | | | | Plus a docs fix to org-drill-order-overdue-entries' header comment. 16 ERT tests covering: - org-drill-entry-status: non-drill nil, empty entry nil, virgin :new, future :future, low-quality :failed, due+short-interval :young, due+long-interval :old, very-overdue :overdue, skipped-leech :unscheduled, three-element return shape - org-drill-entry-days-since-creation: with DATE_ADDED, missing without flag (nil), missing with use-last-interval-p flag (overdue+interval) - org-drill-order-overdue-entries: empty stays empty, non-lapsed sorted by DUE desc, lapsed split (by DUE crossing threshold, not AGE) appearing after sorted by AGE desc Fixed misleading header comment at line 2888 — it claimed the lapse split was by AGE, but the code uses DUE (cl-second). This matches the semantic gate in org-drill--entry-lapsed-p, so the code was right and the comment was stale. Updated the comment to state the actual three-step sort.
* fix: don't create zero-width overlay for hint-less clozesCraig Jennings2026-05-051-2/+8
| | | | | | | | | | | | | | | | org-drill-hide-cloze-hints checked (null (match-beginning 2)) to detect "no hint present," but the cloze regex's hint group is an empty-allowed alternation — the group always participates in the match, so match-beginning is always a position, never nil. For a card like "[Paris]" (no hint), the function fell through to org-drill-hide-region with start = end and made a zero-width overlay. Cosmetically harmless but accumulates one stray overlay per hint-less cloze. On a buffer with many such cards the tracking cost is real. Switched the guard to (= (match-beginning 2) (match-end 2)) — empty match. Found while writing tests; locked in by tests/test-org-drill-hide-show.el's test-org-drill-hide-cloze-hints-no-hint-no-overlay.
* fix: include child subtree in entry-empty-p search bound (upstream #13)Craig Jennings2026-05-051-2/+8
| | | | | | | | | | | | kqr (2019-07-22) reported that drill entries whose answer lives inside a child sub-heading were silently skipped. Their example: a question in the heading text and the answer under `** The Answer`. The function returned t (empty) for such entries, so they never got presented during drill sessions. The cause is `(outline-next-heading)` in `org-drill-entry-empty-p`. That primitive lands on the first heading at any level, including children. So the search range was metadata-end up to the child's heading line, which excluded the child's body. Bodies that lived in child sub-headings never got searched. I switched the bound to `(org-end-of-subtree t t)`, which covers the whole subtree of the current heading and degrades gracefully at the last heading in the buffer. The reporter suggested `outline-forward-same-level`, but that primitive errors at the last sibling, which would be its own regression. `org-end-of-subtree` is the canonical Emacs idiom for this kind of bound and handles end-of-buffer correctly. I added `tests/test-org-drill-entry-empty-p.el` with 6 ERT tests across Normal, Boundary (kqr's exact fixture), and edge categories. The two regression tests fail at HEAD before the fix and pass after. One semantic note worth flagging: any subtree content now counts as non-empty, including bare child headings with no body of their own. The bug report is silent on that case and I expect it to be rare in practice. If anyone reports the new behavior as a regression, the fix would be to filter heading lines out of the graphical-character search.
* fix: preserve default-input-method during key reads (upstream #52, #58)Craig Jennings2026-05-051-5/+17
| | | | | | | | | | Two reports from breadncup (issue #52 in 2023, issue #58 in 2024) said that running an org-drill session silently nulled out their `default-input-method`. The reproduction is exact: every rating prompt clears the user's persistent setting. The cause is `(set-input-method nil)` in `org-drill--read-key-sequence`. When `current-input-method` is nil, calling `set-input-method` with nil clears `default-input-method` as a documented side effect. The unwind-protect on the way back has the symmetric problem, since it passes the captured nil. The fix is to use the primitives that are scoped to current state. `deactivate-input-method` and `activate-input-method` don't touch `default-input-method`, and I wrap each call in a guard so the function is a no-op when no input method is active. The same pattern lives in `org-drill-response-get-buffer-create`, which propagates the caller's input method into the response buffer. When the caller has no input method active, the captured value is nil and `(set-input-method nil)` runs in the new buffer, clearing `default-input-method` again. I applied the same guard there. I added `tests/test-org-drill-read-key-sequence.el` with 6 ERT tests across Normal, Boundary (the bug case), and Error categories. The four regression tests fail at HEAD before the fix and pass after.
* fix: increment totaln on Simple8 failure pathCraig Jennings2026-04-291-0/+1
| | | | | | | | The Simple8 failure branch was missing (cl-incf totaln) while SM2 and SM5 both increment total-repeats on failure. After this change, DRILL_TOTAL_REPEATS counts every review attempt regardless of which scheduling algorithm produced it, including failures. Going-forward only. Historical totaln values for Simple8 failures stay under-counted by one. Correct counting starts with the next failed review. Paired with the test commit 5c68f1e, which captured the new expected behavior first. Full suite at 214 of 214.
* fix: use correct calling convention for `display-buffer`p-snow2026-04-291-1/+1
| | | | (cherry picked from commit eacb6d0c018839d8207ee80e02b46b314278ac3f)
* Update org-drill-time-to-inactive-org-timestamp to Org 9.6 formatJoseph Turner2026-04-291-1/+4
| | | | (cherry picked from commit 76d45fb0ea6e216b2cb173bdcf73ef284d350ff8)
* fix: add error handling for malformed timestamps in ↵Craig Jennings2025-11-131-1/+3
| | | | | | | | | | | | | | | | org-drill-entry-days-since-creation Wrapped org-time-stamp-to-now call in condition-case to gracefully handle malformed DATE_ADDED property values. Now returns nil instead of crashing when encountering invalid timestamp formats. Changes: - Added condition-case around org-time-stamp-to-now (lines 2896-2898) - Returns nil on error, allowing the function to fall through to other branches or return nil gracefully This prevents unhandled errors in long-running sessions when drill entries have corrupted or manually-edited timestamp values.
* fix: correct property names in org-drill-merge-buffersCraig Jennings2025-11-131-4/+4
| | | | | | | | | | Changed incorrect property names to match standard naming convention: - LAST_QUALITY → DRILL_LAST_QUALITY (lines 3394-3395) - LAST_REVIEWED → DRILL_LAST_REVIEWED (lines 3397-3398) This ensures consistency with the rest of the codebase where all drill properties use the DRILL_ prefix. The old names would create properties that don't match the standard schema and wouldn't be read correctly.
* refactor: extract hardcoded lapse threshold into customizable variableCraig Jennings2025-11-131-6/+18
| | | | | | | | | | | | | Created org-drill-lapse-threshold-days defcustom (default 90) to replace hardcoded values scattered throughout the code. This improves maintainability and allows users to customize when entries are considered lapsed. Changes: - Added defcustom org-drill-lapse-threshold-days (line 660-669) - Updated org-drill-order-overdue-entries to use variable (line 2867) - Simplified org-drill--entry-lapsed-p to use variable (line 2884-2886) - Added safe-local-variable declaration (line 687) - Updated docstring references to use variable name
* fix: prevent window corruption in org-drill-merge-buffersCraig Jennings2025-11-131-16/+16
| | | | | | | | | | | | | | | | Replaced switch-to-buffer with with-current-buffer to avoid changing visible buffers during merge operation. This prevents window state corruption and allows the function to work correctly in batch mode. Changed line 3374-3375 from: (save-excursion (switch-to-buffer (marker-buffer marker)) ...) To: (with-current-buffer (marker-buffer marker) (save-excursion ...))
* refactor: remove obsolete commented code for failure countCraig Jennings2025-11-131-3/+0
| | | | | | | | Removed commented-out code that redundantly set the failures variable. The failures count is already obtained from org-drill-entry-failure-count on line 1538, making the commented code (lines 1548-1550) unnecessary. This cleans up maintainability issues and removes confusing dead code.
* fix: improve error handling for empty property blocks in cloze processingCraig Jennings2025-11-131-4/+18
| | | | | | | | | | | Previously used (or (cdr (org-get-property-block)) (point)) which could return invalid position if no property block exists. Now properly positions after heading and metadata using org-end-of-meta-data when property block is missing. Affects: - org-drill-present-multicloze-hide-n (line 2267) - org-drill-present-multicloze-hide-nth (line 2345)