Skip to content

Commit

Permalink
Support clj-reload workflow (#850)
Browse files Browse the repository at this point in the history
Fixes #849
  • Loading branch information
filipesilva authored Mar 5, 2024
1 parent 20e646d commit d8fbd01
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Let's also acknowledge some of the projects leveraged by cider-nrepl:
* [clj-suitable][] - for ClojureScript code completion using runtime inspection
* [tools.trace][] - for tracing
* [tools.namespace][] - for namespace reloading
* [clj-reload][] - for namespace reloading
* [cljfmt][] - for code formatting

## License
Expand All @@ -149,6 +150,7 @@ Distributed under the Eclipse Public License, the same as Clojure.
[clj-suitable]: https://github.com/clojure-emacs/clj-suitable
[tools.trace]: https://github.com/clojure/tools.trace
[tools.namespace]: https://github.com/clojure/tools.namespace
[clj-reload]: https://github.com/tonsky/clj-reload
[cljfmt]: https://github.com/weavejester/cljfmt
[vim-replant]: https://github.com/SevereOverfl0w/vim-replant
[vim-fireplace]: https://github.com/tpope/vim-fireplace
Expand Down
51 changes: 51 additions & 0 deletions doc/modules/ROOT/pages/nrepl-api/ops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1783,3 +1783,54 @@ 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``.



=== `cider.clj-reload/reload-clear`

Clears the state of clj-reload. This can help recover from a failed load or a circular dependency error.

Required parameters::
{blank}

Optional parameters::
{blank}

Returns::
{blank}
2 changes: 2 additions & 0 deletions doc/modules/ROOT/pages/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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.4.0"]
^:inline-dep [org.clojure/tools.trace "0.7.11"]
^:inline-dep [org.clojure/tools.reader "1.3.6"]
[mx.cider/logjam "0.3.0"]]
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/cider/nrepl.clj
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,25 @@ 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`."}}
"cider.clj-reload/reload-clear"
{:doc "Clears the state of clj-reload. This can help recover from a failed load or a circular dependency error."}}})

(def-wrapper wrap-resource cider.nrepl.middleware.resource/handle-resource
{:doc "Middleware that provides the path to resource."
:handles {"resource"
Expand Down
1 change: 1 addition & 0 deletions src/cider/nrepl/middleware.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions src/cider/nrepl/middleware/reload.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
(ns cider.nrepl.middleware.reload
"Reload changed namespaces.
Alternative to cider.nrepl.middleware.refresh, using clj-reload instead
of tools.namespace."
(: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
"Resolve clj-reload.core/<sym> from the user project or return fallback."
[sym fallback]
(or (some-> (symbol "clj-reload.core" (str sym)) ;; Don't use mrandorsenized version
resolve)
fallback))

(defn- init
"Initialize clj-reload with dirs.
Only used for test, but necessary because of mranderson."
[dirs]
(reload/init {:dirs dirs}))

(defn respond
[{:keys [transport] :as msg} response]
(transport/send transport (response-for msg response)))

(defn operation
[msg]
(let [opts {:log-fn (fn [& args]
(respond msg {:progress (str/join " " args)}))}
reload (user-reload 'reload reload/reload)
unload (user-reload 'unload reload/unload)]
(cond
(:all msg) (reload (assoc opts :all true))
(:clear msg) (unload opts)
:else (reload opts))))

(defn- reload-reply
[{:keys [::print/print-fn transport session id] :as msg}]
(let [{:keys [exec]} (meta session)]
(exec id
(fn []
(try
(operation msg)
(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" (reload-reply msg)
"cider.clj-reload/reload-all" (reload-reply (assoc msg :all true))
"cider.clj-reload/reload-clear" (reload-reply (assoc msg :clear true))
(handler msg)))
46 changes: 46 additions & 0 deletions test/clj/cider/nrepl/middleware/reload_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
(ns cider.nrepl.middleware.reload-test
(:require
[cider.nrepl.middleware.reload :as rl]
[cider.nrepl.test-session :as session]
[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"])

;; Calling init from reload ns to work around mrandersonized version
;; See cider.nrepl.middleware.refresh-test for another test that suffers from this.
(#'rl/init dirs-to-reload)

(deftest user-reload
(testing "returns fallback if clojure.tools.namespace isn't loaded"
(with-redefs [resolve (constantly nil)]
(is (= :foo (#'rl/user-reload 'reload :foo))))))

(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.
;; It's two separate messages, but in (:progress response) they are
;; concatenated.
(is (= "Nothing to unloadNothing 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))))))

(deftest reload-clear-op-test
(testing "reload-all op works"
(let [response (session/message {:op "cider.clj-reload/reload-clear"})]
(is (seq (:progress response)))
(is (= "Nothing to unload" (:progress response)))
(is (= #{"done" "ok"} (:status response))))))

0 comments on commit d8fbd01

Please sign in to comment.