Skip to content

Commit 2b58bda

Browse files
committed
Honor content types for interactive evaluations
Rich results were REPL-prompt-only since 2018 (#2476): evaluating a form returning an image from a source buffer just printed the object. Interactive evals now opt in to content-typed responses, and the new cider-eval-rich-content-destination controls where rich results render: inline in the result overlay (the default), in the REPL (reusing its content-type handlers, buttons included), in the *cider-result* popup, or nil for the old plain-value behavior.
1 parent 3e96026 commit 2b58bda

5 files changed

Lines changed: 244 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### New features
66

7+
- 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).
78
- Add a transient menu to the debugger (`cider-debug-menu`, bound to `?` during a debug session), grouping all the debugger's single-key commands; they are also proper named commands now (e.g. `cider-debug-next`, `cider-debug-quit`), so they can be invoked via `M-x` as well.
89
- Add a transient menu to the inspector (`cider-inspector-menu`, bound to `m` in the inspector buffer), grouping all the inspector commands.
910
- [#4065](https://github.com/clojure-emacs/cider/pull/4065): Add a `--print-fn=` flag to the pretty-print transient menu (`cider-eval-pprint-menu`) to pick the printer (pr/pprint/fipp/puget/zprint or a custom var) per invocation.

doc/modules/ROOT/pages/repl/configuration.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ IO (see https://github.com/clojure-emacs/cider/issues/2825[the discussion]).
306306
The on-demand button, together with the hardened `cider-nrepl` middleware
307307
(scheme allowlist, size caps, graceful fetch errors), resolves that.
308308

309+
TIP: Interactive evaluations (e.g. kbd:[C-x C-e] in a source buffer) get the
310+
same treatment, controlled separately by
311+
`cider-eval-rich-content-destination` - see
312+
xref:usage/code_evaluation.adoc[Code Evaluation].
313+
309314
== REPL type detection
310315

311316
Normally CIDER would detect automatically the type of a REPL (Clojure or ClojureScript), based

doc/modules/ROOT/pages/usage/code_evaluation.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,32 @@ Additionally, there's the variable `cider-redirect-server-output-to-repl` that c
411411

412412
NOTE: The redirection functionality is implemented in `cider-nrepl` as nREPL middleware. If you're using CIDER without `cider-nrepl` no output redirection is going to take place.
413413

414+
=== Rich Results
415+
416+
When an interactive evaluation produces a value with a recognized content
417+
type - an image, or a reference to external content such as a file or URL -
418+
CIDER can render it instead of just printing the value. Where it renders is
419+
controlled by `cider-eval-rich-content-destination`:
420+
421+
* `inline` (the default) - directly displayable content (images) shows in
422+
the result overlay at point, just like a regular evaluation result;
423+
anything that can't render inline falls back to its plain-text
424+
representation.
425+
* `repl` - rich results render in the current REPL buffer, exactly like the
426+
results of forms entered at the prompt; references to external content get
427+
a `[show content]` button there.
428+
* `popup` - rich results render in the `+*cider-result*+` popup buffer,
429+
where external references also get a fetch button.
430+
* `nil` - disable the feature; results display as plain printed values.
414431

432+
[source,lisp]
433+
----
434+
(setq cider-eval-rich-content-destination 'repl)
435+
----
436+
437+
This is the interactive-evaluation counterpart of the REPL's rich content
438+
support (see xref:repl/configuration.adoc[REPL Configuration]) and likewise
439+
requires `cider-nrepl`.
415440

416441
=== Storing eval results
417442

lisp/cider-eval.el

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,30 @@ If `output-buffer', it's sent to a dedicated `*cider-out*' buffer."
143143
:group 'cider
144144
:package-version '(cider . "2.0.0"))
145145

146+
(defcustom cider-eval-rich-content-destination 'inline
147+
"Where rich (content-typed) interactive evaluation results are rendered.
148+
When an interactive evaluation (e.g. `cider-eval-last-sexp') produces a
149+
value with a recognized content type - an image, or a reference to external
150+
content such as a file or URL - this controls where that content shows up:
151+
152+
`inline' - render directly displayable content (images) in the result
153+
overlay at point; other rich results fall back to their
154+
plain-text representation.
155+
`repl' - render in the current REPL buffer, exactly like rich results
156+
of forms entered at the REPL prompt (references to external
157+
content get a [show content] button there).
158+
`popup' - render in the `*cider-result*' popup buffer.
159+
nil - don't request rich content; results display as plain values.
160+
161+
This is the interactive-evaluation counterpart of
162+
`cider-repl-use-content-types', and likewise requires cider-nrepl."
163+
:type '(choice (const :tag "Inline, in the result overlay" inline)
164+
(const :tag "In the REPL buffer" repl)
165+
(const :tag "In a popup buffer" popup)
166+
(const :tag "Plain printed values" nil))
167+
:group 'cider
168+
:package-version '(cider . "2.0.0"))
169+
146170
(defcustom cider-comment-prefix ";; => "
147171
"The prefix to insert before the first line of commented output."
148172
:type 'string
@@ -403,6 +427,109 @@ Cancel their animation timers and delete the overlays."
403427
(setq cider--eval-pending-overlays nil))
404428

405429
(declare-function cider-inspect-last-result "cider-inspector")
430+
;;; Rich content rendering for interactive evals
431+
432+
(defvar cider-repl-content-type-handler-alist)
433+
(declare-function cider-repl-emit-result "cider-repl")
434+
435+
(defconst cider--image-content-types
436+
'(("image/jpeg" . jpeg) ("image/png" . png) ("image/svg+xml" . svg))
437+
"Mapping from image MIME types to Emacs image types.")
438+
439+
(defun cider--external-body-url (content-type)
440+
"Return the URL referenced by an external-body CONTENT-TYPE, or nil."
441+
(pcase-let ((`(,type ,attrs) content-type))
442+
(when (equal type "message/external-body")
443+
(when-let* ((access-type (nrepl-dict-get attrs "access-type")))
444+
(nrepl-dict-get attrs access-type)))))
445+
446+
(defun cider--rich-content-fallback-string (body content-type)
447+
"Return a plain-text stand-in for BODY of CONTENT-TYPE.
448+
Used when rich content can't (or shouldn't) be rendered: external
449+
references display as their URL, images as a short tag, and anything
450+
else as its raw body."
451+
(pcase-let ((`(,type ,_attrs) content-type))
452+
(cond
453+
((cider--external-body-url content-type))
454+
((assoc type cider--image-content-types) (format "#content[%s]" type))
455+
(t body))))
456+
457+
(defun cider--render-rich-content-inline (body content-type point)
458+
"Render BODY of CONTENT-TYPE in a result overlay at POINT, if possible.
459+
Only directly displayable content (images) is rendered inline; return nil
460+
otherwise, so the caller falls back to a plain display."
461+
(when-let* ((image-type (cdr (assoc (car content-type)
462+
cider--image-content-types))))
463+
(when (and point
464+
(display-images-p)
465+
(memq (cider--eval-result-display) '(overlay both)))
466+
(cider--make-result-overlay (propertize " " 'display
467+
(create-image body image-type t))
468+
:where point
469+
:duration cider-eval-result-duration
470+
:prepend-face 'cider-result-overlay-face)
471+
(message "%s#content[%s]" cider-eval-result-prefix (car content-type))
472+
t)))
473+
474+
(defun cider--render-rich-content-repl (body content-type)
475+
"Render BODY of CONTENT-TYPE in the current REPL buffer.
476+
Reuses the REPL's `cider-repl-content-type-handler-alist' handlers, so
477+
this behaves exactly like a rich result of a form entered at the prompt.
478+
Return nil when there's no REPL to render into."
479+
(when-let* ((repl (cider-current-repl)))
480+
(if-let* ((handler (cdr (assoc (car content-type)
481+
cider-repl-content-type-handler-alist))))
482+
(funcall handler content-type repl body t t)
483+
(cider-repl-emit-result repl body t t))
484+
t))
485+
486+
(defun cider--popup-insert-rich-content (buffer body content-type)
487+
"Insert BODY of CONTENT-TYPE at the end of the popup BUFFER."
488+
(with-current-buffer buffer
489+
(let ((inhibit-read-only t))
490+
(goto-char (point-max))
491+
(cond
492+
((when-let* ((image-type (cdr (assoc (car content-type)
493+
cider--image-content-types))))
494+
(insert-image (create-image body image-type t) " ")
495+
(insert "\n")
496+
t))
497+
((when-let* ((url (cider--external-body-url content-type)))
498+
(insert url " ")
499+
(insert-text-button "[show content]"
500+
'follow-link t
501+
'help-echo (format "Fetch %s and render it here" url)
502+
'action (lambda (_button)
503+
(cider--popup-fetch-external-body buffer url)))
504+
(insert "\n")
505+
t))
506+
(t (insert body "\n"))))))
507+
508+
(defun cider--popup-fetch-external-body (buffer url)
509+
"Fetch URL via the cider/slurp op and render it into popup BUFFER."
510+
(cider-nrepl-send-request
511+
(list "op" "cider/slurp" "url" url)
512+
(cider-make-eval-handler
513+
:buffer buffer
514+
:on-content-type (lambda (body content-type)
515+
(cider--popup-insert-rich-content buffer body content-type)))))
516+
517+
(defun cider--render-rich-content-popup (body content-type)
518+
"Render BODY of CONTENT-TYPE in the `*cider-result*' popup buffer."
519+
(let ((buffer (cider-popup-buffer cider-result-buffer nil 'clojure-mode 'ancillary)))
520+
(cider--popup-insert-rich-content buffer body content-type)
521+
t))
522+
523+
(defun cider--display-rich-content (body content-type point)
524+
"Display BODY of CONTENT-TYPE per `cider-eval-rich-content-destination'.
525+
POINT anchors inline overlays. Return non-nil when the content was
526+
rendered; nil means the caller should fall back to a plain display."
527+
(pcase cider-eval-rich-content-destination
528+
('inline (cider--render-rich-content-inline body content-type point))
529+
('repl (cider--render-rich-content-repl body content-type))
530+
('popup (cider--render-rich-content-popup body content-type))
531+
(_ nil)))
532+
406533
(defun cider-interactive-eval-handler (&optional buffer place)
407534
"Make an interactive eval handler for BUFFER.
408535
PLACE is used to display the evaluation result.
@@ -426,6 +553,14 @@ when `cider-auto-inspect-after-eval' is non-nil."
426553
(cider--eval-pending-overlay-remove)))
427554
(setq res (concat res value))
428555
(cider--display-interactive-eval-result res 'value end))
556+
:on-content-type (lambda (body content-type)
557+
(when (buffer-live-p eval-buffer)
558+
(with-current-buffer eval-buffer
559+
(cider--eval-pending-overlay-remove)))
560+
(unless (cider--display-rich-content body content-type end)
561+
(setq res (concat res (cider--rich-content-fallback-string
562+
body content-type)))
563+
(cider--display-interactive-eval-result res 'value end)))
429564
:on-stdout #'cider-emit-interactive-eval-output
430565
:on-stderr (lambda (err)
431566
(cider-emit-interactive-eval-err-output err)
@@ -674,7 +809,12 @@ arguments and only proceed with evaluation if it returns nil."
674809
:ns (if (cider-ns-form-p form) "user" (cider-current-ns))
675810
:line (when start (line-number-at-pos start))
676811
:column (when start (cider-column-number-at-pos start))
677-
:additional-params additional-params
812+
;; Opt in to content-typed responses; handlers without an
813+
;; `:on-content-type' slot still see the plain value, since
814+
;; the server sends both.
815+
:additional-params (append (when cider-eval-rich-content-destination
816+
(list "content-type" "true"))
817+
additional-params)
678818
:connection connection)))))))
679819

680820
(defun cider-eval-region (start end)

test/cider-eval-tests.el

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,75 @@
403403
nil
404404
(lambda () (setq captured cider-print-fn)))
405405
(expect captured :to-equal 'pprint))))
406+
407+
(describe "cider--external-body-url"
408+
(it "extracts the URL from an external-body content-type"
409+
(expect (cider--external-body-url
410+
(list "message/external-body"
411+
(nrepl-dict "access-type" "URL" "URL" "file:/tmp/x.png")))
412+
:to-equal "file:/tmp/x.png"))
413+
(it "returns nil for other content types"
414+
(expect (cider--external-body-url (list "image/png" (nrepl-dict)))
415+
:to-be nil)))
416+
417+
(describe "cider--rich-content-fallback-string"
418+
(it "falls back to the URL for external bodies"
419+
(expect (cider--rich-content-fallback-string
420+
""
421+
(list "message/external-body"
422+
(nrepl-dict "access-type" "URL" "URL" "https://foo.bar/x.png")))
423+
:to-equal "https://foo.bar/x.png"))
424+
(it "falls back to a short tag for images"
425+
(expect (cider--rich-content-fallback-string "<binary>" (list "image/png" (nrepl-dict)))
426+
:to-equal "#content[image/png]"))
427+
(it "falls back to the raw body for anything else"
428+
(expect (cider--rich-content-fallback-string "graph foo {}" (list "text/vnd.graphviz" (nrepl-dict)))
429+
:to-equal "graph foo {}")))
430+
431+
(describe "cider--display-rich-content"
432+
(it "does nothing when the destination is nil"
433+
(let ((cider-eval-rich-content-destination nil))
434+
(expect (cider--display-rich-content "body" (list "image/png" (nrepl-dict)) nil)
435+
:to-be nil)))
436+
437+
(it "dispatches to the REPL's content-type handlers for the repl destination"
438+
(let ((cider-eval-rich-content-destination 'repl)
439+
(handled nil))
440+
(cl-letf (((symbol-function 'cider-current-repl)
441+
(lambda (&rest _) (current-buffer)))
442+
(cider-repl-content-type-handler-alist
443+
(list (cons "image/png"
444+
(lambda (_type _buffer body &rest _)
445+
(setq handled body))))))
446+
(expect (cider--display-rich-content "IMG" (list "image/png" (nrepl-dict)) nil)
447+
:to-be-truthy)
448+
(expect handled :to-equal "IMG"))))
449+
450+
(it "reports failure for the repl destination when no REPL is around"
451+
(let ((cider-eval-rich-content-destination 'repl))
452+
(cl-letf (((symbol-function 'cider-current-repl) (lambda (&rest _) nil)))
453+
(expect (cider--display-rich-content "IMG" (list "image/png" (nrepl-dict)) nil)
454+
:to-be nil))))
455+
456+
(it "renders images inline via a result overlay"
457+
(let ((cider-eval-rich-content-destination 'inline)
458+
(overlaid nil))
459+
(cl-letf (((symbol-function 'display-images-p) (lambda (&rest _) t))
460+
((symbol-function 'cider--eval-result-display) (lambda () 'both))
461+
((symbol-function 'cider--make-result-overlay)
462+
(lambda (string &rest _) (setq overlaid string)))
463+
((symbol-function 'create-image)
464+
(lambda (&rest _) '(image :type png)))
465+
((symbol-function 'message) #'ignore))
466+
(expect (cider--display-rich-content "IMG" (list "image/png" (nrepl-dict)) 42)
467+
:to-be-truthy)
468+
(expect (get-text-property 0 'display overlaid) :to-equal '(image :type png)))))
469+
470+
(it "declines inline rendering for external bodies"
471+
(let ((cider-eval-rich-content-destination 'inline))
472+
(expect (cider--display-rich-content
473+
""
474+
(list "message/external-body"
475+
(nrepl-dict "access-type" "URL" "URL" "file:/x"))
476+
42)
477+
:to-be nil))))

0 commit comments

Comments
 (0)