Skip to content

Commit 186c34f

Browse files
committed
Add cider-repl-history-doctor command
Per the user's suggestion in #3915: when REPL history accumulates typo-ed entries that break the history browser, the user wants a way to see which entries are problematic and clean them up. `M-x cider-repl-history-doctor' walks `cider-repl-input-history' for entries whose parens don't balance under Clojure syntax (the kind of corruption that trips the browser via `check-parens'-style hooks), shows each candidate in a side buffer, and prompts y/n/q. When done, it rewrites the history file if `cider-repl-history-file' is set so the cleanup survives a restart.
1 parent f3306d5 commit 186c34f

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Introduce `cider-jack-in-tools` and `cider-register-jack-in-tool` so third-party packages can register new project tools for `cider-jack-in` and `cider-jack-in-universal`.
1010
- Cache the result of `cider--running-nrepl-paths` (used by `cider-locate-running-nrepl-ports`) for `cider-running-nrepl-paths-cache-ttl` seconds (default 5). Repeated `cider-connect` completions no longer re-spawn a fresh round of `ps`/`lsof` subprocesses each time. `cider-clear-running-nrepl-paths-cache` discards the cache on demand.
1111
- New `nrepl-make-eval-handler` with a keyword-arg API (`:on-value`, `:on-stdout`, `:on-stderr`, `:on-done`, `:on-eval-error`, `:on-content-type`, `:on-truncated`). Sub-handlers no longer take a buffer argument -- they close over whatever they need. `nrepl-make-response-handler`, the legacy 7-positional-arg form, is preserved as an obsolete shim that adapts the old (buffer x) lambdas to the new (x) lambdas, so existing extensions keep working.
12+
- New `cider-repl-history-doctor' command: walks `cider-repl-input-history' looking for entries whose parens don't balance under Clojure syntax, shows each in a side buffer, and asks whether to delete it. When done, rewrites `cider-repl-history-file' if one is configured. Useful for cleaning up history after a typo got committed that breaks `cider-repl-history' rendering (see [#3915](https://github.com/clojure-emacs/cider/issues/3915)).
1213
- Decouple the nREPL transport layer from CIDER's UI layer (closes [#1099](https://github.com/clojure-emacs/cider/issues/1099)). `nrepl-make-eval-handler` is now CIDER-agnostic: it no longer references `nrepl-namespace-handler-function`, `nrepl-err-handler-function`, `nrepl-need-input-handler-function`, or any hardcoded UI strings. New `:on-ns` and `:on-status` keyword slots let any consumer wire up their own namespace tracking and status handling. The editor-level `cider-make-eval-handler` wraps it with CIDER's UI behavior (ns tracking, default error handler, need-input prompt, "Evaluation interrupted." / "Namespace not found." messages); in-tree callers all use it.
1314

1415
### Bugs fixed

lisp/cider-repl.el

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,122 @@ constructs."
16721672
(when (equal major-mode 'cider-repl-mode)
16731673
(cider-repl-history-just-save)))))
16741674

1675+
1676+
;;; History doctor
1677+
1678+
(defun cider-repl--history-entry-balanced-p (entry)
1679+
"Return non-nil if ENTRY's parens balance under Clojure syntax."
1680+
(with-temp-buffer
1681+
(delay-mode-hooks (clojure-mode))
1682+
(insert entry)
1683+
(condition-case nil
1684+
(progn (scan-sexps (point-min) (point-max)) t)
1685+
(scan-error nil))))
1686+
1687+
(defun cider-repl--history-malformed-entries (history)
1688+
"Return a list of (INDEX . ENTRY) cells for malformed entries in HISTORY.
1689+
An entry is malformed when its parens don't balance under Clojure syntax."
1690+
(let ((idx 0) (out nil))
1691+
(dolist (entry history)
1692+
(when (and (stringp entry)
1693+
(not (cider-repl--history-entry-balanced-p entry)))
1694+
(push (cons idx entry) out))
1695+
(cl-incf idx))
1696+
(nreverse out)))
1697+
1698+
(defun cider-repl--history-doctor-buffer ()
1699+
"Get-or-create the doctor inspection buffer."
1700+
(let ((buf (get-buffer-create "*cider-repl-history-doctor*")))
1701+
(with-current-buffer buf
1702+
(delay-mode-hooks (clojure-mode))
1703+
(setq buffer-read-only t))
1704+
buf))
1705+
1706+
(defun cider-repl--history-doctor-show (buf idx entry i total)
1707+
"Display ENTRY at history index IDX in BUF, with progress header (I/TOTAL)."
1708+
(with-current-buffer buf
1709+
(let ((inhibit-read-only t))
1710+
(erase-buffer)
1711+
(insert (format ";; Malformed entry %d/%d -- history index %d, %d chars\n"
1712+
i total idx (length entry)))
1713+
(insert ";; y = delete, n = keep, q = stop reviewing\n\n")
1714+
(insert entry))
1715+
(goto-char (point-min))))
1716+
1717+
(defun cider-repl--history-doctor-walk (repl malformed)
1718+
"Walk MALFORMED entries and prompt the user to delete each.
1719+
MALFORMED is a list of (INDEX . ENTRY) cells against REPL's history.
1720+
Returns the list of deleted indices."
1721+
(let ((buf (cider-repl--history-doctor-buffer))
1722+
(total (length malformed))
1723+
(i 0)
1724+
(deleted nil)
1725+
(done nil))
1726+
(save-window-excursion
1727+
(display-buffer buf)
1728+
(unwind-protect
1729+
(dolist (pair malformed)
1730+
(unless done
1731+
(cl-incf i)
1732+
(let ((idx (car pair))
1733+
(entry (cdr pair)))
1734+
(cider-repl--history-doctor-show buf idx entry i total)
1735+
(pcase (read-char-choice
1736+
(format "Entry %d/%d (index %d) -- delete? (y/n/q) "
1737+
i total idx)
1738+
'(?y ?n ?q))
1739+
(?y (push idx deleted))
1740+
(?n nil)
1741+
(?q (setq done t))))))
1742+
(kill-buffer buf)))
1743+
(when deleted
1744+
(with-current-buffer repl
1745+
(dolist (idx (sort deleted #'>))
1746+
(setq cider-repl-input-history
1747+
(append (seq-take cider-repl-input-history idx)
1748+
(seq-drop cider-repl-input-history (1+ idx)))))))
1749+
deleted))
1750+
1751+
(declare-function cider-current-repl "cider-session")
1752+
(defun cider-repl-history-doctor ()
1753+
"Interactively review and delete malformed entries in the REPL history.
1754+
1755+
Walks `cider-repl-input-history' looking for entries whose parens don't
1756+
balance under Clojure syntax -- the kind of corruption that causes
1757+
`cider-repl-history' to fail with \"Unmatched bracket or quote\" (see
1758+
issue #3915). Each problematic entry is shown in a side buffer; answer
1759+
`y' to delete it, `n' to keep it, `q' to stop reviewing. When done,
1760+
the history file is rewritten if `cider-repl-history-file' is set."
1761+
(interactive)
1762+
(let ((repl (or (and (derived-mode-p 'cider-repl-mode) (current-buffer))
1763+
(bound-and-true-p cider-repl-history-repl-buffer)
1764+
(and (fboundp 'cider-current-repl) (cider-current-repl))
1765+
(user-error "No CIDER REPL available"))))
1766+
(let* ((history (buffer-local-value 'cider-repl-input-history repl))
1767+
(malformed (cider-repl--history-malformed-entries history))
1768+
(total (length malformed)))
1769+
(cond
1770+
((zerop total)
1771+
(message "REPL history is clean (%d entries, all parse)."
1772+
(length history)))
1773+
(t
1774+
(message "Found %d malformed %s out of %d. Reviewing..."
1775+
total (if (= total 1) "entry" "entries") (length history))
1776+
(let ((deleted (cider-repl--history-doctor-walk repl malformed)))
1777+
(cond
1778+
((null deleted)
1779+
(message "No entries deleted."))
1780+
(t
1781+
(when cider-repl-history-file
1782+
(with-current-buffer repl
1783+
(cider-repl--history-write cider-repl-history-file)))
1784+
(message "Deleted %d %s%s."
1785+
(length deleted)
1786+
(if (= 1 (length deleted)) "entry" "entries")
1787+
(if cider-repl-history-file
1788+
(format ", saved %s" cider-repl-history-file)
1789+
""))))))))))
1790+
16751791

16761792
;;; REPL shortcuts
16771793
(defcustom cider-repl-shortcut-dispatch-char ?\,

test/cider-repl-history-tests.el

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,47 @@
7070
(when (get-buffer hist-buf-name)
7171
(kill-buffer hist-buf-name)))))))
7272

73+
(describe "cider-repl--history-entry-balanced-p"
74+
(it "accepts well-formed Clojure forms"
75+
(expect (cider-repl--history-entry-balanced-p "(+ 1 2)") :to-be-truthy)
76+
(expect (cider-repl--history-entry-balanced-p "(let [x [1 2 3]] (map inc x))")
77+
:to-be-truthy)
78+
(expect (cider-repl--history-entry-balanced-p "{:a 1 :b #{2 3}}") :to-be-truthy)
79+
(expect (cider-repl--history-entry-balanced-p "") :to-be-truthy)
80+
(expect (cider-repl--history-entry-balanced-p "42") :to-be-truthy))
81+
82+
(it "rejects entries with unbalanced parens"
83+
(expect (cider-repl--history-entry-balanced-p "(let [x 1] x))))")
84+
:not :to-be-truthy)
85+
(expect (cider-repl--history-entry-balanced-p "(+ 1 (")
86+
:not :to-be-truthy)
87+
(expect (cider-repl--history-entry-balanced-p "[1 2 3")
88+
:not :to-be-truthy)))
89+
90+
(describe "cider-repl--history-malformed-entries"
91+
(it "returns nil for an empty history"
92+
(expect (cider-repl--history-malformed-entries '()) :to-equal nil))
93+
94+
(it "returns nil for a fully well-formed history"
95+
(expect (cider-repl--history-malformed-entries
96+
'("(+ 1 2)" "(println :ok)" "[]"))
97+
:to-equal nil))
98+
99+
(it "reports (INDEX . ENTRY) pairs for each malformed entry"
100+
(let ((hist '("(+ 1 2)" ; 0
101+
"(let [x 1] x))" ; 1 -- malformed
102+
"(println :ok)" ; 2
103+
"[1 2 3")) ; 3 -- malformed
104+
(out (cider-repl--history-malformed-entries
105+
'("(+ 1 2)"
106+
"(let [x 1] x))"
107+
"(println :ok)"
108+
"[1 2 3"))))
109+
(ignore hist)
110+
(expect (length out) :to-equal 2)
111+
(expect (car (nth 0 out)) :to-equal 1)
112+
(expect (car (nth 1 out)) :to-equal 3))))
113+
73114
(provide 'cider-repl-history-tests)
74115

75116
;;; cider-repl-history-tests.el ends here

0 commit comments

Comments
 (0)