2
2
" Expression-based debugger for clojure code"
3
3
{:author " Artur Malabarba" }
4
4
(:require
5
+ [clojure.string :as str]
5
6
[cider.nrepl.middleware.inspect :refer [swap-inspector!]]
6
7
[cider.nrepl.middleware.util :as util :refer [respond-to]]
7
8
[cider.nrepl.middleware.util.cljs :as cljs]
9
+ [cider.nrepl.middleware.util.eval]
8
10
[cider.nrepl.middleware.util.instrument :as ins]
9
11
[cider.nrepl.middleware.util.nrepl :refer [notify-client]]
10
- [nrepl.middleware.interruptible-eval :refer [*msg*]]
12
+ [nrepl.middleware.interruptible-eval :as ieval : refer [*msg*]]
11
13
[nrepl.middleware.print :as print]
12
14
[orchard.info :as info]
13
15
[orchard.inspect :as inspect]
@@ -466,6 +468,10 @@ this map (identified by a key), and will `dissoc` it afterwards."}
466
468
467
469
(def ^:dynamic *tmp-forms* (atom {}))
468
470
(def ^:dynamic *do-locals* true )
471
+ #_:clj-kondo/ignore
472
+ (def ^:dynamic ^:private *found-debugger-tag*)
473
+ #_:clj-kondo/ignore
474
+ (def ^:dynamic ^:private *top-level-form-meta*)
469
475
470
476
(defmacro with-initial-debug-bindings
471
477
" Let-wrap `body` with STATE__ map containing code, file, line, column etc.
@@ -476,17 +482,26 @@ this map (identified by a key), and will `dissoc` it afterwards."}
476
482
{:style/indent 0 }
477
483
[& body]
478
484
; ; 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})
485
+ `(let [~'STATE__ {:msg ~(if (bound? #'*top-level-form-meta*)
486
+ (let [{:keys [line column ns ], form-info ::form-info }
487
+ *top-level-form-meta*
488
+ {:keys [code file original-id]} form-info]
489
+ {:code code
490
+ ; ; Passing clojure.lang.Namespace object
491
+ ; ; as :original-ns breaks nREPL in bewildering
492
+ ; ; ways.
493
+ ; ; NOTE: column numbers in the response map
494
+ ; ; start from 1 according to Clojure.
495
+ ; ; This is not a bug and should be converted to
496
+ ; ; 0-based indexing by the client if necessary.
497
+ :original-ns (str (or ns *ns*))
498
+ :original-id original-id
499
+ :file file, :line line, :column column})
500
+ (let [{:keys [code file line column ns original-id]} *msg*]
501
+ {:code code
502
+ :original-ns (str (or ns *ns*))
503
+ :original-id original-id
504
+ :file file, :line line, :column column}))
490
505
; ; the coor of first form is used as the debugger session id
491
506
:session-id (atom nil )
492
507
:skip (atom false )
@@ -626,50 +641,59 @@ this map (identified by a key), and will `dissoc` it afterwards."}
626
641
; ;; ## Data readers
627
642
; ;
628
643
; ; Set in `src/data_readers.clj`.
644
+
645
+ (defn- found-debugger-tag []
646
+ (when (bound? #'*found-debugger-tag*)
647
+ (set! *found-debugger-tag* true )))
648
+
629
649
(defn breakpoint-reader
630
650
" #break reader. Mark `form` for breakpointing."
631
651
[form]
652
+ (found-debugger-tag )
632
653
(ins/tag-form form #'breakpoint-with-initial-debug-bindings true ))
633
654
634
655
(defn debug-reader
635
656
" #dbg reader. Mark all forms in `form` for breakpointing.
636
657
`form` itself is also marked."
637
658
[form]
659
+ (found-debugger-tag )
638
660
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-interesting)
639
661
#'breakpoint-if-interesting-with-initial-debug-bindings))
640
662
641
663
(defn break-on-exception-reader
642
664
" #exn reader. Wrap `form` in try-catch and break only on exception"
643
665
[form]
666
+ (found-debugger-tag )
644
667
(ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings true ))
645
668
646
669
(defn debug-on-exception-reader
647
670
" #dbgexn reader. Mark all forms in `form` for breakpointing on exception.
648
671
`form` itself is also marked."
649
672
[form]
673
+ (found-debugger-tag )
650
674
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-exception)
651
675
#'breakpoint-if-exception-with-initial-debug-bindings))
652
676
653
677
(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))))))
678
+ (binding [*top-level-form-meta* ( meta form)]
679
+ ( let [form1 (ins/instrument-tagged-code form)]
680
+ (try
681
+ (binding [*tmp-forms* (atom {})]
682
+ (eval form1))
683
+ (catch java.lang.RuntimeException e
684
+ (if (some #(when %
685
+ (re-matches #".*Method code too large!.*"
686
+ (.getMessage ^Throwable %)))
687
+ [e (.getCause e)])
688
+ (do (notify-client *msg*
689
+ (str " Method code too large!\n "
690
+ " Locals and evaluation in local context won't be available." )
691
+ :warning )
692
+ ; ; re-try without locals
693
+ (binding [*tmp-forms* (atom {})
694
+ *do-locals* false ]
695
+ (eval form1)))
696
+ (throw e) ))))))
673
697
674
698
(def ^:dynamic *debug-data-readers*
675
699
" Reader macros like #dbg which cause code to be instrumented when present."
@@ -701,6 +725,30 @@ this map (identified by a key), and will `dissoc` it afterwards."}
701
725
; ; If there was no reader macro, fallback on regular eval.
702
726
msg)))
703
727
728
+ (defn- maybe-debug-nrepl-1-5+
729
+ " Alternative implementation of `maybe-debug` that is only supported with nREPL
730
+ 1.5+ or higher. This version supports forms compiled by `load-file` and
731
+ doesn't perform double read like the older version."
732
+ [msg]
733
+ (let [read-fn
734
+ (fn [options reader]
735
+ (binding [*found-debugger-tag* false ]
736
+ ; ; Read the form normally and then check if the flag turned on that
737
+ ; ; tells us the form contains any debugger reader tags.
738
+ (let [[form code] (ins/comment-trimming-read+string options reader)]
739
+ (if *found-debugger-tag*
740
+ ; ; Attach the original (but cleaned up) source code for the
741
+ ; ; instrumenter to set up correct debugger state later.
742
+ (vary-meta form assoc
743
+ ::form-info {:code code
744
+ :file (:file msg)
745
+ :original-id (:id msg)})
746
+ form))))]
747
+ (assoc msg
748
+ ::ieval/read-fn read-fn
749
+ ::ieval/eval-fn (cider.nrepl.middleware.util.eval/eval-dispatcher
750
+ instrument-and-eval ::form-info ))))
751
+
704
752
(defn- initialize
705
753
" Initialize the channel used for debug-input requests."
706
754
[{:keys [:nrepl.middleware.print/options ] :as msg}]
@@ -723,7 +771,9 @@ this map (identified by a key), and will `dissoc` it afterwards."}
723
771
(case op
724
772
" eval" (do (when (instance? clojure.lang.Atom session)
725
773
(swap! session assoc #'*skip-breaks* (atom nil )))
726
- (handler (maybe-debug msg)))
774
+ (handler (if (cider.nrepl.middleware.util.nrepl/satisfies-version? 1 5 )
775
+ (maybe-debug-nrepl-1-5+ msg)
776
+ (maybe-debug msg))))
727
777
" debug-instrumented-defs" (instrumented-defs-reply msg)
728
778
" debug-input" (when-let [pro (@promises (:key msg))]
729
779
(deliver pro input))
0 commit comments