Skip to content
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
- [#84](https://github.com/nrepl/neorepl/pull/84): Locations in text, as `where`, `refs`, `deps` and `check` print them, are relative to the directory neorepl runs in, when they're in it.
- [#85](https://github.com/nrepl/neorepl/pull/85): The profile summary fits in a terminal, without the columns only CIDER's inspector needs.
- [#85](https://github.com/nrepl/neorepl/pull/85): `neorepl profile --format edn` and `json` give the summary as a list of maps, one for each function, rather than the text table.
- [#87](https://github.com/nrepl/neorepl/pull/87): Jack-in brings cider-nrepl 0.64.1, whose reloads point the aliases and refers on the namespaces they make anew at the new ones, and keep the session in its namespace, which neorepl now does itself only for an older cider-nrepl.

### Bug fixes

- [#83](https://github.com/nrepl/neorepl/pull/83): After a reload, the aliases and refers of the namespaces it didn't reload, like the REPL's, point at the reloaded ones, so a function added since resolves through an alias, and requiring with the same alias again works.
- [#87](https://github.com/nrepl/neorepl/pull/87): `,reload` leaves `*1` as it was, rather than setting it to the REPL's namespace.

## 0.5.1 (2026-11-05)

Expand Down
8 changes: 4 additions & 4 deletions PLAN.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion integration/neorepl/cljs_servers_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
(deftest repl-test
(on-each-server
(fn [server]
(let [{:keys [status out]} (run-with server (str ",sessions cljs\n(js/parseInt \"7\")\n"
(let [{:keys [status out]} (run-with server (str ",sessions cljs\n(js/parseInt \"7\")\n,reload\n"
",set print-length 3\n(range 10)\n,set pretty off\n(range 10)\n"
",ns demo.core\n(greet \"r\")\n"
",ns demo.a\n,ns demo.a-test\n(a/f)\n"
Expand All @@ -126,6 +126,9 @@
"repl")]
(is (= 0 status))
(is (str/includes? out "cljs.user=> 7\n"))
(testing "reloading the server's Clojure, which cider-nrepl's refresh won't in the session"
(is (re-find #"cljs\.user=> (Reloaded \S|Nothing changed\n)" out))
(is (not (str/includes? out "Clojure-only"))))
;; Printed again on the JVM, which takes the limit.
(is (str/includes? out "cljs:cljs.user=> (0 1 2 ...)\ncljs:cljs.user=> pretty off\ncljs:cljs.user=> (0 1 2 ...)\n"))
(is (str/includes? out "demo.core=> \"hi r\"\n"))
Expand Down
8 changes: 8 additions & 0 deletions integration/neorepl/real_servers_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@
(is (= ":added\n" (:out (run :clojure "eval" "(demo.core/added)"))))
(testing "with the aliases of a namespace it left alone on the one it made anew"
(is (= {:status 0 :out ":added\n" :err ""} (run :clojure "eval" "-s" "refreshing" "(c/added)"))))
(testing "and the session it reloads in back in its namespace, made anew"
(with-client [client {:port (discovery/read-port (io/file (project-dir :clojure) ".nrepl-port"))}]
(let [session (nrepl/clone-session client)
values #(keep :value (nrepl/request client {:op "eval" :session session :code %}))]
(values "(in-ns 'demo.core)")
(spit file "\n(defn more [] :more)\n" :append true)
(is (= ["demo.core" "demo.core-test"] (:reloaded (features/reload client [] {:session session} {}))))
(is (= [":more"] (values "(more)"))))))
(finally
(spit file servers/core)
(run :clojure "reload")))))
Expand Down
156 changes: 71 additions & 85 deletions src/neorepl/features.clj
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,23 @@
(defn- supports? [client op]
(contains? (:ops client) op))

(defn- cider-release
"The server's cider-nrepl version, by describe's :cider-version, as
[[major minor patch] prerelease], with a prerelease like alpha3 or nil,
or nil when there's no version."
[{:keys [cider-version]}]
(when-let [[_ major minor patch prerelease] (some->> cider-version (re-find #"^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?"))]
[(mapv #(Long/parseLong %) [major minor patch]) prerelease]))

(defn- orchard-spaces?
"Whether the server's cider-nrepl puts a space either side of each entity
in a Javadoc's HTML, as those before 0.64.0-alpha3 do, the first with
orchard#438."
[{:keys [cider-version]}]
(let [[_ major minor patch prerelease] (some->> cider-version (re-find #"^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?"))]
(or (nil? major)
(let [release (mapv #(Long/parseLong %) [major minor patch])]
(or (neg? (compare release [0 64 0]))
(and (= [0 64 0] release) (contains? #{"alpha1" "alpha2"} prerelease)))))))
[client]
(let [[release prerelease] (cider-release client)]
(or (nil? release)
(neg? (compare release [0 64 0]))
(and (= [0 64 0] release) (contains? #{"alpha1" "alpha2"} prerelease)))))

(defn- no-namespace
"The exception saying there's no namespace called ns."
Expand Down Expand Up @@ -652,51 +659,41 @@
normalize-causes
not-empty)))

(def ^:private relink-code
"Has the aliases and refers in every namespace that are on a namespace a
reload made anew point at the new one, and its var by the same name.
cider-nrepl's refresh and clj-reload make the namespaces they reload anew
with remove-ns and leave the others' on the old ones, as clj-reload's
README says, where a function added since isn't there, and the alias
can't be made again. What has nothing new to point at stays as it was,
the way an :as-alias keeps working for keywords. It goes by the var's
own namespace and name, rather than its metadata, which potemkin's
import-vars copies from another var, and a var from Var/create has no
namespace at all. Only Clojure has those reloads, so it does nothing on
babashka. cider-nrepl does the same after its reloads since
cider-nrepl#1077, so this can go once that's in the oldest one supported."
"#?(:bb nil
:clj (clojure.core/let [now #(clojure.core/some-> % clojure.core/ns-name clojure.core/find-ns)]
(clojure.core/doseq [^clojure.lang.Namespace n (clojure.core/all-ns)]
(clojure.core/reduce-kv
(clojure.core/fn [_ a target]
(clojure.core/let [current (now target)]
(clojure.core/when (clojure.core/and current (clojure.core/not (clojure.core/identical? target current)))
(.removeAlias n a)
(.addAlias n a current))))
nil (.getAliases n))
(clojure.core/reduce-kv
(clojure.core/fn [_ s v]
(clojure.core/when (clojure.core/instance? clojure.lang.Var v)
(clojure.core/let [target (.-ns ^clojure.lang.Var v)
^clojure.lang.Namespace current (now target)
^clojure.lang.Var new (clojure.core/when (clojure.core/and current (clojure.core/not (clojure.core/identical? target current)))
(.findInternedVar current (.-sym ^clojure.lang.Var v)))]
(clojure.core/when (clojure.core/and new (.isPublic new))
(.unmap n s)
(.refer n s new)))))
nil (.getMappings n)))))")
(def ^:private reenter-code
"Puts the session back in its namespace, by its name, which a reload can
make anew, leaving the session in the old one. The REPL then sets *3 to
*2, *2 to *1 and *1 to the value, so the code sets them a step back and
comes to *1, which leaves all three as they were."
"(clojure.core/let [v1 clojure.core/*1 v2 clojure.core/*2 v3 clojure.core/*3]
(clojure.core/in-ns (clojure.core/ns-name clojure.core/*ns*))
(set! clojure.core/*2 v3)
(set! clojure.core/*1 v2)
v1)")

(defn- reenter
"Puts session back in its namespace after a reload that made namespaces
anew, as reenter-code does, when the server's cider-nrepl doesn't: before
0.64.1, the first with cider-nrepl#1078, so this can go once that's the
oldest one supported."
[client session]
(let [[release] (cider-release client)]
(when (or (nil? release) (neg? (compare release [0 64 1])))
(nrepl/request client {:op "eval" :session session :code reenter-code
;; *1 could be anything, even endless.
:nrepl.middleware.print/quota 1}))))

(defn reload
"Reloads code and returns {:reloaded [the namespaces]}, and the :output
loading them printed, like reflection warnings, if any. With nses, a seq
of namespace names, those namespaces get reloaded. Without, the ones that
changed since the last reload, which takes cider-nrepl's refresh, and the
aliases and refers on them then get relinked, as relink-code says. A big
project takes a while, so it waits as long as that takes unless opts have
a :timeout-ms."
([client nses] (reload client nses {}))
([client nses opts]
changed since the last reload, which takes cider-nrepl's refresh, in
ctx's :session, if it has one. That makes the namespaces anew, and
cider-nrepl, since 0.64.1, has the aliases and refers on them point at
the new ones, and puts the session back in its namespace, as reenter does
for an older one. A big project takes a while, so it waits as long as
that takes unless opts have a :timeout-ms."
([client nses] (reload client nses {} {}))
([client nses {:keys [session]} opts]
(let [opts (merge {:timeout-ms nil} opts)
[reloaded responses]
(cond
Expand All @@ -708,24 +705,30 @@
opts)]

(supports? client "refresh")
(let [refresh #(ask client {:op "refresh"} opts)
responses (try
(refresh)
(catch clojure.lang.ExceptionInfo e
;; cider-nrepl keeps trying to reload a namespace
;; whose file is gone, until it forgets them all,
;; which reloads the rest too, so only then.
(if-let [ns (some :error-ns (:responses (ex-data e)))]
(if (str/includes? (ex-message e) (str "Could not locate "
(-> ns (str/replace "-" "_") (str/replace "." "/"))
"__init.class"))
(do (ask client {:op "refresh-clear"} opts)
(refresh))
(throw e))
(throw e))))
(let [refresh #(let [responses (ask client (cond-> {:op "refresh"} session (assoc :session session)) opts)]
;; cider-nrepl won't in a ClojureScript session, which
;; has no Clojure namespace to go back to anyway.
(if (some (fn [response] (nrepl/status? response "clojure-only")) responses)
[(ask client {:op "refresh"} opts) nil]
[responses session]))
[responses refreshed-in]
(try
(refresh)
(catch clojure.lang.ExceptionInfo e
;; cider-nrepl keeps trying to reload a namespace
;; whose file is gone, until it forgets them all,
;; which reloads the rest too, so only then.
(if-let [ns (some :error-ns (:responses (ex-data e)))]
(if (str/includes? (ex-message e) (str "Could not locate "
(-> ns (str/replace "-" "_") (str/replace "." "/"))
"__init.class"))
(do (ask client {:op "refresh-clear"} opts)
(refresh))
(throw e))
(throw e))))
reloaded (vec (some :reloading responses))]
(when (seq reloaded)
(nrepl/request client {:op "eval" :code relink-code}))
(when (and refreshed-in (seq reloaded))
(reenter client refreshed-in))
[reloaded responses])

:else
Expand Down Expand Up @@ -1768,25 +1771,13 @@
opts
(fn [_]))))

(def ^:private reenter-code
"Puts the session back in its namespace, by its name, which a reload can
make anew, leaving the session in the old one. The REPL then sets *3 to
*2, *2 to *1 and *1 to the value, so the code sets them a step back and
comes to *1, which leaves all three as they were. cider-nrepl puts the
session that asked for the reload back itself since cider-nrepl#1078, so
this can go once that's in the oldest one supported."
"(clojure.core/let [v1 clojure.core/*1 v2 clojure.core/*2 v3 clojure.core/*3]
(clojure.core/in-ns (clojure.core/ns-name clojure.core/*ns*))
(set! clojure.core/*2 v3)
(set! clojure.core/*1 v2)
v1)")

(defn reload-changed
"Reloads the namespaces whose files changed since the last time, the ones
that depend on them after them, with cider-nrepl's clj-reload, which
keeps what defonce defined and leaves alone namespaces nothing loaded,
then puts session back in its namespace and relinks the aliases and
refers on the namespaces it made anew, as relink-code says. It runs in
keeps what defonce defined and leaves alone namespaces nothing loaded.
It makes the namespaces it reloads anew, and cider-nrepl, since 0.64.1,
puts session back in its namespace, as reenter does for an older one, and
has the aliases and refers on them point at the new ones. It runs in
session, the way evaluation/evaluate does, whose opts it takes. Returns
nil once it's done, or else what evaluate does, saying why not, with
:clojurescript? in a ClojureScript session, where there's nothing for it
Expand Down Expand Up @@ -1814,12 +1805,7 @@
(= "error" status) (assoc result :message "Reloading what changed failed")
(not= "ok" status) result
;; Without a count, as from a project's own clj-reload, it may have.
(= "0" @reloaded) nil
:else (do (nrepl/request client {:op "eval" :session session :code reenter-code
;; *1 could be anything, even endless.
:nrepl.middleware.print/quota 1})
(nrepl/request client {:op "eval" :code relink-code})
nil))))
(not= "0" @reloaded) (do (reenter client session) nil))))

(defn note-changes
"Has cider-nrepl's clj-reload, if the server at host and port has it,
Expand Down
2 changes: 1 addition & 1 deletion src/neorepl/jack_in.clj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
only gets cider-nrepl, since it comes with the nREPL it's made for and
runs ClojureScript REPLs itself."
{:nrepl "1.8.0"
:cider-nrepl "0.64.0"
:cider-nrepl "0.64.1"
:piggieback "0.8.0"})

(defn- builds-with-project?
Expand Down
4 changes: 2 additions & 2 deletions src/neorepl/lookups.clj
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
:none (fn [_ sym] (str sym " uses no functions"))}
{:name "reload" :arg "NS..." :args? true :optional? true
:help "Reload the namespaces NS, or the ones that changed"
:find (fn [client {:keys [timeout-ms]} nses]
(features/reload client nses {:timeout-ms timeout-ms}))
:find (fn [client {:keys [timeout-ms] :as ctx} nses]
(features/reload client nses ctx {:timeout-ms timeout-ms}))
:text (fn [{:keys [reloaded output]}]
(str (some-> output str/trimr (str "\n"))
(if (seq reloaded)
Expand Down
13 changes: 1 addition & 12 deletions src/neorepl/repl.clj
Original file line number Diff line number Diff line change
Expand Up @@ -460,21 +460,10 @@
(nrepl/session-ns client id)
(when (= id (:session own)) "this REPL's")]))))))

(defn- reenter
"Puts session back in the namespace called ns, as tools.namespace's refresh
does: reloading code can make a namespace anew, and the session would go
on in the old one."
[client session ns]
;; Qualified, since the namespace it's in might not refer clojure.core.
(nrepl/request client {:op "eval" :session session
:code (format "(clojure.core/in-ns (clojure.core/symbol %s))" (pr-str ns))}))

(def ^:private follow-ups
"What the REPL does after some lookups, by name, given command-output's
state and what the lookup found."
{"reload" (fn [{:keys [client session ns]} _]
(reenter client session ns))
"trace" (fn [{:keys [show-traces]} {:keys [results]}]
{"trace" (fn [{:keys [show-traces]} {:keys [results]}]
(when (some #(#{"traced" "already-traced"} (:status %)) results)
(show-traces)))
"untrace" (fn [{:keys [hide-traces]} {:keys [untraced]}]
Expand Down
Loading
Loading