Skip to content

Commit 90c06db

Browse files
committed
Display interactive eval results in the minibuffer
Add a port-eval-display option (minibuffer / repl / both, default minibuffer) controlling where C-x C-e and friends render their result. The minibuffer and both modes route the form through the tool socket via a new port.tooling/-user-eval bootstrap entry that takes a target namespace and a form string, captures stdout/stderr, and returns a result map matching the existing -eval shape (plus a terse :ex-message field so we don't have to try to re-parse a printed Throwable->map). The repl mode preserves the historical behavior of streaming through the user socket. Captured stdout/stderr always lands in the REPL buffer regardless of mode so prints are never silently lost; in both mode the value is also echoed there and a fresh prompt left at the bottom. Best-effort current-ns detection prefers clojure-find-ns from the source buffer, then falls back to the user socket's tracked ns, then "user".
1 parent 7e536cc commit 90c06db

3 files changed

Lines changed: 299 additions & 14 deletions

File tree

lisp/port-eval.el

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,135 @@
99

1010
;;; Commentary:
1111

12-
;; Interactive evaluation commands. Forms are sent to the active prepl
13-
;; via the REPL buffer; all output (return value, stdout, stderr) is
14-
;; rendered into the REPL buffer, in monroe style.
12+
;; Interactive evaluation commands. Where the result lands depends on
13+
;; `port-eval-display':
14+
;;
15+
;; `minibuffer' Send the form through the tool socket and show the
16+
;; returned value in the minibuffer (CIDER-style).
17+
;; Captured stdout/stderr are still echoed into the
18+
;; REPL buffer.
19+
;;
20+
;; `repl' The historical Port behavior: send the form through
21+
;; the user socket so it appears in the REPL buffer
22+
;; with live streaming output.
23+
;;
24+
;; `both' Send through the tool socket, but also echo the
25+
;; form and the result into the REPL buffer.
1526

1627
;;; Code:
1728

1829
(require 'port-client)
1930
(require 'port-session)
2031
(require 'port-repl)
32+
(require 'port-tooling)
33+
34+
(declare-function clojure-find-ns "ext:clojure-mode")
35+
36+
(defcustom port-eval-display 'minibuffer
37+
"Where to display the result of interactive evaluation commands.
38+
The commands `port-eval-last-sexp', `port-eval-defun-at-point',
39+
`port-eval-region', and `port-eval-buffer' use this option to
40+
decide where the result lands.
41+
42+
Possible values:
43+
44+
`minibuffer' Show the value in the minibuffer (default). The
45+
form is evaluated through the tool socket so its
46+
stdout/stderr are captured and echoed into the
47+
REPL buffer; only the value itself appears in the
48+
minibuffer. No live streaming -- prints arrive
49+
with the result.
50+
51+
`repl' Send the form to the user socket so it appears in
52+
the REPL buffer as if typed there, with live
53+
streaming output. Nothing is shown in the
54+
minibuffer. This is the historical Port
55+
behavior.
56+
57+
`both' Like `minibuffer', but additionally echo the form
58+
into the REPL input area and the result into the
59+
REPL buffer."
60+
:type '(choice (const :tag "Minibuffer" minibuffer)
61+
(const :tag "REPL buffer" repl)
62+
(const :tag "Both" both))
63+
:group 'port)
2164

2265
(defun port-eval-string (code)
23-
"Send CODE (a string) to the active prepl user socket.
24-
The code is rendered into the REPL buffer as if typed there."
66+
"Evaluate CODE (a string) according to `port-eval-display'."
2567
(let* ((session (port-current-session))
26-
(conn (port-session-user-conn session))
27-
(repl (port-session-repl-buffer session)))
68+
(display port-eval-display))
69+
(cond
70+
((eq display 'repl)
71+
(port-eval--send-via-repl session code))
72+
(t
73+
(let ((ns (port-eval--current-ns session)))
74+
(when (eq display 'both)
75+
(port-eval--echo-form session code))
76+
(port-tooling-user-eval session ns code
77+
#'port-eval--display-result))))))
78+
79+
(defun port-eval--current-ns (session)
80+
"Best-effort namespace name (string) for evaluating from the current buffer.
81+
Falls back to the user socket's tracked ns, then to \"user\"."
82+
(or (and (fboundp 'clojure-find-ns) (clojure-find-ns))
83+
(port-client-current-ns (port-session-user-conn session))
84+
"user"))
85+
86+
(defun port-eval--echo-form (session code)
87+
"Echo CODE into SESSION's REPL buffer as if it had been typed there.
88+
Used by the `both' display mode so the form appears in the REPL
89+
even though evaluation is happening on the tool socket."
90+
(let ((repl (port-session-repl-buffer session)))
2891
(when (and repl (buffer-live-p repl))
2992
(with-current-buffer repl
3093
(let ((inhibit-read-only t))
3194
(goto-char (point-max))
3295
(insert code)
33-
(insert "\n")
96+
(unless (string-suffix-p "\n" code) (insert "\n"))
3497
(add-text-properties port-repl-input-start-marker (point)
35-
'(read-only t
36-
rear-nonsticky (read-only))))))
37-
(port-client-send conn code)))
98+
'(read-only t rear-nonsticky (read-only)))
99+
(set-marker port-repl-prompt-marker (point))
100+
(set-marker port-repl-input-start-marker (point))
101+
(setq port-repl-prompt-active-p nil))))))
102+
103+
(defun port-eval--send-via-repl (session code)
104+
"Send CODE through SESSION's user socket, echoing into the REPL buffer."
105+
(port-eval--echo-form session code)
106+
(port-client-send (port-session-user-conn session) code))
107+
108+
(defun port-eval--display-result (result)
109+
"Render RESULT (the result alist from `port-tooling-user-eval').
110+
Captured stdout/stderr always go to the REPL buffer so prints are
111+
not silently lost. The value (or error message) is shown in the
112+
minibuffer, and additionally in the REPL when `port-eval-display'
113+
is `both'."
114+
(let* ((tag (alist-get :tag result))
115+
(val (alist-get :val result))
116+
(out (alist-get :out result))
117+
(err (alist-get :err result))
118+
(msg (or (alist-get :ex-message result) (alist-get :ex result))))
119+
(when (and out (not (string-empty-p out)))
120+
(port-repl-emit-text out 'port-repl-stdout-face))
121+
(when (and err (not (string-empty-p err)))
122+
(port-repl-emit-text err 'port-repl-stderr-face))
123+
(when (eq port-eval-display 'both)
124+
(port-repl-emit-text
125+
(format "%s\n" (if (eq tag :err) msg val))
126+
(if (eq tag :err) 'port-repl-stderr-face 'port-repl-result-face))
127+
;; If `port-repl-emit-text' didn't already restore a live
128+
;; prompt (i.e. we appended at point-max because the prompt
129+
;; was already consumed by `port-eval--echo-form'), drop one
130+
;; in now so the next interaction starts cleanly.
131+
(when-let ((buf (port-session-repl-buffer (port-current-session))))
132+
(when (buffer-live-p buf)
133+
(with-current-buffer buf
134+
(unless port-repl-prompt-active-p
135+
(port-repl--insert-prompt))))))
136+
(cond
137+
((eq tag :err)
138+
(message "%s" (propertize (or msg "<error>") 'face 'error)))
139+
(t
140+
(message "=> %s" val)))))
38141

39142
;;;###autoload
40143
(defun port-eval-last-sexp ()

lisp/port-tooling.el

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,30 @@
3434
{:port/id id :tag :ok :val (pr-str v)
3535
:out (str out-buf) :err (str err-buf)})
3636
(catch Throwable t
37-
{:port/id id :tag :err :ex (pr-str (Throwable->map t))
37+
{:port/id id :tag :err
38+
:ex (pr-str (Throwable->map t))
39+
:ex-message (or (.getMessage t) (.getName (class t)))
40+
:out (str out-buf) :err (str err-buf)})))))
41+
(clojure.core/defn -user-eval [id ns-sym form-string]
42+
(let [out-buf (java.io.StringWriter.)
43+
err-buf (java.io.StringWriter.)]
44+
(binding [*out* out-buf *err* err-buf
45+
*ns* (or (find-ns ns-sym) (find-ns 'user))]
46+
(try
47+
(let [v (eval (read-string (str \"(do\\n\" form-string \"\\n)\")))]
48+
{:port/id id :tag :ok :val (pr-str v)
49+
:out (str out-buf) :err (str err-buf)
50+
:ns (str (ns-name *ns*))})
51+
(catch Throwable t
52+
{:port/id id :tag :err
53+
:ex (pr-str (Throwable->map t))
54+
:ex-message (or (.getMessage t) (.getName (class t)))
3855
:out (str out-buf) :err (str err-buf)}))))))"
3956
"Clojure form sent on the tool socket on connect.
40-
Defines `port.tooling/-eval', the wrapper used by `port-tooling-call'.")
57+
Defines `port.tooling/-eval' (the wrapper used by `port-tooling-call'
58+
for internal helper queries) and `port.tooling/-user-eval' (the
59+
namespace-aware variant used for interactive evaluation from source
60+
buffers).")
4161

4262
(defun port-tooling-install (session)
4363
"Install the tool-socket handler on SESSION and send the bootstrap form."
@@ -51,13 +71,28 @@ Defines `port.tooling/-eval', the wrapper used by `port-tooling-call'.")
5171
"Evaluate FORM-STRING on SESSION's tool socket.
5272
CALLBACK is invoked with the result alist, which has keys
5373
`:port/id', `:tag' (`:ok' or `:err'), `:val' (printed return value
54-
when `:ok'), `:ex' (printed Throwable->map when `:err'), `:out',
74+
when `:ok'), `:ex' (printed Throwable->map when `:err'),
75+
`:ex-message' (just the exception's message when `:err'), `:out',
5576
`:err'."
5677
(let* ((id (port-session-next-id! session))
5778
(wrapped (format "(port.tooling/-eval %d (fn [] %s))" id form-string)))
5879
(port-session-register-callback session id callback)
5980
(port-client-send (port-session-tool-conn session) wrapped)))
6081

82+
(defun port-tooling-user-eval (session ns code callback)
83+
"Evaluate CODE in namespace NS on SESSION's tool socket.
84+
NS is a Clojure namespace name as a string or symbol; if it can't
85+
be resolved on the JVM side, evaluation falls back to the `user'
86+
namespace. CODE is a Clojure source string, possibly containing
87+
several top-level forms (the wrapper splices them into a `do').
88+
CALLBACK is invoked with the parsed result alist (same shape as
89+
`port-tooling-call', plus `:ns' on success)."
90+
(let* ((id (port-session-next-id! session))
91+
(wrapped (format "(port.tooling/-user-eval %d (quote %s) %S)"
92+
id ns code)))
93+
(port-session-register-callback session id callback)
94+
(port-client-send (port-session-tool-conn session) wrapped)))
95+
6196
(defun port-tooling-call-sync (session form-string &optional timeout)
6297
"Like `port-tooling-call' but block until the response arrives.
6398
TIMEOUT defaults to 2 seconds. Returns the result alist, or nil on

test/port-eval-tests.el

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
;;; port-eval-tests.el --- Tests for port-eval -*- lexical-binding: t -*-
2+
3+
;;; Commentary:
4+
5+
;; Tests for the eval-display dispatch and rendering helpers. We
6+
;; don't talk to a real prepl: we synthesize result alists and
7+
;; capture what `message' would print and what gets emitted into a
8+
;; fixture REPL buffer.
9+
10+
;;; Code:
11+
12+
(require 'ert)
13+
(require 'cl-lib)
14+
(require 'port-client)
15+
(require 'port-session)
16+
(require 'port-repl)
17+
(require 'port-tooling)
18+
(require 'port-eval)
19+
20+
(defun port-eval-tests--with-message (thunk)
21+
"Run THUNK; return the last formatted message it produced."
22+
(let (captured)
23+
(cl-letf (((symbol-function 'message)
24+
(lambda (fmt &rest args)
25+
(setq captured (apply #'format fmt args)))))
26+
(funcall thunk))
27+
captured))
28+
29+
(defun port-eval-tests--make-session ()
30+
"Create a session with a stub user-conn (no real process)."
31+
(let ((conn (port-client--make :host "h" :port 1 :process nil
32+
:buffer nil :pending ""
33+
:current-ns "user" :handler #'ignore)))
34+
(port-session--make :host "h" :port 1
35+
:user-conn conn :tool-conn conn)))
36+
37+
(ert-deftest port-eval-test-display-minibuffer-shows-value ()
38+
(let* ((port-eval-display 'minibuffer)
39+
(session (port-eval-tests--make-session))
40+
(port-default-session session)
41+
(msg (port-eval-tests--with-message
42+
(lambda ()
43+
(port-eval--display-result
44+
'((:tag . :ok) (:val . "3")
45+
(:out . "") (:err . "") (:ns . "user")))))))
46+
(should (equal "=> 3" msg))))
47+
48+
(ert-deftest port-eval-test-display-error-uses-ex-message ()
49+
(let* ((port-eval-display 'minibuffer)
50+
(session (port-eval-tests--make-session))
51+
(port-default-session session)
52+
(msg (port-eval-tests--with-message
53+
(lambda ()
54+
(port-eval--display-result
55+
'((:tag . :err)
56+
(:ex-message . "Divide by zero")
57+
(:ex . "{:cause \"Divide by zero\" ...}")
58+
(:out . "") (:err . "")))))))
59+
(should (equal "Divide by zero" msg))))
60+
61+
(ert-deftest port-eval-test-display-error-falls-back-to-ex ()
62+
"When :ex-message is missing, fall back to the printed Throwable->map."
63+
(let* ((port-eval-display 'minibuffer)
64+
(session (port-eval-tests--make-session))
65+
(port-default-session session)
66+
(msg (port-eval-tests--with-message
67+
(lambda ()
68+
(port-eval--display-result
69+
'((:tag . :err)
70+
(:ex . "boom") (:out . "") (:err . "")))))))
71+
(should (equal "boom" msg))))
72+
73+
(ert-deftest port-eval-test-display-both-echoes-result-to-repl ()
74+
(let* ((session (port-eval-tests--make-session))
75+
(port-default-session session)
76+
(port-eval-display 'both)
77+
(buf (port-repl-create-buffer session)))
78+
(unwind-protect
79+
(progn
80+
(port-eval-tests--with-message
81+
(lambda ()
82+
(port-eval--display-result
83+
'((:tag . :ok) (:val . "42")
84+
(:out . "") (:err . "") (:ns . "user")))))
85+
(with-current-buffer buf
86+
(should (string-match-p "42\nuser=> "
87+
(buffer-substring-no-properties
88+
(point-min) (point-max))))))
89+
(kill-buffer buf))))
90+
91+
(ert-deftest port-eval-test-display-out-always-emitted-to-repl ()
92+
"Captured stdout should land in the REPL even in `minibuffer' mode."
93+
(let* ((session (port-eval-tests--make-session))
94+
(port-default-session session)
95+
(port-eval-display 'minibuffer)
96+
(buf (port-repl-create-buffer session)))
97+
(unwind-protect
98+
(progn
99+
(port-eval-tests--with-message
100+
(lambda ()
101+
(port-eval--display-result
102+
'((:tag . :ok) (:val . "nil")
103+
(:out . "side-effect\n") (:err . "")
104+
(:ns . "user")))))
105+
(with-current-buffer buf
106+
(should (string-match-p "side-effect\n"
107+
(buffer-substring-no-properties
108+
(point-min) (point-max))))))
109+
(kill-buffer buf))))
110+
111+
(ert-deftest port-eval-test-current-ns-prefers-buffer-ns ()
112+
"Honor `clojure-find-ns' when it returns a value."
113+
(let ((session (port-eval-tests--make-session)))
114+
(cl-letf (((symbol-function 'clojure-find-ns) (lambda () "my.ns")))
115+
(should (equal "my.ns" (port-eval--current-ns session))))))
116+
117+
(ert-deftest port-eval-test-current-ns-falls-back-to-user-conn ()
118+
"Use the user socket's tracked ns when `clojure-find-ns' returns nil."
119+
(let* ((session (port-eval-tests--make-session)))
120+
(setf (port-client-current-ns (port-session-user-conn session)) "lib.foo")
121+
(cl-letf (((symbol-function 'clojure-find-ns) (lambda () nil)))
122+
(should (equal "lib.foo" (port-eval--current-ns session))))))
123+
124+
(ert-deftest port-tooling-test-user-eval-form-construction ()
125+
"Verify the wire form sent for a `port-tooling-user-eval' call."
126+
(let* ((session (port-eval-tests--make-session))
127+
sent)
128+
(cl-letf (((symbol-function 'port-client-send)
129+
(lambda (_conn s) (setq sent s))))
130+
(port-tooling-user-eval session "my.ns" "(+ 1 2)" #'ignore))
131+
(should (string-match-p "(port\\.tooling/-user-eval [0-9]+ (quote my\\.ns)"
132+
sent))
133+
;; The code is sent as a properly quoted Clojure string literal.
134+
(should (string-match-p (regexp-quote "\"(+ 1 2)\"") sent))))
135+
136+
(ert-deftest port-tooling-test-user-eval-escapes-quotes ()
137+
"Strings containing quotes survive the Elisp -> Clojure round trip."
138+
(let* ((session (port-eval-tests--make-session))
139+
sent)
140+
(cl-letf (((symbol-function 'port-client-send)
141+
(lambda (_conn s) (setq sent s))))
142+
(port-tooling-user-eval session "user" "(println \"hi\")" #'ignore))
143+
(should (string-match-p (regexp-quote "\"(println \\\"hi\\\")\"") sent))))
144+
145+
(provide 'port-eval-tests)
146+
147+
;;; port-eval-tests.el ends here

0 commit comments

Comments
 (0)