|
3 | 3 | {:author "Bozhidar Batsov"}
|
4 | 4 | (:require
|
5 | 5 | [cider.nrepl.middleware.util.cljs :as cljs]
|
6 |
| - [cider.nrepl.middleware.util.error-handling :refer [with-safe-transport]] |
| 6 | + [cider.nrepl.middleware.util.error-handling |
| 7 | + :refer [base-error-response eval-interceptor-transport with-safe-transport]] |
7 | 8 | [orchard.cljs.analysis :as cljs-ana]
|
| 9 | + [nrepl.misc :refer [response-for]] |
| 10 | + [nrepl.transport :as transport] |
8 | 11 | [clojure.pprint :as pp]
|
9 | 12 | [clojure.tools.reader :as reader]
|
10 | 13 | [clojure.walk :as walk]
|
11 | 14 | [orchard.misc :as misc])
|
12 | 15 | (:import
|
13 | 16 | [clojure.lang Var]))
|
14 | 17 |
|
| 18 | +;; Macroexpansion for CLJ and CLJS follow two separate paths. In CLJ, a special |
| 19 | +;; macroexpanding code is formatted and passed down as `eval` message, then the |
| 20 | +;; result is intercepted, processed, and returned. In CLJS, the middleware |
| 21 | +;; itself performs the expansion as there is no way to macroexpand the |
| 22 | +;; expression with the evaluating part of CLJS. |
| 23 | + |
15 | 24 | ;; Common helpers
|
16 | 25 |
|
17 | 26 | (defn- var->namespace-qualified-name [^Var v]
|
18 | 27 | (symbol (str (.name (.ns v)))
|
19 | 28 | (str (.sym v))))
|
20 | 29 |
|
21 |
| -(defn- expandable? |
22 |
| - "Return true if form is macro-expandable." |
23 |
| - [form] |
24 |
| - (not= (macroexpand-1 form) form)) |
25 |
| - |
26 | 30 | (defn macroexpand-step
|
27 | 31 | "Walk form, expanding the next subform."
|
28 | 32 | [form]
|
29 | 33 | (let [expanded? (atom false)]
|
30 | 34 | (walk/prewalk (fn [x]
|
31 |
| - (if (and (not @expanded?) |
32 |
| - (expandable? x)) |
33 |
| - (do (reset! expanded? true) |
34 |
| - (macroexpand-1 x)) |
| 35 | + (if-not @expanded? |
| 36 | + (let [x' (macroexpand-1 x)] |
| 37 | + (when (not= x x') |
| 38 | + (reset! expanded? true)) |
| 39 | + x') |
35 | 40 | x))
|
36 | 41 | form)))
|
37 | 42 |
|
|
68 | 73 | (and (symbol? x) (namespace x))
|
69 | 74 | (tidy-namespaced-sym ns aliases refers))))
|
70 | 75 |
|
| 76 | +(defn- macroexpansion-response-map |
| 77 | + [{:keys [print-meta] :as msg} expanded-form] |
| 78 | + (let [expansion (with-out-str |
| 79 | + (binding [*print-meta* (boolean print-meta)] |
| 80 | + (pp/write expanded-form :dispatch pp/code-dispatch)))] |
| 81 | + {:expansion expansion})) |
| 82 | + |
71 | 83 | ;; Clojure impl
|
72 | 84 |
|
73 | 85 | (defn- resolve-expander-clj
|
74 |
| - "Returns the macroexpansion fn for macroexpanding Clojure code, corresponding |
75 |
| - to the given value of the :expander option." |
| 86 | + "Returns the qualified macroexpansion fn symbol for macroexpanding Clojure code, |
| 87 | + corresponding to the given value of the :expander option." |
76 | 88 | [expander]
|
77 | 89 | (case expander
|
78 |
| - "macroexpand-1" macroexpand-1 |
79 |
| - "macroexpand" macroexpand |
80 |
| - "macroexpand-all" walk/macroexpand-all |
81 |
| - "macroexpand-step" macroexpand-step |
| 90 | + "macroexpand-1" `macroexpand-1 |
| 91 | + "macroexpand" `macroexpand |
| 92 | + "macroexpand-all" `walk/macroexpand-all |
| 93 | + "macroexpand-step" `macroexpand-step |
82 | 94 | (throw (IllegalArgumentException. (format "Unrecognized expander: %s" expander)))))
|
83 | 95 |
|
84 | 96 | (defn- tidy-walker-clj
|
|
108 | 120 | "tidy" (tidy-walker-clj msg)
|
109 | 121 | (throw (IllegalArgumentException. (format "Unrecognized value for display-namespaces: %s" display-namespaces)))))
|
110 | 122 |
|
111 |
| -(defn- expand-clj |
112 |
| - "Returns the macroexpansion of the given Clojure form :code, performed in the |
113 |
| - context of the given :ns, using the provided :expander and :display-namespaces |
114 |
| - options." |
115 |
| - [{:keys [code expander ns] :as msg}] |
116 |
| - ;; Bind LOADER, which can be unbound under certain code paths, particularly when using tools.deps: |
117 |
| - (with-bindings {clojure.lang.Compiler/LOADER |
118 |
| - (if (instance? clojure.lang.Var$Unbound |
119 |
| - @clojure.lang.Compiler/LOADER) |
120 |
| - (clojure.lang.DynamicClassLoader. (clojure.lang.RT/baseLoader)) |
121 |
| - @clojure.lang.Compiler/LOADER)} |
122 |
| - (->> (let [expander-fn (resolve-expander-clj expander)] |
123 |
| - (binding [*ns* (find-ns ns)] |
124 |
| - (expander-fn (read-string code)))) |
125 |
| - (walk/prewalk (post-expansion-walker-clj msg))))) |
| 123 | +(defn- send-middleware-error [msg ex] |
| 124 | + (transport/send (:transport msg) |
| 125 | + (base-error-response msg ex :done :macroexpand-error))) |
| 126 | + |
| 127 | +(defn macroexpansion-reply-clj [{:keys [transport] :as msg} |
| 128 | + {:keys [value] :as resp}] |
| 129 | + (try (let [msg (update msg :ns #(or (misc/as-sym %) 'user)) |
| 130 | + expansion (walk/prewalk (post-expansion-walker-clj msg) value) |
| 131 | + response-map (macroexpansion-response-map msg expansion)] |
| 132 | + (transport/send transport (response-for msg response-map))) |
| 133 | + (catch Exception ex |
| 134 | + (send-middleware-error msg ex)))) |
| 135 | + |
| 136 | +(defn handle-macroexpand-clj |
| 137 | + "Substitute the incoming `macroexpand` message with an `eval` message that will |
| 138 | + perform the expansion for us, intercept the result with |
| 139 | + `eval-interceptor-transport`, post-process it and return back to the client. |
| 140 | +
|
| 141 | + Delegating the actual expansion (and even reading the code string) to `eval` |
| 142 | + op is preferable because it ensures that the context (state of dynamic |
| 143 | + variables) for macroexpansion is identical to if the user called `(macroexpand |
| 144 | + ...)` manually at the REPL." |
| 145 | + [handler {:keys [code expander ns] :as msg}] |
| 146 | + ;; `try` is not around the handler but only around constructing the msg |
| 147 | + ;; because we don't want to catch an error from some underlying middleware. |
| 148 | + (let [msg |
| 149 | + (try (let [expander-fn (resolve-expander-clj expander) |
| 150 | + ;; Glueing strings may seem ugly, but this is the most |
| 151 | + ;; reliable way to ensure the macroexpansion fully happens in |
| 152 | + ;; a correct context. |
| 153 | + expander-code (format "(%s '%s)" expander-fn code) |
| 154 | + transport (eval-interceptor-transport |
| 155 | + msg macroexpansion-reply-clj :macroexpand-error)] |
| 156 | + (assoc msg :op "eval", :code expander-code, :transport transport)) |
| 157 | + (catch Exception ex |
| 158 | + (send-middleware-error msg ex) |
| 159 | + nil))] |
| 160 | + (some-> msg handler))) |
126 | 161 |
|
127 | 162 | ;; ClojureScript impl
|
128 | 163 |
|
|
213 | 248 | (cljs/with-cljs-ns ns
|
214 | 249 | (expander-fn code))))))
|
215 | 250 |
|
216 |
| -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 251 | +(defn macroexpansion-reply-cljs [msg] |
| 252 | + (let [msg (update msg :ns #(or (misc/as-sym %) 'cljs.user))] |
| 253 | + (macroexpansion-response-map msg (expand-cljs msg)))) |
217 | 254 |
|
218 |
| -(defn macroexpansion [{:keys [print-meta ns] :as msg}] |
219 |
| - (let [msg (merge {:expander "macroexpand" :display-namespaces "qualified"} msg) |
220 |
| - expansion (if (cljs/grab-cljs-env msg) |
221 |
| - (expand-cljs (assoc msg :ns (or (misc/as-sym ns) 'cljs.user))) |
222 |
| - (expand-clj (assoc msg :ns (or (misc/as-sym ns) 'user))))] |
223 |
| - (with-out-str |
224 |
| - (binding [*print-meta* (boolean print-meta)] |
225 |
| - (pp/write expansion :dispatch pp/code-dispatch))))) |
| 255 | +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
226 | 256 |
|
227 |
| -(defn macroexpansion-reply [msg] |
228 |
| - {:expansion (macroexpansion msg)}) |
| 257 | +(defn- handle-macroexpand* [handler msg] |
| 258 | + (let [msg (merge {:expander "macroexpand" :display-namespaces "qualified"} msg)] |
| 259 | + (if (cljs/grab-cljs-env msg) |
| 260 | + (with-safe-transport handler msg |
| 261 | + "macroexpand" [macroexpansion-reply-cljs :macroexpand-error]) |
| 262 | + (handle-macroexpand-clj handler msg)))) |
229 | 263 |
|
230 | 264 | (defn handle-macroexpand [handler msg]
|
231 |
| - (with-safe-transport handler msg |
232 |
| - "macroexpand" [macroexpansion-reply :macroexpand-error])) |
| 265 | + (if (= (:op msg) "macroexpand") |
| 266 | + (handle-macroexpand* handler msg) |
| 267 | + (handler msg))) |
0 commit comments