siam-farmer/cider-error.el

59 lines
2.4 KiB
EmacsLisp

;;; 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