diff --git a/cider-error.el b/cider-error.el new file mode 100644 index 0000000..7fef5d6 --- /dev/null +++ b/cider-error.el @@ -0,0 +1,58 @@ +;;; cider-error.el --- Route game-loop exceptions into cider's stacktrace buffer -*- lexical-binding: t -*- + +;;; Commentary: + +;; The game loop runs on its own thread, so anything it throws never reaches +;; the nREPL session that started it -- cider's stacktrace buffer stays empty +;; and you get an unreadable `println' in the REPL instead. +;; +;; `engine/report-exception!' stashes the throwable in `engine/last-error' and +;; prints a marker line. This hook watches REPL output for that marker, +;; swallows it, and asks the REPL to rethrow the stashed exception -- now on +;; the session's own thread, so cider renders it properly. +;; +;; Load with: (load "/path/to/project/cider-error.el") + +;;; Code: + +(require 'cider-client) + +(defconst clj-game-error-marker "SIAM-REPORT-EXCEPTION" + "Line `engine/report-exception!' prints to announce a stashed exception. +Deliberately does not contain EMACS-CIDER-REPORT-EXCEPTION, which a global +hook elsewhere matches for a different project's exception var.") + +;; `last-error' is a private var holding an atom, so both forms deref twice: +;; once through the var-quote to reach the atom, once to reach the throwable. +(defconst clj-game-error-deref "@@#'engine/last-error") +(defconst clj-game-error-throw + "(when-let [t @@#'engine/last-error] (throw t))") + +(defvar clj-game-error--last nil + "Printed form of the exception most recently handed to cider.") + +(defun clj-game-throw-last-error () + "Rethrow the stashed exception over nREPL so cider renders it. +Does nothing if that exception is already on show in *cider-error*." + (interactive) + (let* ((error-str (nrepl-dict-get + (cider-nrepl-sync-request:eval clj-game-error-deref) + "value")) + (same-error-p (and (get-buffer "*cider-error*") + (equal clj-game-error--last error-str)))) + (unless same-error-p + (setq clj-game-error--last error-str) + (cider-interactive-eval clj-game-error-throw)))) + +(defun clj-game-error-preoutput (output) + "Swallow the marker line in OUTPUT and rethrow; pass anything else through." + (if (string-match-p clj-game-error-marker output) + (progn (clj-game-throw-last-error) "") + output)) + +;; Named, so the dir-locals `eval' re-firing on every file open replaces this +;; hook entry rather than stacking duplicates of it. +(add-hook 'cider-repl-preoutput-hook #'clj-game-error-preoutput) + +(provide 'clj-game-error) +;;; cider-error.el ends here