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
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand Down
70 changes: 51 additions & 19 deletions lisp/cider-client.el
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 23 additions & 11 deletions test/cider-client-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")))
Loading