Skip to content

Commit c43b78d

Browse files
[debug] Correctly process #dbg during load-file
1 parent ceb934c commit c43b78d

File tree

7 files changed

+180
-37
lines changed

7 files changed

+180
-37
lines changed

project.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
:license {:name "Eclipse Public License"
2121
:url "http://www.eclipse.org/legal/epl-v10.html"}
2222
:scm {:name "git" :url "https://github.com/clojure-emacs/cider-nrepl"}
23-
:dependencies [[nrepl/nrepl "1.4.0" :exclusions [org.clojure/clojure]]
23+
:dependencies [[nrepl/nrepl "1.5.0-SNAPSHOT" :exclusions [org.clojure/clojure]]
2424
[cider/orchard "0.37.0" :exclusions [org.clojure/clojure]]
2525
^:inline-dep [compliment "0.7.1"]
2626
^:inline-dep [org.rksm/suitable "0.6.2" :exclusions [org.clojure/clojure

src/cider/nrepl.clj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,15 @@ Depending on the type of the return value of the evaluation this middleware may
207207
"complete-flush-caches"
208208
{:doc "Forces the completion backend to repopulate all its caches"}}}))
209209

210+
;; `wrap-debug` has to be sandwiched between `load-file` and `eval`. First
211+
;; `load-file` transforms its message into an `eval`, then `wrap-debug` attaches
212+
;; its instrumenting functions to the message, and finally `eval` does the work.
210213
(def-wrapper wrap-debug cider.nrepl.middleware.debug/handle-debug
211214
#{"eval"}
212215
(cljs/requires-piggieback
213216
{:doc "Provide instrumentation and debugging functionality."
214217
:expects #{"eval"}
215-
:requires #{#'wrap-print #'session}
218+
:requires #{#'wrap-print #'session "load-file"}
216219
:handles {"debug-input"
217220
{:doc "Read client input on debug action."
218221
:requires {"input" "The user's reply to the input request."

src/cider/nrepl/middleware/debug.clj

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
"Expression-based debugger for clojure code"
33
{:author "Artur Malabarba"}
44
(:require
5+
[clojure.string :as str]
56
[cider.nrepl.middleware.inspect :refer [swap-inspector!]]
67
[cider.nrepl.middleware.util :as util :refer [respond-to]]
78
[cider.nrepl.middleware.util.cljs :as cljs]
9+
[cider.nrepl.middleware.util.eval]
810
[cider.nrepl.middleware.util.instrument :as ins]
911
[cider.nrepl.middleware.util.nrepl :refer [notify-client]]
10-
[nrepl.middleware.interruptible-eval :refer [*msg*]]
12+
[nrepl.middleware.interruptible-eval :as ieval :refer [*msg*]]
1113
[nrepl.middleware.print :as print]
1214
[orchard.info :as info]
1315
[orchard.inspect :as inspect]
@@ -466,6 +468,8 @@ this map (identified by a key), and will `dissoc` it afterwards."}
466468

467469
(def ^:dynamic *tmp-forms* (atom {}))
468470
(def ^:dynamic *do-locals* true)
471+
(def ^:dynamic ^:private *found-debugger-tag*)
472+
(def ^:dynamic ^:private *top-level-form-meta*)
469473

470474
(defmacro with-initial-debug-bindings
471475
"Let-wrap `body` with STATE__ map containing code, file, line, column etc.
@@ -476,17 +480,26 @@ this map (identified by a key), and will `dissoc` it afterwards."}
476480
{:style/indent 0}
477481
[& body]
478482
;; NOTE: *msg* is the message that instrumented the function,
479-
`(let [~'STATE__ {:msg ~(let [{:keys [code id file line column ns]} *msg*]
480-
{:code code
481-
;; Passing clojure.lang.Namespace object
482-
;; as :original-ns breaks nREPL in bewildering
483-
;; ways.
484-
;; NOTE: column numbers in the response map
485-
;; start from 1 according to Clojure.
486-
;; This is not a bug and should be converted to
487-
;; 0-based indexing by the client if necessary.
488-
:original-id id, :original-ns (str (or ns *ns*))
489-
:file file, :line line, :column column})
483+
`(let [~'STATE__ {:msg ~(if (bound? #'*top-level-form-meta*)
484+
(let [{:keys [line column ns], form-info ::form-info}
485+
*top-level-form-meta*
486+
{:keys [code file original-id]} form-info]
487+
{:code code
488+
;; Passing clojure.lang.Namespace object
489+
;; as :original-ns breaks nREPL in bewildering
490+
;; ways.
491+
;; NOTE: column numbers in the response map
492+
;; start from 1 according to Clojure.
493+
;; This is not a bug and should be converted to
494+
;; 0-based indexing by the client if necessary.
495+
:original-ns (str (or ns *ns*))
496+
:original-id original-id
497+
:file file, :line line, :column column})
498+
(let [{:keys [code file line column ns original-id]} *msg*]
499+
{:code code
500+
:original-ns (str (or ns *ns*))
501+
:original-id original-id
502+
:file file, :line line, :column column}))
490503
;; the coor of first form is used as the debugger session id
491504
:session-id (atom nil)
492505
:skip (atom false)
@@ -626,50 +639,59 @@ this map (identified by a key), and will `dissoc` it afterwards."}
626639
;;; ## Data readers
627640
;;
628641
;; Set in `src/data_readers.clj`.
642+
643+
(defn- found-debugger-tag []
644+
(when (bound? #'*found-debugger-tag*)
645+
(set! *found-debugger-tag* true)))
646+
629647
(defn breakpoint-reader
630648
"#break reader. Mark `form` for breakpointing."
631649
[form]
650+
(found-debugger-tag)
632651
(ins/tag-form form #'breakpoint-with-initial-debug-bindings true))
633652

634653
(defn debug-reader
635654
"#dbg reader. Mark all forms in `form` for breakpointing.
636655
`form` itself is also marked."
637656
[form]
657+
(found-debugger-tag)
638658
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-interesting)
639659
#'breakpoint-if-interesting-with-initial-debug-bindings))
640660

641661
(defn break-on-exception-reader
642662
"#exn reader. Wrap `form` in try-catch and break only on exception"
643663
[form]
664+
(found-debugger-tag)
644665
(ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings true))
645666

646667
(defn debug-on-exception-reader
647668
"#dbgexn reader. Mark all forms in `form` for breakpointing on exception.
648669
`form` itself is also marked."
649670
[form]
671+
(found-debugger-tag)
650672
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-exception)
651673
#'breakpoint-if-exception-with-initial-debug-bindings))
652674

653675
(defn instrument-and-eval [form]
654-
(let [form1 (ins/instrument-tagged-code form)]
655-
;; (ins/print-form form1 true false)
656-
(try
657-
(binding [*tmp-forms* (atom {})]
658-
(eval form1))
659-
(catch java.lang.RuntimeException e
660-
(if (some #(when %
661-
(re-matches #".*Method code too large!.*"
662-
(.getMessage ^Throwable %)))
663-
[e (.getCause e)])
664-
(do (notify-client *msg*
665-
(str "Method code too large!\n"
666-
"Locals and evaluation in local context won't be available.")
667-
:warning)
668-
;; re-try without locals
669-
(binding [*tmp-forms* (atom {})
670-
*do-locals* false]
671-
(eval form1)))
672-
(throw e))))))
676+
(binding [*top-level-form-meta* (meta form)]
677+
(let [form1 (ins/instrument-tagged-code form)]
678+
(try
679+
(binding [*tmp-forms* (atom {})]
680+
(eval form1))
681+
(catch java.lang.RuntimeException e
682+
(if (some #(when %
683+
(re-matches #".*Method code too large!.*"
684+
(.getMessage ^Throwable %)))
685+
[e (.getCause e)])
686+
(do (notify-client *msg*
687+
(str "Method code too large!\n"
688+
"Locals and evaluation in local context won't be available.")
689+
:warning)
690+
;; re-try without locals
691+
(binding [*tmp-forms* (atom {})
692+
*do-locals* false]
693+
(eval form1)))
694+
(throw e)))))))
673695

674696
(def ^:dynamic *debug-data-readers*
675697
"Reader macros like #dbg which cause code to be instrumented when present."
@@ -701,6 +723,30 @@ this map (identified by a key), and will `dissoc` it afterwards."}
701723
;; If there was no reader macro, fallback on regular eval.
702724
msg)))
703725

726+
(defn- maybe-debug-nrepl-1-5+
727+
"Alternative implementation of `maybe-debug` that is only supported with nREPL
728+
1.5+ or higher. This version supports forms compiled by `load-file` and
729+
doesn't perform double read like the older version."
730+
[msg]
731+
(let [read-fn
732+
(fn [options reader]
733+
(binding [*found-debugger-tag* false]
734+
;; Read the form normally and then check if the flag turned on that
735+
;; tells us the form contains any debugger reader tags.
736+
(let [[form code] (ins/comment-trimming-read+string options reader)]
737+
(if *found-debugger-tag*
738+
;; Attach the original (but cleaned up) source code for the
739+
;; instrumenter to set up correct debugger state later.
740+
(vary-meta form assoc
741+
::form-info {:code code
742+
:file (:file msg)
743+
:original-id (:id msg)})
744+
form))))]
745+
(assoc msg
746+
::ieval/read-fn read-fn
747+
::ieval/eval-fn (cider.nrepl.middleware.util.eval/eval-dispatcher
748+
instrument-and-eval ::form-info))))
749+
704750
(defn- initialize
705751
"Initialize the channel used for debug-input requests."
706752
[{:keys [:nrepl.middleware.print/options] :as msg}]
@@ -723,7 +769,9 @@ this map (identified by a key), and will `dissoc` it afterwards."}
723769
(case op
724770
"eval" (do (when (instance? clojure.lang.Atom session)
725771
(swap! session assoc #'*skip-breaks* (atom nil)))
726-
(handler (maybe-debug msg)))
772+
(handler (if (cider.nrepl.middleware.util.nrepl/satisfies-version? 1 5)
773+
(maybe-debug-nrepl-1-5+ msg)
774+
(maybe-debug msg))))
727775
"debug-instrumented-defs" (instrumented-defs-reply msg)
728776
"debug-input" (when-let [pro (@promises (:key msg))]
729777
(deliver pro input))
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
(ns cider.nrepl.middleware.util.eval
2+
(:import clojure.lang.Compiler))
3+
4+
;; The sole reason for this namespace to exist is to prevent
5+
;; `cider.nrepl.middleware.debug/instrument-and-eval` from appearing on the
6+
;; stacktrace when we don't, in fact, compile with the debugger. Sure, this may
7+
;; seem minor, but I don't want to confuse users and send them on wild geese
8+
;; chases thinking that the debugger may be somehow related to the thrown
9+
;; exceptions when it is not enabled at all.
10+
11+
(defn eval-dispatcher [debugger-eval-fn dispatch-kw]
12+
(fn [form]
13+
(if (get (meta form) dispatch-kw)
14+
(debugger-eval-fn form)
15+
(Compiler/eval form true))))

src/cider/nrepl/middleware/util/instrument.clj

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
(:require
55
clojure.pprint
66
[clojure.walk :as walk]
7-
[orchard.meta :as m]))
7+
[orchard.meta :as m])
8+
(:import (clojure.lang LineNumberingPushbackReader)))
89

910
;;;; # Instrumentation
1011
;;;
@@ -382,6 +383,52 @@
382383
(filter (comp :cider/instrumented meta second))
383384
(map first))))
384385

386+
;; Utilities for correctly mapping the read form to its string content. Default
387+
;; Clojure `read+string` returns leading comments and other garbage as if it is
388+
;; part of the read form, making our lives harder when we want to align that
389+
;; with the content of Emacs buffer.
390+
391+
(defn- skip-n-lines
392+
"Find the character offset where the nth line starts (after n newlines)."
393+
[s lines-to-skip]
394+
(try
395+
(let [matcher (re-matcher #"\r?\n" s)]
396+
(dotimes [_ lines-to-skip] (re-find matcher))
397+
(.end matcher))
398+
(catch IllegalStateException _ 0)))
399+
400+
(defn- trim-to-form
401+
"Trim captured string from reader start position to actual form start position"
402+
[captured-string start-line start-col form-line form-col]
403+
(if (and (= start-line form-line) (= start-col form-col))
404+
;; No trimming needed - form starts exactly where we started reading
405+
captured-string
406+
;; Need to trim: walk through the string counting lines and columns
407+
(let [lines-to-skip (- form-line start-line)
408+
final-offset (if (= lines-to-skip 0)
409+
(- form-col start-col)
410+
(+ (skip-n-lines captured-string lines-to-skip)
411+
(dec form-col)))] ;; 1-based to 0-based
412+
(subs captured-string final-offset))))
413+
414+
(defn comment-trimming-read+string
415+
"Like `read+string` but trims comments and skipped forms from the string result,
416+
thus only returning the string that actually backs the read form, without the
417+
cruft could be before it."
418+
[opts, ^LineNumberingPushbackReader reader]
419+
(let [start-line (.getLineNumber reader)
420+
start-col (.getColumnNumber reader)]
421+
(.captureString reader)
422+
(let [form (read opts reader)
423+
captured-string (.getString reader)
424+
{:keys [line column]} (meta form)
425+
;; If form has line/column metadata, trim the captured string.
426+
trimmed-string (if (and line column)
427+
(trim-to-form captured-string start-line start-col
428+
line column)
429+
captured-string)]
430+
[form trimmed-string])))
431+
385432
;;; Instrumentation test support
386433
;;;
387434
;;; This code migrated out of the test namespace to avoid a dependency

src/cider/nrepl/middleware/util/nrepl.clj

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
(ns cider.nrepl.middleware.util.nrepl
2-
"Common utilities for interaction with the client."
2+
"Common nREPL-related utilities."
33
(:require
44
[nrepl.middleware.interruptible-eval :refer [*msg*]]
55
[nrepl.misc :refer [response-for]]
6-
[nrepl.transport :as transport]))
6+
[nrepl.transport :as transport]
7+
[nrepl.version :refer [version]]))
8+
9+
(defn satisfies-version?
10+
"Check if the nREPL version is of the provided major and minor parts or newer."
11+
[major minor]
12+
(>= (compare ((juxt :major :minor) version) [major minor]) 0))
13+
14+
#_(satisfies-version? 0 9)
15+
#_(satisfies-version? 1 10)
716

817
(defn notify-client
918
"Send user level notification to client as a response to request `msg`.

test/clj/cider/nrepl/middleware/debug_integration_test.clj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@
9898
(defmethod debugger-send :eval [_ code]
9999
(nrepl-send {:op "eval" :code code}))
100100

101+
(defmethod debugger-send :load-file [_ code]
102+
(nrepl-send {:op "load-file", :file code
103+
:file-path "path/to/file.clj", :file-name "file.clj"}))
104+
101105
(defmacro def-debug-op [op]
102106
`(defmethod debugger-send ~op [_#]
103107
(nrepl-send {:op "debug-input" :input ~(str op) :key (current-key)})))
@@ -712,3 +716,20 @@
712716
(--> :continue-all)
713717
(<-- {:value "{:transport 23}"})
714718
(<-- {:status ["done"]}))
719+
720+
(deftest load-file-enables-debugger-test
721+
(--> :load-file ";; comments before form
722+
#_(redundant stuff)
723+
(defn foo [a b] #dbg (+ a b))")
724+
(<-- {:value "#'user/foo"})
725+
(<-- {:status ["done"]})
726+
727+
(--> :eval "(foo 4 5)")
728+
(<-- {:debug-value "4" :coor [3 1]
729+
:code "(defn foo [a b] #dbg (+ a b))"})
730+
(--> :next)
731+
(<-- {:debug-value "5" :coor [3 2]})
732+
(--> :next)
733+
(<-- {:debug-value "9" :coor [3]})
734+
(--> :next)
735+
(<-- {:value "9"}))

0 commit comments

Comments
 (0)