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,8 @@ 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
+ (def ^:dynamic ^:private *found-debugger-tag*)
472
+ (def ^:dynamic ^:private *top-level-form-meta*)
469
473
470
474
(defmacro with-initial-debug-bindings
471
475
" 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."}
476
480
{:style/indent 0 }
477
481
[& body]
478
482
; ; 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}))
490
503
; ; the coor of first form is used as the debugger session id
491
504
:session-id (atom nil )
492
505
:skip (atom false )
@@ -626,50 +639,59 @@ this map (identified by a key), and will `dissoc` it afterwards."}
626
639
; ;; ## Data readers
627
640
; ;
628
641
; ; 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
+
629
647
(defn breakpoint-reader
630
648
" #break reader. Mark `form` for breakpointing."
631
649
[form]
650
+ (found-debugger-tag )
632
651
(ins/tag-form form #'breakpoint-with-initial-debug-bindings true ))
633
652
634
653
(defn debug-reader
635
654
" #dbg reader. Mark all forms in `form` for breakpointing.
636
655
`form` itself is also marked."
637
656
[form]
657
+ (found-debugger-tag )
638
658
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-interesting)
639
659
#'breakpoint-if-interesting-with-initial-debug-bindings))
640
660
641
661
(defn break-on-exception-reader
642
662
" #exn reader. Wrap `form` in try-catch and break only on exception"
643
663
[form]
664
+ (found-debugger-tag )
644
665
(ins/tag-form form #'breakpoint-if-exception-with-initial-debug-bindings true ))
645
666
646
667
(defn debug-on-exception-reader
647
668
" #dbgexn reader. Mark all forms in `form` for breakpointing on exception.
648
669
`form` itself is also marked."
649
670
[form]
671
+ (found-debugger-tag )
650
672
(ins/tag-form (ins/tag-form-recursively form #'breakpoint-if-exception)
651
673
#'breakpoint-if-exception-with-initial-debug-bindings))
652
674
653
675
(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) ))))))
673
695
674
696
(def ^:dynamic *debug-data-readers*
675
697
" 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."}
701
723
; ; If there was no reader macro, fallback on regular eval.
702
724
msg)))
703
725
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
+
704
750
(defn- initialize
705
751
" Initialize the channel used for debug-input requests."
706
752
[{:keys [:nrepl.middleware.print/options ] :as msg}]
@@ -723,7 +769,9 @@ this map (identified by a key), and will `dissoc` it afterwards."}
723
769
(case op
724
770
" eval" (do (when (instance? clojure.lang.Atom session)
725
771
(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))))
727
775
" debug-instrumented-defs" (instrumented-defs-reply msg)
728
776
" debug-input" (when-let [pro (@promises (:key msg))]
729
777
(deliver pro input))
0 commit comments