branch: elpa/cider
commit e90868295c38241e57e63644fa1c8c9c3d3d0459
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Let a stdin prompt send EOF, and document input handling
When evaluated code reads from stdin, RET sends a line but there was no
way to signal end-of-input: CIDER always appended a newline, so it could
never send the empty string nREPL turns into EOF. Code reading to EOF
(`slurp *in*`, a read-line loop) therefore couldn't be satisfied
interactively - the only escape was to interrupt the whole evaluation.
Add `C-c C-d` at the stdin prompt to send EOF (an empty string), so
`read-line` returns nil and `slurp` completes. Also document stdin
handling - the prompt and its RET/EOF/cancel keys - in the Code
Evaluation manual, where it was previously undocumented.
---
CHANGELOG.md | 1 +
doc/modules/ROOT/pages/usage/code_evaluation.adoc | 31 +++++++++++++++++++++++
lisp/cider-client.el | 31 +++++++++++++++++++----
test/cider-client-tests.el | 21 +++++++++++++--
4 files changed, 77 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b77f74f54..db21bbd3c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### New features
+- [#4108](https://github.com/clojure-emacs/cider/pull/4108): Add `C-c C-d` at
the stdin prompt (shown when evaluated code reads input) to send end-of-input,
so `read-line` returns `nil` and `(slurp *in*)` completes - code reading to EOF
can now be satisfied interactively instead of only by interrupting the
evaluation.
- [#4094](https://github.com/clojure-emacs/cider/pull/4094): Add
`cider-doctor`, which checks your Emacs setup (and the active nREPL session, if
any) for common problems and shows a copy-pasteable report -
Emacs/CIDER/dependency versions, build tools on the path, stale byte-code,
leftover obsolete config, and cider-nrepl/nREPL/Clojure compatibility.
- Render fetched `text/html` content as formatted text (via `shr`) in the REPL
and the rich-content popup, and make the URL of an external-content result a
clickable link that opens in the browser (next to its `[show content]` button).
Remote images in that HTML are not fetched, so rendering a result never makes a
network request on its own (only inline `data:` images render).
- Honor content types for interactive evaluations too
([#2476](https://github.com/clojure-emacs/cider/issues/2476)): a
`cider-eval-last-sexp` returning an image can now render it, per the new
`cider-eval-rich-content-destination` - `inline` (the default, in the result
overlay at point), `repl` (like results of forms typed at the prompt, with the
`[show content]` button for external references), `popup` (the `*cider-result*`
buffer) or `nil` (plain values, the previous behavior).
diff --git a/doc/modules/ROOT/pages/usage/code_evaluation.adoc
b/doc/modules/ROOT/pages/usage/code_evaluation.adoc
index dbcd11385b..daa3bb947f 100644
--- a/doc/modules/ROOT/pages/usage/code_evaluation.adoc
+++ b/doc/modules/ROOT/pages/usage/code_evaluation.adoc
@@ -159,6 +159,37 @@ for sync eval. You can adjust this default if needed:
NOTE: CIDER internally increases the timeout to 30 seconds for the first sync
eval request it does, as it might require a lot of namespaces and take more
time to complete. See `cider--prep-interactive-eval` for details.
+== Reading Input from Running Code
+
+When code you evaluate reads from standard input - `(read-line)`, `(read)`,
+`(slurp *in*)`, and so on - the evaluation blocks until you supply that input.
+CIDER prompts for it at the minibuffer with a `Stdin` prompt. This happens for
+any evaluation, whether you triggered it from the REPL or from a source buffer,
+and the input is routed back to the exact connection that asked for it.
+
+At the prompt you have three options:
+
+|===
+| Key | Effect
+
+| kbd:[RET]
+| Send the typed text as one line (a trailing newline is added, so `read-line`
+returns it).
+
+| kbd:[C-c C-d]
+| Send *end-of-input* (EOF). `read-line` returns `nil`, `(slurp *in*)`
+completes, and read loops terminate. This is distinct from pressing kbd:[RET]
+on an empty line, which sends an empty line (so `read-line` returns `""` and
the
+program keeps reading).
+
+| kbd:[C-c C-c]
+| Cancel: interrupt the blocked evaluation altogether.
+|===
+
+TIP: If code reads until end-of-stream (for example `(slurp *in*)` or a
+`(loop [] (when-let [line (read-line)] ... (recur)))`), you need kbd:[C-c C-d]
+to finish it - there is no other way to signal that no more input is coming.
+
== Configuration
=== Enable evaluation interrupts on Java 21 and newer
diff --git a/lisp/cider-client.el b/lisp/cider-client.el
index 7906b80366..b0cca1f09e 100644
--- a/lisp/cider-client.el
+++ b/lisp/cider-client.el
@@ -1195,8 +1195,23 @@ side effects; only the global handlers fire (need-input,
eval-error, ...)."
When the session has more than one connection, name REPL so it's clear which
evaluation is waiting for input."
(if (and (bufferp repl) (cdr (cider-repls)))
- (format "Stdin for %s (C-c C-c to cancel): " (buffer-name repl))
- "Stdin (C-c C-c to cancel): "))
+ (format "Stdin for %s (C-c C-d = EOF, C-c C-c = cancel): " (buffer-name
repl))
+ "Stdin (C-c C-d = EOF, C-c C-c = cancel): "))
+
+(defvar cider-need-input--eof nil
+ "Set by `cider-need-input-send-eof' while a stdin prompt is open.
+`cider-need-input' binds it and checks it after the read to decide whether to
+send end-of-input rather than a line.")
+
+(defun cider-need-input-send-eof ()
+ "End the current stdin read by sending end-of-input to the evaluation.
+Bound in the stdin minibuffer prompt (see `cider-need-input'); the empty
+string CIDER sends is what nREPL turns into EOF, so `read-line' returns nil,
+`slurp' completes, and read loops terminate - which pressing \\`RET' (an empty
+line) does not do."
+ (interactive)
+ (setq cider-need-input--eof t)
+ (exit-minibuffer))
(defun cider--connection-for-session (session)
"Return the connection buffer whose nREPL SESSION matches, or nil.
@@ -1222,7 +1237,10 @@ RESPONSE is the nREPL need-input response (the message
whose status is
\"need-input\"). When given, its `session' selects the exact connection to
answer (important with several connections) and its `id' lets a cancel
interrupt just the blocked evaluation. Without it, both fall back to
-`cider-current-repl' and a connection-wide interrupt."
+`cider-current-repl' and a connection-wide interrupt.
+
+At the prompt, \\`RET' sends the line, `cider-need-input-send-eof' sends
+end-of-input, and cancelling (quit) interrupts the evaluation."
(with-current-buffer buffer
(let* ((session (and response (nrepl-dict-get response "session")))
(id (and response (nrepl-dict-get response "id")))
@@ -1231,9 +1249,12 @@ interrupt just the blocked evaluation. Without it, both
fall back to
(map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-map)
(define-key map (kbd "C-c C-c") #'abort-recursive-edit)
+ (define-key map (kbd "C-c C-d") #'cider-need-input-send-eof)
(condition-case nil
- (let ((input (read-from-minibuffer (cider-need-input--prompt repl)
nil map)))
- (nrepl-request:stdin (concat input "\n")
+ (let* ((cider-need-input--eof nil)
+ (input (read-from-minibuffer (cider-need-input--prompt repl)
nil map)))
+ ;; An empty string is what nREPL turns into EOF; a line otherwise.
+ (nrepl-request:stdin (if cider-need-input--eof "" (concat input
"\n"))
(cider-stdin-handler buffer)
repl))
(quit
diff --git a/test/cider-client-tests.el b/test/cider-client-tests.el
index 65fdea275d..460887f4d5 100644
--- a/test/cider-client-tests.el
+++ b/test/cider-client-tests.el
@@ -564,7 +564,7 @@
(it "uses a plain prompt with a single connection"
(spy-on 'cider-repls :and-return-value (list 'repl-a))
(expect (cider-need-input--prompt (generate-new-buffer " *repl*"))
- :to-equal "Stdin (C-c C-c to cancel): "))
+ :to-equal "Stdin (C-c C-d = EOF, C-c C-c = cancel): "))
(it "names the REPL when the session has several connections"
(let ((repl (generate-new-buffer " *cider-repl foo*")))
@@ -572,7 +572,7 @@
(progn
(spy-on 'cider-repls :and-return-value (list repl 'repl-b))
(expect (cider-need-input--prompt repl)
- :to-equal (format "Stdin for %s (C-c C-c to cancel): "
+ :to-equal (format "Stdin for %s (C-c C-d = EOF, C-c C-c =
cancel): "
(buffer-name repl))))
(kill-buffer repl)))))
@@ -595,6 +595,16 @@
(expect (car (spy-calls-args-for 'nrepl-request:stdin 0)) :to-equal
"hello\n")
(expect 'cider-interrupt-repl :not :to-have-been-called))
+ (it "sends EOF (an empty string) when end-of-input is requested"
+ ;; simulate C-c C-d: `cider-need-input-send-eof' sets the flag mid-read
+ (spy-on 'read-from-minibuffer :and-call-fake
+ (lambda (&rest _) (setq cider-need-input--eof t) ""))
+ (with-temp-buffer
+ (cider-need-input (current-buffer)))
+ ;; an empty string, not "\n" - that is what nREPL turns into EOF
+ (expect (car (spy-calls-args-for 'nrepl-request:stdin 0)) :to-equal "")
+ (expect 'cider-interrupt-repl :not :to-have-been-called))
+
(it "interrupts the evaluation when the prompt is cancelled"
(spy-on 'read-from-minibuffer :and-call-fake (lambda (&rest _) (signal
'quit nil)))
(with-temp-buffer
@@ -645,3 +655,10 @@
(cider-need-input (current-buffer) '(dict "session" "s1" "id" "42")))
(expect 'nrepl-request:interrupt :to-have-been-called-with "42" #'ignore
conn)
(expect 'cider-interrupt-repl :not :to-have-been-called)))
+
+(describe "cider-need-input-send-eof"
+ (it "sets the EOF flag"
+ (let ((cider-need-input--eof nil))
+ ;; `exit-minibuffer' errors outside a minibuffer; the flag is set first
+ (ignore-errors (cider-need-input-send-eof))
+ (expect cider-need-input--eof :to-be t))))