From 37a8dbf00a9cac7e5e4a4d4bba1af4a81d905b92 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Fri, 23 Feb 2024 17:58:14 +0000 Subject: [PATCH] Support clj-reload workflow Fix #849 --- doc/modules/ROOT/pages/nrepl-api/ops.adoc | 37 ++++++++++++++ doc/modules/ROOT/pages/usage.adoc | 2 + project.clj | 2 + src/cider/nrepl.clj | 17 +++++++ src/cider/nrepl/middleware.clj | 1 + src/cider/nrepl/middleware/reload.clj | 50 +++++++++++++++++++ .../cider/nrepl/middleware/reload_test.clj | 36 +++++++++++++ 7 files changed, 145 insertions(+) create mode 100644 src/cider/nrepl/middleware/reload.clj create mode 100644 test/clj/cider/nrepl/middleware/reload_test.clj diff --git a/doc/modules/ROOT/pages/nrepl-api/ops.adoc b/doc/modules/ROOT/pages/nrepl-api/ops.adoc index a7ee74b5..d5202dc9 100644 --- a/doc/modules/ROOT/pages/nrepl-api/ops.adoc +++ b/doc/modules/ROOT/pages/nrepl-api/ops.adoc @@ -1777,3 +1777,40 @@ Returns:: * `:status` done * `:cider/log-update-consumer` The consumer that was updated. + + +=== `cider.clj-reload/reload` + +Reloads all changed files in dependency order, +using the io.github.tonsky/clj-reload library. It is bundled with cider-nrepl. +If that dependency is already in present your project and clj-reload.core/init has been invoked beforehand, +those configured directories will be honored. + +Required parameters:: +{blank} + +Optional parameters:: +{blank} + +Returns:: +* `:error` A sequence of all causes of the thrown exception when ``status`` is ``:error``. +* `:progress` Description of current namespace being unloaded/loaded. +* `:status` ``:ok`` if reloading was successful; otherwise ``:error``. + + + +=== `cider.clj-reload/reload-all` + +Reloads all files in dependency order. + +Required parameters:: +{blank} + +Optional parameters:: +{blank} + +Returns:: +* `:error` A sequence of all causes of the thrown exception when ``status`` is ``:error``. +* `:reloading` Description of current namespace being unloaded/loaded. +* `:status` ``:ok`` if reloading was successful; otherwise ``:error``. + diff --git a/doc/modules/ROOT/pages/usage.adoc b/doc/modules/ROOT/pages/usage.adoc index 71f3f113..e2be9ec9 100644 --- a/doc/modules/ROOT/pages/usage.adoc +++ b/doc/modules/ROOT/pages/usage.adoc @@ -47,6 +47,7 @@ under `:repl-options`. cider.nrepl/wrap-spec cider.nrepl/wrap-profile cider.nrepl/wrap-refresh + cider.nrepl/wrap-reload cider.nrepl/wrap-resource cider.nrepl/wrap-stacktrace cider.nrepl/wrap-test @@ -161,6 +162,7 @@ That's how CIDER's nREPL handler is created: cider.nrepl/wrap-slurp cider.nrepl/wrap-profile cider.nrepl/wrap-refresh + cider.nrepl/wrap-reload cider.nrepl/wrap-resource cider.nrepl/wrap-spec cider.nrepl/wrap-stacktrace diff --git a/project.clj b/project.clj index d062e815..958a934a 100644 --- a/project.clj +++ b/project.clj @@ -19,6 +19,7 @@ ~(with-meta '[org.clojure/tools.namespace "1.3.0"] ;; :cognitest uses tools.namespace, so we cannot inline it while running tests. {:inline-dep (not= "true" (System/getenv "SKIP_INLINING_TEST_DEPS"))}) + ^:inline-dep [io.github.tonsky/clj-reload "0.2.0"] ^:inline-dep [org.clojure/tools.trace "0.7.11"] ^:inline-dep [org.clojure/tools.reader "1.3.6"] [mx.cider/logjam "0.2.0"]] @@ -157,6 +158,7 @@ cider.nrepl/wrap-out cider.nrepl/wrap-profile cider.nrepl/wrap-refresh + cider.nrepl/wrap-reload cider.nrepl/wrap-resource cider.nrepl/wrap-slurp cider.nrepl/wrap-spec diff --git a/src/cider/nrepl.clj b/src/cider/nrepl.clj index 6ed14e47..de7215cf 100644 --- a/src/cider/nrepl.clj +++ b/src/cider/nrepl.clj @@ -632,6 +632,23 @@ if applicable, and re-render the updated value." "refresh-clear" {:doc "Clears the state of the refresh middleware. This can help recover from a failed load or a circular dependency error."}}}) +(def-wrapper wrap-reload cider.nrepl.middleware.reload/handle-reload + {:doc "Reload middleware." + :requires #{"clone" #'wrap-print} + :handles {"cider.clj-reload/reload" + {:doc "Reloads all changed files in dependency order, +using the io.github.tonsky/clj-reload library. It is bundled with cider-nrepl. +If that dependency is already in present your project and clj-reload.core/init has been invoked beforehand, +those configured directories will be honored." + :returns {"progress" "Description of current namespace being unloaded/loaded." + "status" "`:ok` if reloading was successful; otherwise `:error`." + "error" "A sequence of all causes of the thrown exception when `status` is `:error`."}} + "cider.clj-reload/reload-all" + {:doc "Reloads all files in dependency order." + :returns {"reloading" "Description of current namespace being unloaded/loaded." + "status" "`:ok` if reloading was successful; otherwise `:error`." + "error" "A sequence of all causes of the thrown exception when `status` is `:error`."}}}}) + (def-wrapper wrap-resource cider.nrepl.middleware.resource/handle-resource {:doc "Middleware that provides the path to resource." :handles {"resource" diff --git a/src/cider/nrepl/middleware.clj b/src/cider/nrepl/middleware.clj index 293eb097..f053a9ba 100644 --- a/src/cider/nrepl/middleware.clj +++ b/src/cider/nrepl/middleware.clj @@ -23,6 +23,7 @@ cider.nrepl/wrap-slurp cider.nrepl/wrap-profile cider.nrepl/wrap-refresh + cider.nrepl/wrap-reload cider.nrepl/wrap-resource cider.nrepl/wrap-spec cider.nrepl/wrap-stacktrace diff --git a/src/cider/nrepl/middleware/reload.clj b/src/cider/nrepl/middleware/reload.clj new file mode 100644 index 00000000..76103e5a --- /dev/null +++ b/src/cider/nrepl/middleware/reload.clj @@ -0,0 +1,50 @@ +(ns cider.nrepl.middleware.reload + (:require + [clj-reload.core :as reload] + [clojure.main :refer [repl-caught]] + [clojure.string :as str] + [haystack.analyzer :as analyzer] + [nrepl.middleware.interruptible-eval :refer [*msg*]] + [nrepl.middleware.print :as print] + [nrepl.misc :refer [response-for]] + [nrepl.transport :as transport] + [orchard.misc :as misc])) + +(defn- user-reload + "clj-reload.core/reload from the user project. + Must be configured via clj-reload.core/init before being called." + [] + (some-> (symbol "clj-reload.core" "reload") ;; Don't use mrandorsenized version + resolve)) + +(defn respond + [{:keys [transport] :as msg} response] + (transport/send transport (response-for msg response))) + +(defn- refresh-reply + [{:keys [::print/print-fn transport session id] :as msg}] + (let [{:keys [exec]} (meta session)] + (exec id + (fn [] + (try + (let [reload (or (user-reload) reload/reload)] + (reload (cond-> {:log-fn (fn [& args] + (respond msg {:progress (str/join " " args)}))} + (:all msg) (assoc :only :all))) + (respond msg {:status :ok})) + (catch Throwable error + (respond msg {:status :error + ;; TODO assoc :file, :line info if available + :error (analyzer/analyze error print-fn)}) + (binding [*msg* msg + *err* (print/replying-PrintWriter :err msg {})] + (repl-caught error))))) + + (fn [] (respond msg {:status :done}))))) + +(defn handle-reload [handler msg] + (case (:op msg) + "cider.clj-reload/reload" (refresh-reply msg) + "cider.clj-reload/reload-all" (refresh-reply (assoc msg :all true)) + (handler msg))) + diff --git a/test/clj/cider/nrepl/middleware/reload_test.clj b/test/clj/cider/nrepl/middleware/reload_test.clj new file mode 100644 index 00000000..a36b5e84 --- /dev/null +++ b/test/clj/cider/nrepl/middleware/reload_test.clj @@ -0,0 +1,36 @@ +(ns cider.nrepl.middleware.reload-test + (:require + [cider.nrepl.middleware.reload :as rl] + [cider.nrepl.test-session :as session] + [clj-reload.core :as reload] + [clojure.test :refer :all])) + +(use-fixtures :each session/session-fixture) + +(def ^:private dirs-to-reload + ;; Limit the scope of what we reload, because (for example) reloading the + ;; cider.nrepl.middleware.test-session ns causes *session* in that ns to be + ;; unloaded, which breaks session-fixture, and hence all of the below tests. + ["test/clj/cider/nrepl/middleware/util"]) + +(reload/init {:dirs dirs-to-reload}) + +(deftest user-reload + (testing "returns nil if clojure.tools.namespace isn't loaded" + (with-redefs [resolve (constantly nil)] + (is (nil? (#'rl/user-reload)))))) + +(deftest reload-op-test + (testing "reload op works" + (let [response (session/message {:op "cider.clj-reload/reload"})] + ;; There is nothing to reload since the files did not change, + ;; but the message does come from clj-reload.core/reload. + (is (= "Nothing to reload" (:progress response))) + (is (= #{"done" "ok"} (:status response)))))) + +(deftest reload-all-op-test + (testing "reload-all op works" + (let [response (session/message {:op "cider.clj-reload/reload-all"})] + (is (seq (:progress response))) + (is (= #{"done" "ok"} (:status response)))))) +