diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91fd0cd6b..fb8701862 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,6 +67,7 @@ jobs: with: node-version: 16 - run: npm install shadow-cljs@2.20.13 -g + - run: npm install nbb@1.1.152 -g - name: Test integration run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c761d94b..2a4fe659b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New features - [#3278](https://github.com/clojure-emacs/cider/pull/3278) Introduce integration tests, which also fix a long standing issue with orphaned process on MS-Windows by contracting `taskkill`, if available, to properly kill the nREPL server process tree. +- [#3061](https://github.com/clojure-emacs/cider/issues/3061): Add support for nbb. - [#3249](https://github.com/clojure-emacs/cider/pull/3249): Add support for Clojure Spec 2. - [#3247](https://github.com/clojure-emacs/cider/pull/3247): Add the `cider-stacktrace-analyze-at-point` and `cider-stacktrace-analyze-in-region` commands to view printed exceptions in the stacktrace inspector. diff --git a/cider.el b/cider.el index dc44e1cfc..e7283bc69 100644 --- a/cider.el +++ b/cider.el @@ -12,7 +12,7 @@ ;; Maintainer: Bozhidar Batsov ;; URL: http://www.github.com/clojure-emacs/cider ;; Version: 1.6.0-snapshot -;; Package-Requires: ((emacs "26") (clojure-mode "5.15.1") (parseedn "1.0.6") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2")) +;; Package-Requires: ((emacs "26") (clojure-mode "5.16.0") (parseedn "1.0.6") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2")) ;; Keywords: languages, clojure, cider ;; This program is free software: you can redistribute it and/or modify @@ -230,6 +230,27 @@ By default we favor the project-specific shadow-cljs over the system-wide." :safe #'stringp :package-version '(cider . "1.2.0")) +(defcustom cider-nbb-command + "nbb" + "The command used to execute nbb." + :type 'string + :safe #'stringp + :package-version '(cider . "1.3.0")) + +(defcustom cider-nbb-global-options + nil + "Command line options used to execute nbb." + :type 'string + :safe #'stringp + :package-version '(cider . "1.3.0")) + +(defcustom cider-nbb-parameters + "nrepl-server" + "Params passed to nbb to start an nREPL server via `cider-jack-in'." + :type 'string + :safe #'stringp + :package-version '(cider . "1.3.0")) + (defcustom cider-jack-in-default (if (executable-find "clojure") 'clojure-cli 'lein) "The default tool to use when doing `cider-jack-in' outside a project. This value will only be consulted when no identifying file types, i.e. @@ -243,7 +264,8 @@ to Leiningen." (const clojure-cli) (const shadow-cljs) (const gradle) - (const babashka)) + (const babashka) + (const nbb)) :safe #'symbolp :package-version '(cider . "0.9.0")) @@ -262,6 +284,7 @@ command when there is no ambiguity." (const shadow-cljs) (const gradle) (const babashka) + (const nbb) (const :tag "Always ask" nil)) :safe #'symbolp :package-version '(cider . "0.13.0")) @@ -337,6 +360,7 @@ Sub-match 1 must be the project path.") ('babashka cider-babashka-command) ('shadow-cljs cider-shadow-cljs-command) ('gradle cider-gradle-command) + ('nbb cider-nbb-command) (_ (user-error "Unsupported project type `%S'" project-type)))) (defun cider-jack-in-resolve-command (project-type) @@ -357,6 +381,7 @@ Throws an error if PROJECT-TYPE is unknown." ;; relative path like "./gradlew" use locate file instead of checking ;; the exec-path ('gradle (cider--resolve-project-command cider-gradle-command)) + ('nbb (cider--resolve-command cider-nbb-command)) (_ (user-error "Unsupported project type `%S'" project-type)))) (defun cider-jack-in-global-options (project-type) @@ -368,6 +393,7 @@ Throws an error if PROJECT-TYPE is unknown." ('babashka cider-babashka-global-options) ('shadow-cljs cider-shadow-cljs-global-options) ('gradle cider-gradle-global-options) + ('nbb cider-nbb-global-options) (_ (user-error "Unsupported project type `%S'" project-type)))) (defun cider-jack-in-params (project-type) @@ -383,6 +409,7 @@ Throws an error if PROJECT-TYPE is unknown." ('babashka cider-babashka-parameters) ('shadow-cljs cider-shadow-cljs-parameters) ('gradle cider-gradle-parameters) + ('nbb cider-nbb-parameters) (_ (user-error "Unsupported project type `%S'" project-type)))) @@ -766,6 +793,10 @@ dependencies." (cider-add-clojure-dependencies-maybe cider-jack-in-dependencies) (cider-jack-in-normalized-nrepl-middlewares))) + ('nbb (concat + global-opts + (unless (seq-empty-p global-opts) " ") + params)) (_ (error "Unsupported project type `%S'" project-type)))) @@ -993,21 +1024,26 @@ The supplied string will be wrapped in a do form if needed." (def config (edn/read-string (slurp (io/file \"build.edn\")))) (apply cider.piggieback/cljs-repl (krell.repl/repl-env) (mapcat identity config))" cider-check-krell-requirements) + ;; native cljs repl, no form required. + (nbb) (custom cider-custom-cljs-repl-init-form nil)) "A list of supported ClojureScript REPLs. -For each one we have its name, the form we need to evaluate in a Clojure -REPL to start the ClojureScript REPL and functions to verify their requirements. +For each one we have its name, and then, if the repl is not a native +ClojureScript REPL, the form we need to evaluate in a Clojure REPL to +switch to the ClojureScript REPL and functions to verify their +requirements. -The form should be either a string or a function producing a string.") +The form, if any, should be either a string or a function producing a +string.") -(defun cider-register-cljs-repl-type (type init-form &optional requirements-fn) +(defun cider-register-cljs-repl-type (type &optional init-form requirements-fn) "Register a new ClojureScript REPL type. Types are defined by the following: - TYPE - symbol identifier that will be used to refer to the REPL type -- INIT-FORM - string or function (symbol) producing string +- INIT-FORM - (optional) string or function (symbol) producing string - REQUIREMENTS-FN - function to check whether the REPL can be started. This param is optional. @@ -1015,8 +1051,8 @@ All this function does is modifying `cider-cljs-repl-types'. It's intended to be used in your Emacs config." (unless (symbolp type) (user-error "The REPL type must be a symbol")) - (unless (or (stringp init-form) (symbolp init-form)) - (user-error "The init form must be a string or a symbol referring to a function")) + (unless (or (null init-form) (stringp init-form) (symbolp init-form)) + (user-error "The init form must be a string or a symbol referring to a function or nil")) (unless (or (null requirements-fn) (symbolp requirements-fn)) (user-error "The requirements-fn must be a symbol referring to a function")) (add-to-list 'cider-cljs-repl-types (list type init-form requirements-fn))) @@ -1036,6 +1072,7 @@ you're working on." (const :tag "Shadow" shadow) (const :tag "Shadow w/o Server" shadow-select) (const :tag "Krell" krell) + (const :tag "Nbb" nbb) (const :tag "Custom" custom)) :safe #'symbolp :package-version '(cider . "0.17.0")) @@ -1054,15 +1091,16 @@ DEFAULT is the default ClojureScript REPL to offer in completion." (or default (car cider--select-cljs-repl-history)))))) (defun cider-cljs-repl-form (repl-type) - "Get the cljs REPL form for REPL-TYPE." - (if-let* ((repl-form (cadr (seq-find - (lambda (entry) - (eq (car entry) repl-type)) - cider-cljs-repl-types)))) - ;; repl-form can be either a string or a function producing a string - (if (symbolp repl-form) - (funcall repl-form) - repl-form) + "Get the cljs REPL form for REPL-TYPE, if any." + (if-let* ((repl-type-info (seq-find + (lambda (entry) + (eq (car entry) repl-type)) + cider-cljs-repl-types))) + (when-let ((repl-form (cadr repl-type-info))) + ;; repl-form can be either a string or a function producing a string + (if (symbolp repl-form) + (funcall repl-form) + repl-form)) (user-error "No ClojureScript REPL type %s found. Please make sure that `cider-cljs-repl-types' has an entry for it" repl-type))) (defun cider-verify-cljs-repl-requirements (&optional repl-type) @@ -1258,8 +1296,7 @@ server buffer, in which case a new session for that server is created." (cider--update-cljs-type) (cider--update-cljs-init-function) (plist-put :session-name ses-name) - (plist-put :repl-type 'cljs) - (plist-put :cider-repl-cljs-upgrade-pending t))))) + (plist-put :repl-type 'cljs))))) ;;;###autoload (defun cider-connect-clj (&optional params) @@ -1293,8 +1330,7 @@ parameters regardless of their supplied or default values." (cider--update-cljs-type) (cider--update-cljs-init-function) (plist-put :session-name nil) - (plist-put :repl-type 'cljs) - (plist-put :cider-repl-cljs-upgrade-pending t)))) + (plist-put :repl-type 'cljs)))) ;;;###autoload (defun cider-connect-clj&cljs (params &optional soft-cljs-start) @@ -1466,27 +1502,41 @@ non-nil, don't start if ClojureScript requirements are not met." (plist-put :port (cdr endpoint))))))) (defun cider--update-cljs-init-function (params) - "Update PARAMS :repl-init-function for cljs connections." + "Update repl type and any init PARAMS for cljs connections. + +The updated params are: + +:cider-repl-cljs-upgrade-pending nil if it is a cljs REPL, or t +when the init form is required to be sent to the REPL to switch +over to cljs. + +:repl-init-form The form that can switch the REPL over to cljs. + +:repl-init-function The fn that switches the REPL over to cljs." (with-current-buffer (or (plist-get params :--context-buffer) (current-buffer)) (let* ((cljs-type (plist-get params :cljs-repl-type)) (repl-init-form (cider-cljs-repl-form cljs-type))) - (thread-first - params - (plist-put :repl-init-function - (lambda () - (cider--check-cljs cljs-type) - ;; FIXME: ideally this should be done in the state handler - (setq-local cider-cljs-repl-type cljs-type) - (cider-nrepl-send-request - (list "op" "eval" - "ns" (cider-current-ns) - "code" repl-init-form) - (cider-repl-handler (current-buffer))) - (when (and (buffer-live-p nrepl-server-buffer) - cider-offer-to-open-cljs-app-in-browser) - (cider--offer-to-open-app-in-browser nrepl-server-buffer)))) - (plist-put :repl-init-form repl-init-form))))) + (if (null repl-init-form) + (plist-put params :cider-repl-cljs-upgrade-pending nil) + + (thread-first + params + (plist-put :cider-repl-cljs-upgrade-pending t) + (plist-put :repl-init-function + (lambda () + (cider--check-cljs cljs-type) + ;; FIXME: ideally this should be done in the state handler + (setq-local cider-cljs-repl-type cljs-type) + (cider-nrepl-send-request + (list "op" "eval" + "ns" (cider-current-ns) + "code" repl-init-form) + (cider-repl-handler (current-buffer))) + (when (and (buffer-live-p nrepl-server-buffer) + cider-offer-to-open-cljs-app-in-browser) + (cider--offer-to-open-app-in-browser nrepl-server-buffer)))) + (plist-put :repl-init-form repl-init-form)))))) (defun cider--check-existing-session (params) "Ask for confirmation if a session with similar PARAMS already exists. @@ -1666,7 +1716,8 @@ PROJECT-DIR defaults to current project." (babashka . "bb.edn") (shadow-cljs . "shadow-cljs.edn") (gradle . "build.gradle") - (gradle . "build.gradle.kts")))) + (gradle . "build.gradle.kts") + (nbb . "nbb.edn")))) (delq nil (mapcar (lambda (candidate) (when (file-exists-p (cdr candidate)) diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 599eb069a..63802644d 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -13,6 +13,7 @@ * Alternative Platforms ** xref:platforms/overview.adoc[Overview] ** xref:platforms/babashka.adoc[Babashka] +** xref:platforms/nbb.adoc[Nbb] ** xref:platforms/scittle_and_friends.adoc[Scittle and Friends] * Using CIDER ** xref:usage/interactive_programming.adoc[Interactive Programming] diff --git a/doc/modules/ROOT/pages/platforms/nbb.adoc b/doc/modules/ROOT/pages/platforms/nbb.adoc new file mode 100644 index 000000000..4a21fd26e --- /dev/null +++ b/doc/modules/ROOT/pages/platforms/nbb.adoc @@ -0,0 +1,13 @@ += https://github.com/babashka/nbb[nbb] + +Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js. + +It is highly compatible with ClojureScript, so it works with CIDER out of the box. + +You can either jack in to an nbb project with `M-x clojure-jack-in-cljs`. + +or start its bundled nREPL server: + + $ nbb nrepl-server + +and connect to it afterwards using `M-x cider-connect-cljs`. diff --git a/doc/modules/ROOT/pages/platforms/overview.adoc b/doc/modules/ROOT/pages/platforms/overview.adoc index d0d94bc1f..aafd0ae22 100644 --- a/doc/modules/ROOT/pages/platforms/overview.adoc +++ b/doc/modules/ROOT/pages/platforms/overview.adoc @@ -28,6 +28,7 @@ Right now CIDER the supports to some extent the following: * Babashka * ClojureCLR (via Arcadia's nREPL server) * Lumo (via https://github.com/djblue/nrepl-cljs) +* Nbb All of them are derived from Clojure, so supporting them didn't really require much work. diff --git a/test/cider-tests.el b/test/cider-tests.el index 231ee49e5..ecb6cabbc 100644 --- a/test/cider-tests.el +++ b/test/cider-tests.el @@ -579,6 +579,71 @@ (expect (cider--resolve-project-command "command") :to-equal (shell-quote-argument "/bin/command")))) +(describe "cider-connect-sibling-cljs" + ;; restore: + ;; - `cider-cljs-repl-types` changed by `cider-register-cljs-repl-type`. + :var (-cider-cljs-repl-types) + (before-all + (setq -cider-cljs-repl-types cider-cljs-repl-types)) + (after-each + (setq cider-cljs-repl-types -cider-cljs-repl-types)) + + (describe "sets nrepl client local vars correctly" + (it "for nbb project" + (let* ((server-process (nrepl-start-mock-server-process)) + (server-buffer (process-buffer server-process)) + (client-buffer (cider-connect-sibling-cljs + '(:cljs-repl-type nbb) server-buffer))) + ;; native cljs REPL + (expect (buffer-local-value 'cider-repl-type client-buffer) + :to-equal 'cljs) + (expect (buffer-local-value 'cider-repl-cljs-upgrade-pending client-buffer) + :to-equal nil) + (expect (buffer-local-value 'cider-repl-init-function client-buffer) + :to-be nil) + (delete-process (get-buffer-process client-buffer)))) + (it "for shadow project" + (let* ((cider-shadow-default-options "a-shadow-alias") + (server-process (nrepl-start-mock-server-process)) + (server-buffer (process-buffer server-process)) + (client-buffer (cider-connect-sibling-cljs + '(:cljs-repl-type shadow) server-buffer))) + ;; starts as clj REPL and requires a form to switch over to cljs + (expect (buffer-local-value 'cider-repl-type client-buffer) + :to-equal 'cljs) + (expect (buffer-local-value 'cider-repl-cljs-upgrade-pending client-buffer) + :to-equal t) + (expect (buffer-local-value 'cider-repl-init-function client-buffer) + :not :to-be nil) + (delete-process (get-buffer-process client-buffer)))) + (it "for a custom cljs REPL type project" + (cider-register-cljs-repl-type 'native-cljs) + (let* ((server-process (nrepl-start-mock-server-process)) + (server-buffer (process-buffer server-process)) + (client-buffer (cider-connect-sibling-cljs + '(:cljs-repl-type native-cljs) + server-buffer))) + (expect (buffer-local-value 'cider-repl-type client-buffer) + :to-equal 'cljs) + (expect (buffer-local-value 'cider-repl-cljs-upgrade-pending client-buffer) + :to-equal nil) + (delete-process (get-buffer-process client-buffer)))) + (it "for a custom REPL type project that needs to switch to cljs" + (cider-register-cljs-repl-type + 'not-cljs-initially "(form-to-switch-to-cljs-repl)") + (let* ((server-process (nrepl-start-mock-server-process)) + (server-buffer (process-buffer server-process)) + (client-buffer (cider-connect-sibling-cljs + '(:cljs-repl-type not-cljs-initially) + server-buffer))) + (expect (buffer-local-value 'cider-repl-type client-buffer) + :to-equal 'cljs) + (expect (buffer-local-value 'cider-repl-cljs-upgrade-pending client-buffer) + :to-equal t) + (expect (buffer-local-value 'cider-repl-init-function client-buffer) + :not :to-be nil) + (delete-process (get-buffer-process client-buffer)))))) + (provide 'cider-tests) ;;; cider-tests.el ends here diff --git a/test/integration/integration-tests.el b/test/integration/integration-tests.el index b4f76022a..27e0448bd 100644 --- a/test/integration/integration-tests.el +++ b/test/integration/integration-tests.el @@ -216,6 +216,65 @@ (cider-itu-poll-until (not (eq (process-status nrepl-proc) 'run)) 15) (expect (member (process-status nrepl-proc) '(exit signal)))))))))) + (it "to nbb" + (with-cider-test-sandbox + (with-temp-dir temp-dir + ;; Create a project in temp dir + (let* ((project-dir temp-dir) + (nbb-edn (expand-file-name "nbb.edn" project-dir))) + (write-region "{}" nil nbb-edn) + + (with-temp-buffer + ;; set default directory to temp project + (setq-local default-directory project-dir) + + (let* (;; Get a gv reference so as to poll if the client has + ;; connected to the nREPL server. + (client-is-connected* (cider-itu-nrepl-client-connected-ref-make!)) + + ;; jack in and get repl buffer + (nrepl-proc (cider-jack-in-clj '(:cljs-repl-type nbb))) + (nrepl-buf (process-buffer nrepl-proc))) + + ;; wait until the client has successfully connected to the + ;; nREPL server. + (cider-itu-poll-until (eq (gv-deref client-is-connected*) 'connected) 5) + + ;; give it some time to setup the clj REPL + (cider-itu-poll-until (cider-repls 'clj nil) 5) + + ;; send command to the REPL, and push stdout/stderr to + ;; corresponding eval-xxx variables. + (let ((repl-buffer (cider-current-repl)) + (eval-err '()) + (eval-out '())) + (expect repl-buffer :not :to-be nil) + + ;; send command to the REPL + (cider-interactive-eval + ;; ask REPL to return a string that uniquely identifies it. + "(print :nbb? (some? (nbb.core/version)))" + (lambda (return) + (nrepl-dbind-response + return + (out err) + (when err (push err eval-err)) + (when out (push out eval-out)))) ) + + ;; wait for a response to come back. + (cider-itu-poll-until (or eval-err eval-out) 5) + + ;; ensure there are no errors and response is as expected. + (expect eval-err :to-equal '()) + (expect eval-out :to-equal '(":nbb? true")) + + ;; exit the REPL. + (cider-quit repl-buffer) + + ;; wait for the REPL to exit + (cider-itu-poll-until (not (eq (process-status nrepl-proc) 'run)) 5) + (expect (member (process-status nrepl-proc) '(exit signal)))))))))) + (it "to shadow" ;; shadow asks user whether they want to open a browser, force to no (spy-on 'y-or-n-p) diff --git a/test/utils/nrepl-tests-utils.el b/test/utils/nrepl-tests-utils.el index e7a4a1716..549476d2d 100644 --- a/test/utils/nrepl-tests-utils.el +++ b/test/utils/nrepl-tests-utils.el @@ -25,6 +25,8 @@ ;;; Code: +(require 'nrepl-client) + (defmacro nrepl-tests-log/init! (enable? name log-filename &optional clean?) "Create a NAME/log! elisp function to log messages to LOG-FILENAME, taking the same arguments as `message'. Messages are appended to @@ -97,5 +99,20 @@ calling process." ;; invoke mock server " -l test/nrepl-server-mock.el -f nrepl-server-mock-start")) +(defun nrepl-start-mock-server-process () + "Start and return the mock nrepl server process." + (let* ((up? nil) + (server-process (nrepl-start-server-process + default-directory + (nrepl-server-mock-invocation-string) + (lambda (server-buffer) + (setq up? t)))) + server-buffer (process-buffer server-process)) + ;; server has reported its endpoint + (nrepl-tests-sleep-until 2 up?) + server-process)) (provide 'nrepl-tests-utils) + +;;; nrepl-tests-utils.el ends here +