Skip to content

Commit acb0b5a

Browse files
authored
cider.nrepl.middleware.reload: support :before / :after functions (#856)
1 parent 0a08e5f commit acb0b5a

File tree

7 files changed

+122
-97
lines changed

7 files changed

+122
-97
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
## Bugs fixed
1111

12-
* [#854](https://github.com/clojure-emacs/cider-nrepl/pull/854): fix cider.clj-reload/reload-all and improve its test
12+
* [#854](https://github.com/clojure-emacs/cider-nrepl/pull/854): Fix `cider.nrepl.middleware.reload/reload-all`.
13+
* `cider.nrepl.middleware.reload`: support `:before` / `:after` functions.
1314

1415
## 0.46.0 (2024-0305)
1516

doc/modules/ROOT/pages/nrepl-api/ops.adoc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,9 @@ Required parameters::
17961796
{blank}
17971797

17981798
Optional parameters::
1799-
{blank}
1799+
* `:after` The namespace-qualified name of a zero-arity function to call after reloading.
1800+
* `:before` The namespace-qualified name of a zero-arity function to call before reloading.
1801+
18001802

18011803
Returns::
18021804
* `:error` A sequence of all causes of the thrown exception when ``status`` is ``:error``.
@@ -1813,7 +1815,9 @@ Required parameters::
18131815
{blank}
18141816

18151817
Optional parameters::
1816-
{blank}
1818+
* `:after` The namespace-qualified name of a zero-arity function to call after reloading.
1819+
* `:before` The namespace-qualified name of a zero-arity function to call before reloading.
1820+
18171821

18181822
Returns::
18191823
* `:error` A sequence of all causes of the thrown exception when ``status`` is ``:error``.

src/cider/nrepl.clj

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -609,15 +609,18 @@ if applicable, and re-render the updated value."
609609
:requires {}
610610
:returns {"status" "Done"}}}})
611611

612+
(def code-reloading-before-after-opts
613+
{"before" "The namespace-qualified name of a zero-arity function to call before reloading."
614+
"after" "The namespace-qualified name of a zero-arity function to call after reloading."})
615+
612616
(def-wrapper wrap-refresh cider.nrepl.middleware.refresh/handle-refresh
613617
{:doc "Refresh middleware."
614618
:requires #{"clone" #'wrap-print}
615619
:handles {"refresh"
616620
{:doc "Reloads all changed files in dependency order."
617621
:optional (merge wrap-print-optional-arguments
618-
{"dirs" "List of directories to scan. If no directories given, defaults to all directories on the classpath."
619-
"before" "The namespace-qualified name of a zero-arity function to call before reloading."
620-
"after" "The namespace-qualified name of a zero-arity function to call after reloading."})
622+
{"dirs" "List of directories to scan. If no directories given, defaults to all directories on the classpath."}
623+
code-reloading-before-after-opts)
621624
:returns {"reloading" "List of namespaces that will be reloaded."
622625
"status" "`:ok` if reloading was successful; otherwise `:error`."
623626
"error" "A sequence of all causes of the thrown exception when `status` is `:error`."
@@ -643,11 +646,13 @@ if applicable, and re-render the updated value."
643646
using the io.github.tonsky/clj-reload library. It is bundled with cider-nrepl.
644647
If that dependency is already in present your project and clj-reload.core/init has been invoked beforehand,
645648
those configured directories will be honored."
649+
:optional code-reloading-before-after-opts
646650
:returns {"progress" "Description of current namespace being unloaded/loaded."
647651
"status" "`:ok` if reloading was successful; otherwise `:error`."
648652
"error" "A sequence of all causes of the thrown exception when `status` is `:error`."}}
649653
"cider.clj-reload/reload-all"
650654
{:doc "Reloads all files in dependency order."
655+
:optional code-reloading-before-after-opts
651656
:returns {"reloading" "Description of current namespace being unloaded/loaded."
652657
"status" "`:ok` if reloading was successful; otherwise `:error`."
653658
"error" "A sequence of all causes of the thrown exception when `status` is `:error`."}}

src/cider/nrepl/middleware/refresh.clj

Lines changed: 8 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@
55
;; when developing cider-nrepl itself, or when cider-nrepl is used as a
66
;; checkout dependency - tools.namespace doesn't reload source in JARs.
77
(:require
8+
[cider.nrepl.middleware.util.reload :as reload-utils]
89
[clojure.main :refer [repl-caught]]
910
[clojure.tools.namespace.dir :as dir]
1011
[clojure.tools.namespace.find :as find]
1112
[clojure.tools.namespace.reload :as reload]
1213
[clojure.tools.namespace.track :as track]
13-
[haystack.analyzer :as stacktrace.analyzer]
14-
[nrepl.middleware.interruptible-eval :refer [*msg*]]
15-
[nrepl.middleware.print :as print]
1614
[nrepl.misc :refer [response-for]]
17-
[nrepl.transport :as transport]
18-
[orchard.misc :as misc]))
15+
[nrepl.transport :as transport]))
1916

2017
(defonce ^:private refresh-tracker (volatile! (track/tracker)))
2118

@@ -56,101 +53,28 @@
5653
(update-in [::track/load] #(remove load-disabled? %))
5754
(update-in [::track/unload] #(remove unload-disabled? %))))
5855

59-
(defn- zero-arity-callable? [func]
60-
(and (fn? (if (var? func) @func func))
61-
(->> (:arglists (meta func))
62-
(some #(or (= [] %) (= '& (first %)))))))
63-
64-
(defn- resolve-and-invoke
65-
"Takes a string and tries to coerce a function from it. If that
66-
function is a function of possible zero arity (ie, truly a thunk or
67-
has optional parameters and can be called with zero args, it is
68-
called. Returns whether the function was resolved."
69-
[sym {:keys [_session] :as msg}]
70-
(let [the-var (some-> sym misc/as-sym resolve)]
71-
72-
(when (and (var? the-var)
73-
(not (zero-arity-callable? the-var)))
74-
(throw (IllegalArgumentException.
75-
(format "%s is not a function of no arguments" sym))))
76-
77-
(binding [*msg* msg
78-
*out* (print/replying-PrintWriter :out msg {})
79-
*err* (print/replying-PrintWriter :err msg {})]
80-
(when (var? the-var)
81-
(@the-var))
82-
(var? the-var))))
83-
8456
(defn- reloading-reply
8557
[{reloading ::track/load}
8658
{:keys [transport] :as msg}]
8759
(transport/send
8860
transport
8961
(response-for msg {:reloading reloading})))
9062

91-
(defn- error-reply
92-
[{:keys [error error-ns]}
93-
{:keys [::print/print-fn transport] :as msg}]
94-
95-
(transport/send
96-
transport
97-
(response-for msg (cond-> {:status :error}
98-
error (assoc :error (stacktrace.analyzer/analyze error print-fn))
99-
error-ns (assoc :error-ns error-ns))))
100-
101-
(binding [*msg* msg
102-
*err* (print/replying-PrintWriter :err msg {})]
103-
(repl-caught error)))
104-
10563
(defn- result-reply
10664
[{error ::reload/error
10765
error-ns ::reload/error-ns}
10866
{:keys [transport] :as msg}]
10967

11068
(if error
111-
(error-reply {:error error :error-ns error-ns} msg)
69+
(reload-utils/error-reply {:error error :error-ns error-ns} msg)
11270
(transport/send
11371
transport
11472
(response-for msg {:status :ok}))))
11573

116-
(defn- before-reply
117-
[{:keys [before transport] :as msg}]
118-
(when before
119-
(transport/send
120-
transport
121-
(response-for msg {:status :invoking-before
122-
:before before}))
123-
124-
(let [resolved? (resolve-and-invoke before msg)]
125-
(transport/send
126-
transport
127-
(response-for msg
128-
{:status (if resolved?
129-
:invoked-before
130-
:invoked-not-resolved)
131-
:before before})))))
132-
133-
(defn- after-reply
74+
(defn after-reply
13475
[{error ::reload/error}
135-
{:keys [after transport] :as msg}]
136-
137-
(when (and (not error) after)
138-
(try
139-
(transport/send
140-
transport
141-
(response-for msg {:status :invoking-after
142-
:after after}))
143-
144-
(let [resolved? (resolve-and-invoke after msg)]
145-
(transport/send
146-
transport
147-
(response-for msg {:status (if resolved?
148-
:invoked-after
149-
:invoked-not-resolved)
150-
:after after})))
151-
152-
(catch Exception e
153-
(error-reply {:error e} msg)))))
76+
msg]
77+
(reload-utils/after-reply error msg))
15478

15579
(defn- refresh-reply
15680
[{:keys [dirs transport session id] :as msg}]
@@ -161,7 +85,7 @@
16185
(vswap! refresh-tracker
16286
(fn [tracker]
16387
(try
164-
(before-reply msg)
88+
(reload-utils/before-reply msg)
16589

16690
(-> tracker
16791
(dir/scan-dirs (or (seq dirs) (user-refresh-dirs))
@@ -173,7 +97,7 @@
17397
(doto (after-reply msg)))
17498

17599
(catch Throwable e
176-
(error-reply {:error e} msg)
100+
(reload-utils/error-reply {:error e} msg)
177101
tracker))))))
178102
(fn []
179103
(transport/send transport (response-for msg {:status :done}))))))

src/cider/nrepl/middleware/reload.clj

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
Alternative to cider.nrepl.middleware.refresh, using clj-reload instead
44
of tools.namespace."
55
(:require
6+
[cider.nrepl.middleware.util.reload :as reload-utils]
67
[clj-reload.core :as reload]
78
[clojure.main :refer [repl-caught]]
89
[clojure.string :as str]
910
[haystack.analyzer :as analyzer]
1011
[nrepl.middleware.interruptible-eval :refer [*msg*]]
1112
[nrepl.middleware.print :as print]
1213
[nrepl.misc :refer [response-for]]
13-
[nrepl.transport :as transport]
14-
[orchard.misc :as misc]))
14+
[nrepl.transport :as transport]))
1515

1616
(defn- user-reload
1717
"Resolve clj-reload.core/<sym> from the user project or return fallback."
@@ -33,7 +33,8 @@
3333
(defn operation
3434
[msg]
3535
(let [opts {:log-fn (fn [& args]
36-
(respond msg {:progress (str/join " " args)}))}
36+
(respond msg {:progress (str/join " " args)}))
37+
:throw false} ;; mimic the tools.namespace behavior so that we can use `reload-utils/after-reply` uniformly
3738
reload (user-reload 'reload reload/reload)
3839
unload (user-reload 'unload reload/unload)]
3940
(cond
@@ -47,11 +48,14 @@
4748
(exec id
4849
(fn []
4950
(try
50-
(operation msg)
51-
(respond msg {:status :ok})
51+
(reload-utils/before-reply msg)
52+
(let [{:keys [exception]} (operation msg)]
53+
(reload-utils/after-reply exception msg)
54+
(when exception
55+
(throw exception))
56+
(respond msg {:status :ok}))
5257
(catch Throwable error
5358
(respond msg {:status :error
54-
;; TODO assoc :file, :line info if available
5559
:error (analyzer/analyze error print-fn)})
5660
(binding [*msg* msg
5761
*err* (print/replying-PrintWriter :err msg {})]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
(ns cider.nrepl.middleware.util.reload
2+
"Common parts for the code-reloading middleware namespaces."
3+
{:added "0.47.0"}
4+
(:require
5+
[clojure.main :refer [repl-caught]]
6+
[haystack.analyzer :as stacktrace.analyzer]
7+
[nrepl.middleware.interruptible-eval :refer [*msg*]]
8+
[nrepl.middleware.print :as print]
9+
[nrepl.misc :refer [response-for]]
10+
[nrepl.transport :as transport]
11+
[orchard.misc :as misc]))
12+
13+
(defn error-reply
14+
[{:keys [error error-ns]}
15+
{:keys [::print/print-fn transport] :as msg}]
16+
17+
(transport/send
18+
transport
19+
(response-for msg (cond-> {:status :error}
20+
error (assoc :error (stacktrace.analyzer/analyze error print-fn))
21+
error-ns (assoc :error-ns error-ns))))
22+
23+
(binding [*msg* msg
24+
*err* (print/replying-PrintWriter :err msg {})]
25+
(repl-caught error)))
26+
27+
(defn- zero-arity-callable? [func]
28+
(and (fn? (if (var? func) @func func))
29+
(->> (:arglists (meta func))
30+
(some #(or (= [] %) (= '& (first %)))))))
31+
32+
(defn- resolve-and-invoke
33+
"Takes a string and tries to coerce a function from it. If that
34+
function is a function of possible zero arity (ie, truly a thunk or
35+
has optional parameters and can be called with zero args, it is
36+
called. Returns whether the function was resolved."
37+
[sym {:keys [_session] :as msg}]
38+
(let [the-var (some-> sym misc/as-sym resolve)]
39+
40+
(when (and (var? the-var)
41+
(not (zero-arity-callable? the-var)))
42+
(throw (IllegalArgumentException.
43+
(format "%s is not a function of no arguments" sym))))
44+
45+
(binding [*msg* msg
46+
*out* (print/replying-PrintWriter :out msg {})
47+
*err* (print/replying-PrintWriter :err msg {})]
48+
(when (var? the-var)
49+
(@the-var))
50+
(var? the-var))))
51+
52+
(defn before-reply [{:keys [before transport] :as msg}]
53+
(when before
54+
(transport/send
55+
transport
56+
(response-for msg {:status :invoking-before
57+
:before before}))
58+
59+
(let [resolved? (resolve-and-invoke before msg)]
60+
(transport/send
61+
transport
62+
(response-for msg
63+
{:status (if resolved?
64+
:invoked-before
65+
:invoked-not-resolved)
66+
:before before})))))
67+
68+
(defn after-reply [error
69+
{:keys [after transport] :as msg}]
70+
(when (and (not error) after)
71+
(try
72+
(transport/send
73+
transport
74+
(response-for msg {:status :invoking-after
75+
:after after}))
76+
77+
(let [resolved? (resolve-and-invoke after msg)]
78+
(transport/send
79+
transport
80+
(response-for msg {:status (if resolved?
81+
:invoked-after
82+
:invoked-not-resolved)
83+
:after after})))
84+
85+
(catch Exception e
86+
(error-reply {:error e} msg)))))

test/clj/cider/nrepl/middleware/refresh_test.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns cider.nrepl.middleware.refresh-test
22
(:require
33
[cider.nrepl.middleware.refresh :as r]
4+
[cider.nrepl.middleware.util.reload :as reload-utils]
45
[cider.nrepl.test-session :as session]
56
[clojure.test :refer :all]))
67

@@ -24,7 +25,7 @@
2425

2526
(deftest invoking-function-tests
2627
(testing "invoking named function works"
27-
(is (#'r/zero-arity-callable?
28+
(is (#'reload-utils/zero-arity-callable?
2829
(resolve (symbol "cider.nrepl.middleware.refresh-test" "before-fn"))))))
2930

3031
(deftest refresh-op-test

0 commit comments

Comments
 (0)