From 20d3ff180c718d635b13b0dd87a2b971e92475cc Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Sat, 16 May 2026 20:09:05 +0800 Subject: [PATCH 1/7] Separate value and stderr in eval-to-comment output The pprint-to-comment handler accumulated stderr into the same variable as the eval value, so when nREPL response chunks interleaved (e.g. a reflection warning arriving between value chunks), the result comment would contain garbled output. Accumulate them separately and compose at on-done: whichever is present alone, or value followed by a newline-separated trimmed stderr block. --- lisp/cider-eval.el | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lisp/cider-eval.el b/lisp/cider-eval.el index edc43bc45..aa530051e 100644 --- a/lisp/cider-eval.el +++ b/lisp/cider-eval.el @@ -437,14 +437,15 @@ LOCATION is the location marker at which to insert. COMMENT-PREFIX is the comment prefix for the first line of output. CONTINUED-PREFIX is the comment prefix to use for the remaining lines. COMMENT-POSTFIX is the text to output after the last line." - (let ((res "")) + (let ((res "") + (stderr "")) (cider-make-eval-handler :buffer buffer :on-value (lambda (value) (setq res (concat res value))) ;; Forward stdout to the usual interactive sink so output like the ;; `time' macro's "Elapsed time" line isn't silently dropped (#3732). :on-stdout #'cider-emit-interactive-eval-output - :on-stderr (lambda (err) (setq res (concat res err))) + :on-stderr (lambda (err) (setq stderr (concat stderr err))) :on-done (lambda () (with-current-buffer buffer (save-excursion @@ -452,7 +453,11 @@ COMMENT-POSTFIX is the text to output after the last line." ;; edge case: defun at eob (unless (bolp) (insert "\n")) (cider-maybe-insert-multiline-comment - res comment-prefix continued-prefix comment-postfix))) + (if (or (string-blank-p res) + (string-blank-p stderr)) + (string-trim (concat res stderr)) + (concat res "\n" (string-trim stderr))) + comment-prefix continued-prefix comment-postfix))) (cider--maybe-set-eval-register res))))) (defun cider-popup-eval-handler (&optional buffer _bounds source-buffer) From 232c8d093af8a15b9d81e990ebae0c05945f468e Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Mon, 22 Jun 2026 19:18:36 +0800 Subject: [PATCH 2/7] Add tests --- test/cider-eval-tests.el | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/test/cider-eval-tests.el b/test/cider-eval-tests.el index 2ef8256ff..cbc809dc5 100644 --- a/test/cider-eval-tests.el +++ b/test/cider-eval-tests.el @@ -39,7 +39,48 @@ (current-buffer) (point-marker) ";; => " ";; " ""))) (funcall handler (nrepl-dict "out" "Elapsed time: 0.042 msecs\n")) (expect 'cider-emit-interactive-eval-output - :to-have-been-called-with "Elapsed time: 0.042 msecs\n"))))) + :to-have-been-called-with "Elapsed time: 0.042 msecs\n")))) + + ;; When eval produces both a value and stderr output (e.g. a reflection warning), + ;; the two must not be interleaved in the inserted comment. + (it "keeps value and stderr separate when both are present" + (with-temp-buffer + (setq-local nrepl-pending-requests (make-hash-table :test 'equal)) + (let* ((indent-line-function #'ignore) + (handler (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) (point-marker) ";; => " ";; " ""))) + ;; Simulate nREPL responses: stderr chunk interleaved with value chunks + (funcall handler (nrepl-dict "value" "{:a 1, ")) + (funcall handler (nrepl-dict "err" "Reflection warning, user.clj:5:1 - reference to field foo can't be resolved.\n")) + (funcall handler (nrepl-dict "value" ":b 2}")) + (funcall handler (nrepl-dict "status" '("done"))) + ;; Value and stderr should be separated by a newline, not mashed together + (expect (string-trim (buffer-string)) + :to-equal + (concat ";; => {:a 1, :b 2}\n" + ";; Reflection warning, user.clj:5:1 - reference to field foo can't be resolved."))))) + + (it "inserts only the value when no stderr is produced" + (with-temp-buffer + (setq-local nrepl-pending-requests (make-hash-table :test 'equal)) + (let* ((indent-line-function #'ignore) + (handler (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) (point-marker) ";; => " ";; " ""))) + (funcall handler (nrepl-dict "value" "42")) + (funcall handler (nrepl-dict "status" '("done"))) + (expect (string-trim (buffer-string)) :to-equal ";; => 42")))) + + (it "inserts only stderr when no value is produced" + (with-temp-buffer + (setq-local nrepl-pending-requests (make-hash-table :test 'equal)) + (let* ((indent-line-function #'ignore) + (handler (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) (point-marker) ";; => " ";; " ""))) + (funcall handler (nrepl-dict "err" "Syntax error compiling at (user.clj:5:1)\nUnable to resolve symbol: oops in this context\n")) + (funcall handler (nrepl-dict "status" '("done"))) + (expect (string-trim (buffer-string)) :to-equal + (concat ";; => Syntax error compiling at (user.clj:5:1)\n" + ";; Unable to resolve symbol: oops in this context")))))) (describe "cider--comment-format" (it "returns the configured prefixes for the `line' style" From 297101a64605a9c0c5c7a081277449e433e31bc7 Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Mon, 22 Jun 2026 12:30:22 +0000 Subject: [PATCH 3/7] Coerce insertion location to marker inside eval-to-comment handler Move the integer->marker coercion from the callsites into the handler itself via copy-marker, so the handler's contract accepts both types. No external facing change. Add a unit test covering the existing behavior (introduced in #2607) --- lisp/cider-eval.el | 7 ++++--- test/cider-eval-tests.el | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lisp/cider-eval.el b/lisp/cider-eval.el index aa530051e..eba22b178 100644 --- a/lisp/cider-eval.el +++ b/lisp/cider-eval.el @@ -438,7 +438,8 @@ COMMENT-PREFIX is the comment prefix for the first line of output. CONTINUED-PREFIX is the comment prefix to use for the remaining lines. COMMENT-POSTFIX is the text to output after the last line." (let ((res "") - (stderr "")) + (stderr "") + (location (copy-marker location))) (cider-make-eval-handler :buffer buffer :on-value (lambda (value) (setq res (concat res value))) @@ -716,7 +717,7 @@ With the prefix arg INSERT-BEFORE, insert before the form, otherwise afterwards. (cider-interactive-eval nil (cider-eval-print-with-comment-handler (current-buffer) - (set-marker (make-marker) insertion-point) + insertion-point prefix postfix) bounds @@ -741,7 +742,7 @@ If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards." (cider-interactive-eval nil (cider-eval-pprint-with-multiline-comment-handler (current-buffer) - (set-marker (make-marker) insertion-point) + insertion-point prefix continued comment-postfix) diff --git a/test/cider-eval-tests.el b/test/cider-eval-tests.el index cbc809dc5..e1758a17d 100644 --- a/test/cider-eval-tests.el +++ b/test/cider-eval-tests.el @@ -41,6 +41,24 @@ (expect 'cider-emit-interactive-eval-output :to-have-been-called-with "Elapsed time: 0.042 msecs\n")))) + (it "tracks async buffer edits using markers (#2607)" + (with-temp-buffer + (setq-local nrepl-pending-requests (make-hash-table :test 'equal)) + (insert "(+ 1 2)") + (let* ((indent-line-function #'ignore) + (insertion-point (point-max)) + (handler (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) insertion-point ";; => " ";; " ""))) + ;; simulate user editing BEFORE the eval result arrives: + (goto-char 1) + (insert "(ns repro)\n\n") + (funcall handler (nrepl-dict "value" "3")) + (funcall handler (nrepl-dict "status" '("done"))) + (expect (buffer-string) :to-equal + (concat "(ns repro)\n\n" + "(+ 1 2)\n" + ";; => 3"))))) + ;; When eval produces both a value and stderr output (e.g. a reflection warning), ;; the two must not be interleaved in the inserted comment. (it "keeps value and stderr separate when both are present" From b7611d858533cdced5609c0c19a9a37852b3f312 Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Fri, 20 Mar 2020 00:00:26 +0800 Subject: [PATCH 4/7] Replace previous eval-to-comment result on re-eval Add an optional REPLACE arg to the multiline-comment eval handler and to cider-pprint-form-to-comment. When set, cider-maybe-delete-multiline-comment removes the previously-inserted result region (both ;; line comments and discarded #_/(comment ...) forms) before the new result is inserted, so re-evaluating a form updates its comment in place instead of stacking. cider-maybe-insert-multiline-comment now returns the bounds of the inserted text (nil when nothing was inserted), and preserves blank lines in pretty-printed output. --- lisp/cider-eval.el | 62 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/lisp/cider-eval.el b/lisp/cider-eval.el index eba22b178..f9d70e436 100644 --- a/lisp/cider-eval.el +++ b/lisp/cider-eval.el @@ -412,28 +412,65 @@ result." (insert (concat comment-prefix res (or comment-postfix "") "\n")))) (cider--maybe-set-eval-register res))))) +(defun cider-maybe-delete-multiline-comment (comment-prefix continued-prefix comment-postfix) + "Delete the region after the point in the form of a multiline comment. +The region must begin with COMMENT-PREFIX, followed by multiple lines +beginning with CONTINUED-PREFIX at the same indentation, +and optionally end with COMMENT-POSTFIX." + (let ((pre-rgx (rx (group-n 1 (* (any " \t"))) ;; may be indented + (literal comment-prefix)))) + (when (looking-at pre-rgx) + (let* ((cont-rgx (rx (literal (match-string 1)) ;; match indentation rigidly + (literal continued-prefix))) + (post-rgx (rx (literal (match-string 1)) + ;; trim the leading newline, NOTE string-trim messes with match data + (literal (if (string-prefix-p "\n" comment-postfix) + (substring comment-postfix 1) + comment-postfix)))) + (start (point)) + (end (save-excursion + (goto-char (match-end 0)) + (if (string-prefix-p ";" comment-prefix) + ;; line comment - skip past any similarly indented comments + (progn + (forward-line 1) + (while (and (not (eobp)) + (looking-at-p cont-rgx)) + (forward-line 1))) + ;; otherwise it's probably some sort of discarded form like #_ + (clojure-forward-logical-sexp 1)) + (if (looking-at post-rgx) ;; skip past postfix + (match-end 0) + (point))))) + (delete-region start end))))) + (defun cider-maybe-insert-multiline-comment (result comment-prefix continued-prefix comment-postfix) "Insert eval RESULT at current location if RESULT is not empty. +Returns bounds of the inserted text, or nil if nothing was inserted. RESULT will be preceded by COMMENT-PREFIX. CONTINUED-PREFIX is inserted for each additional line of output. COMMENT-POSTFIX is inserted after final text output." (unless (string= result "") + ;; Make sure inserted comments are indented properly (indent-according-to-mode) - (let ((lines (split-string result "[\n]+" t)) + (let ((lines (split-string result "[\n]" nil)) (beg (point)) (col (current-indentation))) ;; only the first line gets the normal comment-prefix (insert (concat comment-prefix (pop lines))) (dolist (elem lines) (insert (concat "\n" continued-prefix elem))) - (indent-rigidly beg (point) col) - (unless (string= comment-postfix "") - (insert comment-postfix))))) + (insert comment-postfix) + (indent-rigidly beg (line-end-position) col) + (list beg (point))))) -(defun cider-eval-pprint-with-multiline-comment-handler (buffer location comment-prefix continued-prefix comment-postfix) +(defun cider-eval-pprint-with-multiline-comment-handler (buffer location comment-prefix continued-prefix comment-postfix + &optional replace) "Make a handler for evaluating and inserting results in BUFFER. The inserted text is pretty-printed and region will be commented. LOCATION is the location marker at which to insert. +If REPLACE is non-nil, delete any following region which matches the form of a +previously inserted result. COMMENT-PREFIX is the comment prefix for the first line of output. CONTINUED-PREFIX is the comment prefix to use for the remaining lines. COMMENT-POSTFIX is the text to output after the last line." @@ -451,6 +488,10 @@ COMMENT-POSTFIX is the text to output after the last line." (with-current-buffer buffer (save-excursion (goto-char (marker-position location)) + ;; Replace an existing eval comment on the following line + (when replace + (when (eolp) (forward-line 1)) + (cider-maybe-delete-multiline-comment comment-prefix continued-prefix comment-postfix)) ;; edge case: defun at eob (unless (bolp) (insert "\n")) (cider-maybe-insert-multiline-comment @@ -723,7 +764,7 @@ With the prefix arg INSERT-BEFORE, insert before the form, otherwise afterwards. bounds (cider--nrepl-pr-request-plist)))) -(defun cider-pprint-form-to-comment (form-fn insert-before) +(defun cider-pprint-form-to-comment (form-fn insert-before &optional replace) "Evaluate the form selected by FORM-FN and insert result as comment. FORM-FN can be either `cider-last-sexp' or `cider-defun-at-point'. @@ -731,21 +772,22 @@ The comment style is controlled by `cider-comment-style'. For the default `line' style the formatting is further controlled via the `cider-comment-prefix', `cider-comment-continued-prefix' and `cider-comment-postfix' options. -If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards." +If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards. +If REPLACE is non-nil, attempts to replace a previous eval comment." (pcase-let* ((bounds (funcall form-fn 'bounds)) (insertion-point (nth (if insert-before 0 1) bounds)) (`(,prefix ,continued ,postfix) (cider--comment-format)) ;; when insert-before, we need a newline after the output to ;; avoid commenting the first line of the form - (comment-postfix (concat postfix - (if insert-before "\n" "")))) + (comment-postfix (concat postfix (if insert-before "\n" "")))) (cider-interactive-eval nil (cider-eval-pprint-with-multiline-comment-handler (current-buffer) insertion-point prefix continued - comment-postfix) + comment-postfix + replace) bounds (cider--nrepl-print-request-plist fill-column)))) From d1c4005009405bd9e1d22f1170ffa03da549acfd Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Tue, 23 Jun 2026 00:10:32 +0800 Subject: [PATCH 5/7] Add defcustom --- lisp/cider-eval.el | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lisp/cider-eval.el b/lisp/cider-eval.el index f9d70e436..38faab50c 100644 --- a/lisp/cider-eval.el +++ b/lisp/cider-eval.el @@ -167,6 +167,12 @@ the eval-to-comment handlers." cider-comment-continued-prefix cider-comment-postfix)))) +(defcustom cider-replace-comment t + "Whether to replace an existing eval comment." + :type 'boolean + :group 'cider + :package-version '(cider . "1.23.0")) + (defcustom cider-eval-register ?e "The text register assigned to the most recent evaluation result. When non-nil, the return value of all CIDER eval commands are @@ -798,7 +804,7 @@ The comment style is controlled by `cider-comment-style'. If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards." (interactive "P") - (cider-pprint-form-to-comment 'cider-last-sexp insert-before)) + (cider-pprint-form-to-comment 'cider-last-sexp insert-before cider-replace-comment)) (defun cider-pprint-eval-defun-to-comment (&optional insert-before) "Evaluate the \"top-level\" form and insert result as comment. @@ -807,7 +813,7 @@ The comment style is controlled by `cider-comment-style'. If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards." (interactive "P") - (cider-pprint-form-to-comment 'cider-defun-at-point insert-before)) + (cider-pprint-form-to-comment 'cider-defun-at-point insert-before cider-replace-comment)) (defun cider--last-comment-form-bounds () "Return the bounds of the last top-level `comment' form in the buffer. From 087664ff33ba0829e8a9637f17fabca401d606eb Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Tue, 23 Jun 2026 00:03:21 +0800 Subject: [PATCH 6/7] Replace eval comments unconditionally --- lisp/cider-eval.el | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/lisp/cider-eval.el b/lisp/cider-eval.el index 38faab50c..82af847da 100644 --- a/lisp/cider-eval.el +++ b/lisp/cider-eval.el @@ -167,12 +167,6 @@ the eval-to-comment handlers." cider-comment-continued-prefix cider-comment-postfix)))) -(defcustom cider-replace-comment t - "Whether to replace an existing eval comment." - :type 'boolean - :group 'cider - :package-version '(cider . "1.23.0")) - (defcustom cider-eval-register ?e "The text register assigned to the most recent evaluation result. When non-nil, the return value of all CIDER eval commands are @@ -470,13 +464,11 @@ COMMENT-POSTFIX is inserted after final text output." (indent-rigidly beg (line-end-position) col) (list beg (point))))) -(defun cider-eval-pprint-with-multiline-comment-handler (buffer location comment-prefix continued-prefix comment-postfix - &optional replace) +(defun cider-eval-pprint-with-multiline-comment-handler (buffer location comment-prefix continued-prefix comment-postfix) "Make a handler for evaluating and inserting results in BUFFER. The inserted text is pretty-printed and region will be commented. LOCATION is the location marker at which to insert. -If REPLACE is non-nil, delete any following region which matches the form of a -previously inserted result. +Any existing eval comment on the following line is replaced. COMMENT-PREFIX is the comment prefix for the first line of output. CONTINUED-PREFIX is the comment prefix to use for the remaining lines. COMMENT-POSTFIX is the text to output after the last line." @@ -495,9 +487,8 @@ COMMENT-POSTFIX is the text to output after the last line." (save-excursion (goto-char (marker-position location)) ;; Replace an existing eval comment on the following line - (when replace - (when (eolp) (forward-line 1)) - (cider-maybe-delete-multiline-comment comment-prefix continued-prefix comment-postfix)) + (when (eolp) (forward-line 1)) + (cider-maybe-delete-multiline-comment comment-prefix continued-prefix comment-postfix) ;; edge case: defun at eob (unless (bolp) (insert "\n")) (cider-maybe-insert-multiline-comment @@ -770,7 +761,7 @@ With the prefix arg INSERT-BEFORE, insert before the form, otherwise afterwards. bounds (cider--nrepl-pr-request-plist)))) -(defun cider-pprint-form-to-comment (form-fn insert-before &optional replace) +(defun cider-pprint-form-to-comment (form-fn insert-before) "Evaluate the form selected by FORM-FN and insert result as comment. FORM-FN can be either `cider-last-sexp' or `cider-defun-at-point'. @@ -779,7 +770,7 @@ The comment style is controlled by `cider-comment-style'. For the default `cider-comment-continued-prefix' and `cider-comment-postfix' options. If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards. -If REPLACE is non-nil, attempts to replace a previous eval comment." +Any existing eval comment is replaced." (pcase-let* ((bounds (funcall form-fn 'bounds)) (insertion-point (nth (if insert-before 0 1) bounds)) (`(,prefix ,continued ,postfix) (cider--comment-format)) @@ -792,8 +783,7 @@ If REPLACE is non-nil, attempts to replace a previous eval comment." insertion-point prefix continued - comment-postfix - replace) + comment-postfix) bounds (cider--nrepl-print-request-plist fill-column)))) @@ -804,7 +794,7 @@ The comment style is controlled by `cider-comment-style'. If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards." (interactive "P") - (cider-pprint-form-to-comment 'cider-last-sexp insert-before cider-replace-comment)) + (cider-pprint-form-to-comment 'cider-last-sexp insert-before)) (defun cider-pprint-eval-defun-to-comment (&optional insert-before) "Evaluate the \"top-level\" form and insert result as comment. @@ -813,7 +803,7 @@ The comment style is controlled by `cider-comment-style'. If INSERT-BEFORE is non-nil, insert before the form, otherwise afterwards." (interactive "P") - (cider-pprint-form-to-comment 'cider-defun-at-point insert-before cider-replace-comment)) + (cider-pprint-form-to-comment 'cider-defun-at-point insert-before)) (defun cider--last-comment-form-bounds () "Return the bounds of the last top-level `comment' form in the buffer. From b0853028c9e0b7171432993bf984551eb3fb9b87 Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Tue, 23 Jun 2026 00:00:50 +0800 Subject: [PATCH 7/7] Add tests --- test/cider-eval-tests.el | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/test/cider-eval-tests.el b/test/cider-eval-tests.el index e1758a17d..76d8b0341 100644 --- a/test/cider-eval-tests.el +++ b/test/cider-eval-tests.el @@ -98,7 +98,49 @@ (funcall handler (nrepl-dict "status" '("done"))) (expect (string-trim (buffer-string)) :to-equal (concat ";; => Syntax error compiling at (user.clj:5:1)\n" - ";; Unable to resolve symbol: oops in this context")))))) + ";; Unable to resolve symbol: oops in this context"))))) + + (it "replaces an existing multiline comment on re-eval" + (with-temp-buffer + (setq-local nrepl-pending-requests (make-hash-table :test 'equal)) + (let ((indent-line-function #'ignore)) + (insert "(inc 0)") + (let ((h (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) (point) ";; => " ";; " ""))) + (funcall h (nrepl-dict "value" "1")) + (funcall h (nrepl-dict "status" '("done"))) + (expect (buffer-string) :to-equal "(inc 0)\n;; => 1")) + ;; Simulate an edit and re-eval + (goto-char (point-min)) + (kill-line) + (insert "(dec 3)") + (let ((h (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) (point) ";; => " ";; " ""))) + (funcall h (nrepl-dict "value" "2")) + (funcall h (nrepl-dict "status" '("done"))) + (expect (buffer-string) :to-equal "(dec 3)\n;; => 2"))))) + + (it "replaces a single-line comment with a multiline result" + (with-temp-buffer + (setq-local nrepl-pending-requests (make-hash-table :test 'equal)) + (let ((indent-line-function #'ignore) + loc) + (insert "(range 3)") + (setq loc (point)) + (let ((h1 (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) loc ";; => " ";; " ""))) + (funcall h1 (nrepl-dict "value" "(0 1 2)")) + (funcall h1 (nrepl-dict "status" '("done"))) + (expect (buffer-string) :to-equal "(range 3)\n;; => (0 1 2)")) + (let ((h2 (cider-eval-pprint-with-multiline-comment-handler + (current-buffer) loc ";; => " ";; " ""))) + (funcall h2 (nrepl-dict "value" "(0\n 1\n 2)")) + (funcall h2 (nrepl-dict "status" '("done"))) + (expect (buffer-string) :to-equal + (concat "(range 3)\n" + ";; => (0\n" + ";; 1\n" + ";; 2)"))))))) (describe "cider--comment-format" (it "returns the configured prefixes for the `line' style"