diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-05 03:35:45 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-05 03:35:45 -0500 |
| commit | 49459e2d74ca3b7d11917e314cd13c6cd8f3f039 (patch) | |
| tree | c977c2db89369e2cc9ecfa6c2a7ef34fed378d01 | |
| parent | 88da84f2ce342f006e8cfb3ec302cc9e32af0591 (diff) | |
| download | org-drill-49459e2d74ca3b7d11917e314cd13c6cd8f3f039.tar.gz org-drill-49459e2d74ca3b7d11917e314cd13c6cd8f3f039.zip | |
fix: preserve default-input-method during key reads (upstream #52, #58)
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.
| -rw-r--r-- | org-drill.el | 22 | ||||
| -rw-r--r-- | tests/test-org-drill-read-key-sequence.el | 108 |
2 files changed, 125 insertions, 5 deletions
diff --git a/org-drill.el b/org-drill.el index 4d7479c..07de25d 100644 --- a/org-drill.el +++ b/org-drill.el @@ -1482,13 +1482,20 @@ of QUALITY." (reverse intervals))) (defun org-drill--read-key-sequence (prompt) - "Just like `read-key-sequence' but with input method turned off." + "Just like `read-key-sequence' but with input method turned off. + +Uses `deactivate-input-method' / `activate-input-method' rather than +`set-input-method', because `(set-input-method nil)' clears +`default-input-method' as a side effect when no input method is +currently active (upstream issues #52 and #58)." (let ((old-input-method current-input-method)) (unwind-protect (progn - (set-input-method nil) + (when current-input-method + (deactivate-input-method)) (read-key-sequence prompt)) - (set-input-method old-input-method)))) + (when old-input-method + (activate-input-method old-input-method))))) (defun org-drill-reschedule (session) "Returns quality rating (0-5), or nil if the user quit." @@ -1810,14 +1817,19 @@ Consider reformulating the item to make it easier to remember.\n" (org-drill-response-complete)) (defun org-drill-response-get-buffer-create () - "Create a response buffer." + "Create a response buffer. + +Propagates the caller's input method into the new buffer, but only when +one is actually active — otherwise `(set-input-method nil)' would clear +`default-input-method' as a side effect (upstream issues #52, #58)." (let ((local-current-input-method current-input-method)) (with-current-buffer (get-buffer-create "*Org-Drill*") (erase-buffer) (org-drill-response-mode) - (set-input-method local-current-input-method) + (when local-current-input-method + (activate-input-method local-current-input-method)) (current-buffer)))) (defun org-drill-presentation-prompt-in-buffer (session &optional prompt) diff --git a/tests/test-org-drill-read-key-sequence.el b/tests/test-org-drill-read-key-sequence.el new file mode 100644 index 0000000..672b034 --- /dev/null +++ b/tests/test-org-drill-read-key-sequence.el @@ -0,0 +1,108 @@ +;;; test-org-drill-read-key-sequence.el --- Tests for input-method-safe key reading -*- lexical-binding: t; -*- + +;;; Commentary: +;; Regression tests for `org-drill--read-key-sequence'. +;; +;; Upstream issues #52 (breadncup, 2023-11-24) and #58 (breadncup, +;; 2024-12-31) reported that running an org-drill session silently +;; nulled out the user's `default-input-method'. +;; +;; Root cause: the wrapper called `(set-input-method nil)' to disable +;; the input method during the key read. In Emacs, calling +;; `set-input-method' with nil while no input method is active has the +;; documented side effect of clearing `default-input-method' as well — +;; clobbering the user's persistent setting. The fix is to use the +;; primitive that's specifically scoped to current state: +;; `deactivate-input-method' on the way out, and `activate-input-method' +;; on the way back. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'org-drill) + +;;;; Helpers + +(defmacro with-stubbed-key-sequence (return-value &rest body) + "Run BODY with `read-key-sequence' replaced by a stub that returns RETURN-VALUE. +Avoids the interactive prompt during batch test runs." + (declare (indent 1)) + `(cl-letf (((symbol-function 'read-key-sequence) + (lambda (_prompt) ,return-value))) + ,@body)) + +;;;; Normal cases + +(ert-deftest test-org-drill-read-key-sequence-normal-no-input-method-set () + "When neither default- nor current-input-method is set, both stay nil." + (let ((default-input-method nil) + (current-input-method nil)) + (with-stubbed-key-sequence "x" + (org-drill--read-key-sequence "prompt: ")) + (should (null default-input-method)) + (should (null current-input-method)))) + +(ert-deftest test-org-drill-read-key-sequence-normal-returns-stubbed-value () + "The wrapper returns whatever `read-key-sequence' returns." + (let ((default-input-method nil) + (current-input-method nil)) + (should (equal "answer" + (with-stubbed-key-sequence "answer" + (org-drill--read-key-sequence "prompt: ")))))) + +;;;; Boundary / regression — issues #52 and #58 + +(ert-deftest test-org-drill-read-key-sequence-preserves-default-when-no-current () + "Issue #52/#58 regression: default-input-method must survive the call. + +The reporter (breadncup) had `default-input-method' set to a Korean +input method but no input method active at session start. Each +org-drill rating prompt would silently clear `default-input-method'." + (let ((default-input-method "TeX") + (current-input-method nil)) + (with-stubbed-key-sequence "x" + (org-drill--read-key-sequence "prompt: ")) + (should (equal "TeX" default-input-method)))) + +(ert-deftest test-org-drill-read-key-sequence-preserves-default-across-multiple-calls () + "Repeated calls during a session don't accumulate damage to default-input-method." + (let ((default-input-method "TeX") + (current-input-method nil)) + (with-stubbed-key-sequence "x" + (dotimes (_ 5) + (org-drill--read-key-sequence "prompt: "))) + (should (equal "TeX" default-input-method)))) + +;;;; Error cases + +(ert-deftest test-org-drill-read-key-sequence-error-restores-default-after-throw () + "If `read-key-sequence' errors, unwind-protect must still leave default-input-method intact." + (let ((default-input-method "TeX") + (current-input-method nil)) + (cl-letf (((symbol-function 'read-key-sequence) + (lambda (_prompt) (error "simulated read failure")))) + (should-error (org-drill--read-key-sequence "prompt: "))) + (should (equal "TeX" default-input-method)))) + +;;;; Same bug pattern in org-drill-response-get-buffer-create + +(ert-deftest test-org-drill-response-get-buffer-create-preserves-default-input-method () + "Issue #52/#58 regression in the response-buffer path. + +`org-drill-response-get-buffer-create' captures `current-input-method' +from the caller, then propagates it to the new *Org-Drill* buffer via +`set-input-method'. When the caller has no input method active, the +captured value is nil — and `(set-input-method nil)' clears the global +`default-input-method'. Same bug pattern, different function." + (let ((default-input-method "TeX") + (current-input-method nil)) + (unwind-protect + (org-drill-response-get-buffer-create) + (when (get-buffer "*Org-Drill*") + (kill-buffer "*Org-Drill*"))) + (should (equal "TeX" default-input-method)))) + +(provide 'test-org-drill-read-key-sequence) + +;;; test-org-drill-read-key-sequence.el ends here |
