diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c367519..48d5fc8a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Introduce `cider-jack-in-tools` and `cider-register-jack-in-tool` so third-party packages can register new project tools for `cider-jack-in` and `cider-jack-in-universal`. - Cache the result of `cider--running-nrepl-paths` (used by `cider-locate-running-nrepl-ports`) for `cider-running-nrepl-paths-cache-ttl` seconds (default 5). Repeated `cider-connect` completions no longer re-spawn a fresh round of `ps`/`lsof` subprocesses each time. `cider-clear-running-nrepl-paths-cache` discards the cache on demand. - New `nrepl-make-eval-handler` with a keyword-arg API (`:on-value`, `:on-stdout`, `:on-stderr`, `:on-done`, `:on-eval-error`, `:on-content-type`, `:on-truncated`). Sub-handlers no longer take a buffer argument -- they close over whatever they need. `nrepl-make-response-handler`, the legacy 7-positional-arg form, is preserved as an obsolete shim that adapts the old (buffer x) lambdas to the new (x) lambdas, so existing extensions keep working. -- New `cider-repl-history-doctor' command: walks `cider-repl-input-history' looking for entries whose parens don't balance under Clojure syntax, shows each in a side buffer, and asks whether to delete it. When done, rewrites `cider-repl-history-file' if one is configured. Useful for cleaning up history after a typo got committed that breaks `cider-repl-history' rendering (see [#3915](https://github.com/clojure-emacs/cider/issues/3915)). +- New `cider-repl-history-doctor` command: walks `cider-repl-input-history` looking for entries whose parens don't balance under Clojure syntax, shows each in a side buffer, and asks whether to delete it. When done, rewrites `cider-repl-history-file` if one is configured. Useful for cleaning up history after a typo got committed that breaks `cider-repl-history` rendering (see [#3915](https://github.com/clojure-emacs/cider/issues/3915)). - Decouple the nREPL transport layer from CIDER's UI layer (closes [#1099](https://github.com/clojure-emacs/cider/issues/1099)). `nrepl-make-eval-handler` is now CIDER-agnostic: it no longer references `nrepl-namespace-handler-function`, `nrepl-err-handler-function`, `nrepl-need-input-handler-function`, or any hardcoded UI strings. New `:on-ns` and `:on-status` keyword slots let any consumer wire up their own namespace tracking and status handling. The editor-level `cider-make-eval-handler` wraps it with CIDER's UI behavior (ns tracking, default error handler, need-input prompt, "Evaluation interrupted." / "Namespace not found." messages); in-tree callers all use it. ### Bugs fixed @@ -27,13 +27,14 @@ - `nrepl-client-sentinel` now tears down the SSH tunnel buffer/process when the client connection closes. Previously only the orderly `cider-quit` path killed the tunnel, so an abnormal disconnect (server crash, network drop) left the `ssh` subprocess as a zombie until Emacs exited. - Bound `nrepl-completed-requests` with a FIFO cap (`nrepl-completed-requests-max-size`, default 1000). The completed-request handler table previously grew unbounded for the lifetime of a connection; long-running sessions accumulated thousands of stale handler closures. - [#3909](https://github.com/clojure-emacs/cider/issues/3909): `cider--sesman-friendly-session-p` is more robust at attaching buffers to existing sessions. The classpath and namespace caches are now populated eagerly at connection time instead of lazily on first sesman call (previously a nil/empty result was indistinguishable from "not cached" and was re-fetched on every check). The matcher is now a pure path comparison and never blocks on the REPL. Beyond the caching fix, it also falls back to the connection's `nrepl-project-dir` when classpath matching fails, uses `file-in-directory-p` for classpath-root boundary checks (avoiding spurious prefix matches like `/foo/bar` against `/foo/barber/...`), and short-circuits to the chosen session when `cider-default-session` is set. -- `nrepl-bencode` no longer crashes when handed a non-string scalar (symbol, float, etc.). The documented fallback ("everything else is encoded as string") used `string-bytes` directly, which errors on non-string input; values are now coerced via `format' before measuring byte length. -- [#3915](https://github.com/clojure-emacs/cider/issues/3915): Fix `cider-repl-history` failing with "Unmatched bracket or quote" on its second invocation in a session when the user's history contained an entry with unbalanced parens. `cider-repl-history-setup` now erases the reused `*cider-repl-history*` buffer before re-entering `cider-repl-history-mode`, so any user-configured `clojure-mode-hook` (e.g. one that runs `check-parens') runs on an empty buffer instead of stale content from the previous render. +- `nrepl-bencode` no longer crashes when handed a non-string scalar (symbol, float, etc.). The documented fallback ("everything else is encoded as string") used `string-bytes` directly, which errors on non-string input; values are now coerced via `format` before measuring byte length. +- [#3915](https://github.com/clojure-emacs/cider/issues/3915): Fix `cider-repl-history` failing with "Unmatched bracket or quote" on its second invocation in a session when the user's history contained an entry with unbalanced parens. `cider-repl-history-setup` now erases the reused `*cider-repl-history*` buffer before re-entering `cider-repl-history-mode`, so any user-configured `clojure-mode-hook` (e.g. one that runs `check-parens`) runs on an empty buffer instead of stale content from the previous render. - `cider--completing-read-port` now defaults to `7888` when no running nREPL port can be inferred. ### Changes -- Project root detection no longer goes through `clojure-mode'/`clojure-ts-mode'. New `cider-project-dir' built on top of `project.el' is used instead, with `cider-build-tool-files' as the extra root markers. This works identically whether the user is in `clojure-mode', `clojure-ts-mode', or even a buffer not visiting a Clojure file (e.g. an `M-x cider-connect' from Dired), and respects any `project-find-functions' the user has configured. +- Project root detection no longer goes through `clojure-mode`/`clojure-ts-mode`. New `cider-project-dir` built on top of `project.el` is used instead, with `cider-build-tool-files` as the extra root markers. This works identically whether the user is in `clojure-mode`, `clojure-ts-mode`, or even a buffer not visiting a Clojure file (e.g. an `M-x cider-connect` from Dired), and respects any `project-find-functions` the user has configured. +- The path-based fallback in `cider-expected-ns` no longer delegates to `clojure-expected-ns`. Inline the same algorithm using `cider-project-dir` and a new `cider-directory-prefixes` defcustom (mirroring `clojure-directory-prefixes` but owned by cider). No behavior change for files on the classpath (still preferred) or in a recognized project layout; removes the runtime dependency on `clojure-mode` for ns derivation. - [#710](https://github.com/clojure-emacs/cider-nrepl/issues/710): Use namespaced nREPL ops (e.g. `cider/info` instead of `info`) to match cider-nrepl 0.59+. - Bump the injected `nrepl` to [1.7.0](https://github.com/nrepl/nrepl/blob/master/CHANGELOG.md#170-2026-04-14). - Bump the injected `cider-nrepl` to [0.59.0](https://github.com/clojure-emacs/cider-nrepl/blob/master/CHANGELOG.md#0590-2026-04-14). diff --git a/lisp/cider-client.el b/lisp/cider-client.el index 920047d20..939524500 100644 --- a/lisp/cider-client.el +++ b/lisp/cider-client.el @@ -152,27 +152,59 @@ Remove extension and substitute \"/\" with \".\", \"_\" with \"-\"." (replace-regexp-in-string "/" ".") (replace-regexp-in-string "_" "-"))) +(defcustom cider-directory-prefixes + '("\\`clj[scxd]?\\.") + "Namespace prefixes to strip after deriving a ns from a file path. +Used by `cider-expected-ns' to discard intermediate source directories +that aren't really part of the namespace, e.g. a file at +\"src/clj/foo/bar.clj\" should give the namespace \"foo.bar\" rather +than \"clj.foo.bar\"." + :type '(repeat string) + :group 'cider + :safe (lambda (value) + (and (listp value) + (cl-every #'stringp value)))) + +(defun cider--ns-from-path (path) + "Derive a Clojure namespace from PATH using project layout heuristics. +PATH is expected to be an absolute file path inside a project (see +`cider-project-dir'). The first directory component of the project- +relative path is dropped (typically `src' or `test') and any prefix +matching an entry in `cider-directory-prefixes' is stripped from the +result. Returns nil when PATH isn't inside a recognized project." + (when-let* ((proj (cider-project-dir (file-name-directory path))) + (relative (file-relative-name path proj)) + (drop-first (mapconcat #'identity + (cdr (split-string relative "/")) + "/"))) + (cl-reduce (lambda (acc re) (replace-regexp-in-string re "" acc)) + cider-directory-prefixes + :initial-value (cider-path-to-ns drop-first)))) + (defun cider-expected-ns (&optional path) "Return the namespace string matching PATH, or nil if not found. -If PATH is nil, use the path to the file backing the current buffer. The -command falls back to `clojure-expected-ns' in the absence of an active -nREPL connection." - (if (cider-connected-p) - (let* ((path (file-truename (or path buffer-file-name))) - (relpath (thread-last - (cider-classpath-entries) - (seq-filter #'file-directory-p) - (seq-map (lambda (dir) - (when (file-in-directory-p path dir) - (file-relative-name path dir)))) - (seq-filter #'identity) - (seq-sort (lambda (a b) - (< (length a) (length b)))) - (car)))) - (if relpath - (cider-path-to-ns relpath) - (clojure-expected-ns path))) - (clojure-expected-ns path))) +If PATH is nil, use the path to the file backing the current buffer. + +When an nREPL connection is active, the namespace is preferentially +derived from the connection's classpath entries. Otherwise (or when +PATH isn't on the classpath) it falls back to `cider--ns-from-path', +which uses project layout heuristics." + (when-let* ((path (file-truename (or path buffer-file-name)))) + (if (cider-connected-p) + (let ((relpath (thread-last + (cider-classpath-entries) + (seq-filter #'file-directory-p) + (seq-map (lambda (dir) + (when (file-in-directory-p path dir) + (file-relative-name path dir)))) + (seq-filter #'identity) + (seq-sort (lambda (a b) + (< (length a) (length b)))) + (car)))) + (if relpath + (cider-path-to-ns relpath) + (cider--ns-from-path path))) + (cider--ns-from-path path)))) (defun cider--fallback-op (op connection) "Return the effective op name for OP on CONNECTION. diff --git a/test/cider-client-tests.el b/test/cider-client-tests.el index 0e7b50c00..ba04ab66e 100644 --- a/test/cider-client-tests.el +++ b/test/cider-client-tests.el @@ -243,7 +243,11 @@ (spy-on 'file-in-directory-p :and-call-fake (lambda (file dir) (string-prefix-p dir file))) (spy-on 'file-relative-name :and-call-fake (lambda (file dir) - (substring file (+ 1 (length dir)))))) + (substring file (+ 1 (length dir))))) + ;; The path-based fallback uses `cider-project-dir' to compute a + ;; relative path; stub it so we don't accidentally pick up the + ;; surrounding cider repo as a "project". + (spy-on 'cider-project-dir :and-return-value "/cider--proj/")) (it "returns the namespace matching the given string path" (expect (cider-expected-ns "/cider--a/foo/bar/baz_utils.clj") :to-equal @@ -256,14 +260,22 @@ (expect (cider-expected-ns "/cider--c/foo/bar/baz") :to-equal "foo.bar.baz") (expect (cider-expected-ns "/cider--base/clj-dev/foo/bar.clj") :to-equal - "foo.bar") - (expect (cider-expected-ns "/cider--not/in/classpath.clj") :to-equal - (clojure-expected-ns "/cider--not/in/classpath.clj"))) - - (it "returns nil if it cannot find the namespace" - (expect (cider-expected-ns "/cider--z/abc/def") :to-equal "")) - - (it "falls back on `clojure-expected-ns' in the absence of an active nREPL connection" + "foo.bar")) + + (it "falls back on project-relative path heuristics when the file is not on the classpath" + ;; With our `cider-project-dir' stub returning "/cider--proj/", the + ;; mocked `file-relative-name' makes "/cider--proj/src/foo/bar.clj" + ;; relative to "/cider--proj/" -> "src/foo/bar.clj". The path-based + ;; helper drops the first directory ("src") and produces "foo.bar". + (expect (cider-expected-ns "/cider--proj/src/foo/bar.clj") :to-equal + "foo.bar")) + + (it "strips known directory prefixes after deriving the ns" + ;; Files under e.g. src/clj/... should not produce a "clj." prefix. + (expect (cider-expected-ns "/cider--proj/src/clj/foo/bar.clj") :to-equal + "foo.bar")) + + (it "uses the path-based fallback in the absence of an active nREPL connection" (spy-on 'cider-connected-p :and-return-value nil) - (spy-on 'clojure-expected-ns :and-return-value "clojure-expected-ns") - (expect (cider-expected-ns "foo") :to-equal "clojure-expected-ns"))) + (expect (cider-expected-ns "/cider--proj/src/foo/bar.clj") :to-equal + "foo.bar")))