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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## main (unreleased)

### Changes

- [#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.

### 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.
Expand Down
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ Each one is a release people can use. The first three stages below all went into
- Evaluation, done: an eval's top-level forms go out one at a time, each with the line and column it starts at, the next once the one before it is done, so `--timeout`, Ctrl-C and MCP cancellations stop the forms after the one they interrupt, which a server goes on to otherwise, and piggieback, which evaluates only the first form of an eval, gets them all. Forms after one that throws still run, as on the server. Since nREPL's sockets hold an eval's done back until the client acknowledges its value, which Linux delays by 40ms, the JVM client sets `TCP_QUICKACK` before each read, so 100 forms take 150ms there instead of 4s; babashka can't, see the open questions. shadow-cljs's `:repl/exception!` counts as an eval-error, a read from `*in*` under a timeout happens on a thread of its own, and the text printer ends a line stderr leaves open before what goes to stdout.
- MCP, done: `pst` and `apropos` put their lists under `causes` and `vars`, since structured content is an object, which the official SDK checks. Code gets end of input from `*in*`, which carries the protocol. `reload` gives up at the call's timeout. Arguments get checked against the tools' schemas, which say what ports, timeouts and sessions can be, and a line the reader can't take gets a parse error, ids past a long being bigints now. A connection whose server went away gets replaced, and closed, before a call uses it. The default session is `agent`, the skill's, and `--max-output` cuts the whole result.
- Lookups, done: `source` reads the definition from the file and line `info` finds, as CIDER does, which works after `load` and in ClojureScript, and takes it only if it's a definition, since cider-nrepl gives the macros ClojureScript takes from Clojure lines from somewhere else; it falls back to `clojure.repl/source-fn` for files this machine can't read. The eval fallbacks compile in `user` with `*ns*` bound, so they work in a namespace made with `in-ns`. Java members and classes get their doc, from cider-nrepl's Javadoc fragments, and arglists, fields none, and a namespace is named once. `,macroexpand` with cider-nrepl expands in a copy of the session, keeping its `*1`. `--session`, and the MCP tools' `session`, look things up as a session sees them, in its language and namespace; in ClojureScript that's the Clojure namespace under it, which is all `describe` has, so `--ns` names the other.
- Tests, done: a whole-project run with cider-nrepl loads the project's namespaces first, those orchard finds, in a copy of the session, and says which don't load, then runs the tests without `:load-project-ns?`, which never answers when a namespace doesn't load; a server without orchard, like bb's, still gets the flag. A namespace that threw while loading is gone, but counts as loaded to `require` after, its `ns` form having said so, so a reload gets its error again, for every run, named namespaces' too. A test error's location is its test's file, with the line cider-nrepl gives, which is the test's, rather than the file of the frame that threw, and on bb every failure's is, bb giving the file the session loaded last. One deadline covers the run, loading the tests included, and running out while loading them exits 124. A namespace named without tests says so and exits 1, a test namespace that doesn't load says that, and failure headings show files relative to the directory neorepl runs in.
- Tests, done: a whole-project run with cider-nrepl loads the project's namespaces first, those orchard finds, in a copy of the session, and says which don't load, then runs the tests without `:load-project-ns?`, which never answers when a namespace doesn't load; a server without orchard, like bb's, still gets the flag. A namespace that threw while loading is gone, but counts as loaded to `require` after, its `ns` form having said so, so a reload gets its error again, for every run, named namespaces' too. A test error's location is its test's file, with the line cider-nrepl gives, which is the test's, rather than the file of the frame that threw, and on bb every failure's is, bb giving the file the session loaded last. One deadline covers the run, loading the tests included, and running out while loading them exits 124. A namespace named without tests says so and exits 1, a test namespace that doesn't load says that, and failure headings show files relative to the directory neorepl runs in, as every location in text does since 2026-11-06: `where`, `refs`, `deps` and `check` too, in the commands, the REPL and the MCP tools, but not in edn or JSON.
- Errors and output, done: failures come out in `--format edn` and `json` too, with their status and message. "The server couldn't do eval" and friends make way for what's wrong: a namespace that isn't there, which nREPL's `lookup` says only with an error, a regex that doesn't parse, checked before it goes out, and inspected code that throws, which cider-nrepl answers with the whole stack trace, cut to its first line and its root cause. A server restart that loses a named session says so where a new one takes its place, on stderr or in the MCP result, and where it doesn't, as for `pst`. `doc ->` and `where -main` take their symbols for arguments, an argument being an option only when it looks like one. A timeout waiting behind an evaluation left in the session says that, and that interrupting the session stops it. `eval -f` and `load` check a file's delimiters first, as `neorepl check` would, and say a file can't be read. `pst` hides what runs the code, the REPL's eval among them, by the same classes orchard goes by, on servers without cider-nrepl too, and bb's cider-nrepl frames, and takes a frame for a duplicate by its function, rather than cider-nrepl's file and line, which a REPL's functions all share. `reload` that finds a namespace's file gone has cider-nrepl forget the namespaces, with `refresh-clear`, and tries again.
- Tracing, done: tracing what the server's own tools call, `clojure.core`, `clojure.string` and the like, nREPL's, cider-nrepl's and orchard's, takes the server down, tracing calls into tracing, so neorepl looks the name up first and won't.
- The REPL, done: output from another thread that doesn't end its line goes above the prompt a quarter of a second after it comes, or before what's printed next once the line's read, rather than waiting for a newline and running into the next value. Ctrl-R and Ctrl-S show the search's prompt, which JLine clears whenever it shows a tail tip, eldoc's, so the tip is off while searching. `,stacktrace` stays open on a frame it can't open, saying why on its status line, and comes back on the same frame after the editor exits, and `,edit` opens a definition in a jar as a read-only copy under neorepl's cache, copied again when the jar changes. Echo is off while an evaluation runs, so what's typed ahead only shows up at the next prompt; a real terminal doesn't echo the `^C` then either, while JLine's test terminal still does. The plain REPL leaves out the inspector's `[N]` marks, and ends the line the terminal echoed `^C` on before saying what Ctrl-C did, asking `stty` whether it did, since Emacs' M-x shell has echo off. Eldoc's tip is cut to what's left of the line, `NO_COLOR` takes the color out of the completion menu, which an empty style doesn't, JLine falling back on its own, and C-x C-s and C-x C-a show the source and the vars whose names match, as rebel-readline's do. Indenting continuation lines, the vi keymap and the namespaces in the completion menu went to 0.5.
Expand Down
5 changes: 3 additions & 2 deletions src/neorepl/main.clj
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ reload only starts noting what changes."
(let [[line column] (lexer/position code start)]
(assoc msg :code (subs code start end) :line line :column column)))]
(when-not start
(throw (ex-info (str file ":" line ":" column ": No form there or before it to evaluate")
(throw (ex-info (str (render/location-text {:file file :line line :column column}) ": No form there or before it to evaluate")
{:type ::features/failed})))
(let [ns-form (when-not ns
(last (take-while #(<= (:start %) start) (lexer/ns-forms lexed))))]
Expand Down Expand Up @@ -788,7 +788,8 @@ fails, 2 on usage errors and 3 when there's no server to talk to."
(help-text
"Usage: neorepl where [options] SYM"
"Prints where SYM is defined, as file:line:column, the way compilers and
editors write locations. A definition in a jar comes as a jar: URL."
editors write locations, with a file in the current directory relative to
it. A definition in a jar comes as a jar: URL."
lookup-options
discovery-note
"Exit status: 0 when the server knows where SYM is, 1 when it doesn't, 2 on
Expand Down
33 changes: 20 additions & 13 deletions src/neorepl/render.clj
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,27 @@
[causes]
(str/join "\n" (causes-lines causes " at " 0)))

(defn- relative
"path relative to dir, when it's in there and isn't dir itself, or else
as it is, as with a root for dir, which everything is in."
[path dir]
(try
(let [p (.normalize (.toPath (java.io.File. ^String path)))
d (.normalize (.toPath (java.io.File. ^String dir)))]
(if (and (.getParent d) (.startsWith p d) (not= p d))
(str (.relativize d p))
path))
;; Windows' InvalidPathException, which babashka doesn't have by name,
;; for what isn't a path there, like a jar:file: URL or <stdin>.
(catch IllegalArgumentException _ path)))

(defn location-text
"Where something is, its :file, :line and :column, the way compilers and
editors write it: file:line:column."
[{:keys [file line column]}]
(str/join ":" (remove nil? [file line column])))
editors write it: file:line:column, with a file in dir, by default the
directory neorepl runs in, relative to it."
([location] (location-text location (System/getProperty "user.dir")))
([{:keys [file line column]} dir]
(str/join ":" (remove nil? [(some-> file str (relative dir)) line column]))))

(defn references-text
"Cross-references, from features/references, a line each: the function's
Expand Down Expand Up @@ -216,15 +232,6 @@
(concat (frame-lines stacktrace " at ")
(causes-lines more " at " 1)))

(defn- relative
"path relative to dir, when it's in there, or else as it is."
[path dir]
(let [p (.toPath (java.io.File. ^String path))
d (.toPath (java.io.File. ^String dir))]
(if (.startsWith p d)
(str (.relativize d p))
path)))

(defn failure-heading
"The first line of a test that didn't pass, from a run-tests report, as
clojure.test prints it: what it is and where, relative to the directory
Expand All @@ -234,7 +241,7 @@
(str (str/upper-case type)
;; Not in a test, when a fixture fails.
(some->> (not-empty (str/join "/" (remove nil? [ns var]))) (str " in "))
(when file (str " (" (location-text {:file (relative file dir) :line line}) ")")))))
(when file (str " (" (location-text {:file file :line line} dir) ")")))))

(defn failure-text
"A test that didn't pass, from a run-tests report, as clojure.test prints it."
Expand Down
19 changes: 18 additions & 1 deletion test/neorepl/render_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,24 @@
(are [location text] (= text (render/location-text location))
{:file "/x.clj" :line 3 :column 1} "/x.clj:3:1"
{:file "/x.clj" :line 3} "/x.clj:3"
{:file "/x.clj"} "/x.clj"))
{:file "/x.clj"} "/x.clj"
{:file (str (io/file (System/getProperty "user.dir") "x.clj"))} (str (io/file "x.clj")))
(let [dir (str (io/file "/work/app"))]
(testing "relative to the directory neorepl runs in, when it's in there"
(is (= (str (io/file "src/my/app.clj") ":12:1")
(render/location-text {:file (str (io/file dir "src/my/app.clj")) :line 12 :column 1} dir))))
(testing "but not a file in a jar, nor standard input"
(is (= "jar:file:/m2/clojure.jar!/clojure/core.clj:7"
(render/location-text {:file "jar:file:/m2/clojure.jar!/clojure/core.clj" :line 7} dir)))
(is (= "<stdin>:2:1" (render/location-text {:file "<stdin>" :line 2 :column 1} dir))))
(testing "nor the directory itself, or anything when it's a root, where everything is"
(is (= dir (render/location-text {:file dir} dir)))
(let [root (str (first (java.io.File/listRoots)))
file (str (io/file root "work" "app.clj"))]
(is (= (str file ":1") (render/location-text {:file file :line 1} root)))))
(testing "normalized first"
(is (= (str (io/file "src/b.clj"))
(render/location-text {:file (str (io/file dir "src" "a" ".." "b.clj"))} dir))))))

(deftest test-report-text-test
(testing "like clojure.test"
Expand Down
Loading