Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support clj-reload workflow #850

Merged
merged 1 commit into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))))))