From 4fe1e80efac9be04c169e450cda2a8e4a27aa0e1 Mon Sep 17 00:00:00 2001 From: ikappaki Date: Sat, 25 May 2024 23:36:11 +0100 Subject: [PATCH] jack-in support for the Basilisp Clojure dialect in Python --- .github/workflows/test.yml | 6 ++ CHANGELOG.md | 2 + cider.el | 46 +++++++- .../ROOT/pages/basics/up_and_running.adoc | 1 + .../ROOT/pages/platforms/basilisp.adoc | 102 ++++++++++++++++++ .../ROOT/pages/platforms/overview.adoc | 1 + test/integration/integration-tests.el | 73 ++++++++++++- 7 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 doc/modules/ROOT/pages/platforms/basilisp.adoc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f54d4caa4..749b9e81d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,6 +100,12 @@ jobs: - run: npm install shadow-cljs@2.20.13 -g - run: npm install nbb@1.1.152 -g + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: | + pip install basilisp==0.1.0b1 + - name: Test integration run: | # The tests occasionally fail on macos&win in what is seems to diff --git a/CHANGELOG.md b/CHANGELOG.md index a3366acb4..f9927cbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - adds `cider-ns-code-reload-tool` defcustom, defaulting to `'tools.namespace`. - you can change it to `'clj-reload` to use [clj-reload](https://github.com/tonsky/clj-reload) instead of [tools.namespace](https://github.com/clojure/tools.namespace). +- [#3682](https://github.com/clojure-emacs/cider/issues/3682): Add `cider-jack-in` support for [Basilisp](https://github.com/basilisp-lang/basilisp) (Python) + ### Changes - [#3626](https://github.com/clojure-emacs/cider/issues/3626): `cider-ns-refresh`: jump to the relevant file/line on errors. diff --git a/cider.el b/cider.el index c54863e6e..c53388083 100644 --- a/cider.el +++ b/cider.el @@ -12,7 +12,7 @@ ;; Maintainer: Bozhidar Batsov ;; URL: https://www.github.com/clojure-emacs/cider ;; Version: 1.14.0-snapshot -;; Package-Requires: ((emacs "26") (clojure-mode "5.18.1") (parseedn "1.2.1") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2") (transient "0.4.1")) +;; Package-Requires: ((emacs "26") (clojure-mode "5.19") (parseedn "1.2.1") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2") (transient "0.4.1")) ;; Keywords: languages, clojure, cider ;; This program is free software: you can redistribute it and/or modify @@ -273,6 +273,30 @@ By default we favor the project-specific shadow-cljs over the system-wide." :safe #'stringp :package-version '(cider . "1.6.0")) +(defcustom cider-basilisp-command + "basilisp" + "The command used to execute Basilisp. + + If Basilisp is installed in a virtual environment, update this to the + full path of the Basilisp executable within that virtual environment." + :type 'string + :safe #'stringp + :package-version '(cider . "1.14.0")) + +(defcustom cider-basilisp-global-options + nil + "Command line options used to execute Basilisp." + :type 'string + :safe #'stringp + :package-version '(cider . "1.14.0")) + +(defcustom cider-basilisp-parameters + "nrepl-server" + "Params passed to Basilisp to start an nREPL server via `cider-jack-in'." + :type 'string + :safe #'stringp + :package-version '(cider . "1.14.0")) + (make-obsolete-variable 'cider-lein-global-options 'cider-lein-parameters "1.8.0") (make-obsolete-variable 'cider-boot-global-options 'cider-boot-parameters "1.8.0") (make-obsolete-variable 'cider-clojure-cli-global-options 'cider-clojure-cli-parameters "1.8.0") @@ -280,6 +304,7 @@ By default we favor the project-specific shadow-cljs over the system-wide." (make-obsolete-variable 'cider-gradle-global-options 'cider-gradle-parameters "1.8.0") (make-obsolete-variable 'cider-babashka-global-options 'cider-babashka-parameters "1.8.0") (make-obsolete-variable 'cider-nbb-global-options 'cider-nbb-parameters "1.8.0") +(make-obsolete-variable 'cider-basilip-global-options 'cider-basilisp-parameters "1.8.0") (defcustom cider-jack-in-default (if (executable-find "clojure") 'clojure-cli 'lein) @@ -296,7 +321,8 @@ to Leiningen." (const shadow-cljs) (const gradle) (const babashka) - (const nbb)) + (const nbb) + (const basilisp)) :safe #'symbolp :package-version '(cider . "0.9.0")) @@ -316,6 +342,7 @@ command when there is no ambiguity." (const gradle) (const babashka) (const nbb) + (const basilisp) (const :tag "Always ask" nil)) :safe #'symbolp :package-version '(cider . "0.13.0")) @@ -380,7 +407,8 @@ Sub-match 1 must be the project path.") '((clojure-cli (:prefix-arg 1 :cmd (:jack-in-type clj :project-type clojure-cli :edit-project-dir t))) (lein (:prefix-arg 2 :cmd (:jack-in-type clj :project-type lein :edit-project-dir t))) (babashka (:prefix-arg 3 :cmd (:jack-in-type clj :project-type babashka :edit-project-dir t))) - (nbb (:prefix-arg 4 :cmd (:jack-in-type cljs :project-type nbb :cljs-repl-type nbb :edit-project-dir t)))) + (nbb (:prefix-arg 4 :cmd (:jack-in-type cljs :project-type nbb :cljs-repl-type nbb :edit-project-dir t))) + (basilisp (:prefix-arg 5 :cmd (:jack-in-type clj :project-type basilisp :edit-project-dir t)))) "The list of project tools that are supported by the universal jack in command. Each item in the list consists of the tool name and its plist options. @@ -412,6 +440,7 @@ The plist supports the following keys ('shadow-cljs cider-shadow-cljs-command) ('gradle cider-gradle-command) ('nbb cider-nbb-command) + ('basilisp cider-basilisp-command) (_ (user-error "Unsupported project type `%S'" project-type)))) (defcustom cider-enrich-classpath nil @@ -476,6 +505,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)) + ('basilisp (cider--resolve-command cider-basilisp-command)) (_ (user-error "Unsupported project type `%S'" project-type)))) (defun cider-jack-in-global-options (project-type) @@ -488,6 +518,7 @@ Throws an error if PROJECT-TYPE is unknown." ('shadow-cljs cider-shadow-cljs-global-options) ('gradle cider-gradle-global-options) ('nbb cider-nbb-global-options) + ('basilisp cider-basilisp-global-options) (_ (user-error "Unsupported project type `%S'" project-type)))) (defun cider-jack-in-params (project-type) @@ -504,6 +535,7 @@ Throws an error if PROJECT-TYPE is unknown." ('shadow-cljs cider-shadow-cljs-parameters) ('gradle cider-gradle-parameters) ('nbb cider-nbb-parameters) + ('basilisp cider-basilisp-parameters) (_ (user-error "Unsupported project type `%S'" project-type)))) @@ -963,6 +995,10 @@ middleware and dependencies." global-opts (unless (seq-empty-p global-opts) " ") params)) + ('basilisp (concat + global-opts + (unless (seq-empty-p global-opts) " ") + params)) (_ (error "Unsupported project type `%S'" project-type)))) @@ -1245,6 +1281,7 @@ you're working on." (const :tag "Shadow w/o Server" shadow-select) (const :tag "Krell" krell) (const :tag "Nbb" nbb) + (const :tag "Basilisp" basilisp) (const :tag "Custom" custom)) :safe #'symbolp :package-version '(cider . "0.17.0")) @@ -2038,7 +2075,8 @@ PROJECT-DIR defaults to current project." (shadow-cljs . "shadow-cljs.edn") (gradle . "build.gradle") (gradle . "build.gradle.kts") - (nbb . "nbb.edn")))) + (nbb . "nbb.edn") + (basilisp . "basilisp.edn")))) (delq nil (mapcar (lambda (candidate) (when (file-exists-p (cdr candidate)) diff --git a/doc/modules/ROOT/pages/basics/up_and_running.adoc b/doc/modules/ROOT/pages/basics/up_and_running.adoc index b45a9a435..b4af70b4f 100644 --- a/doc/modules/ROOT/pages/basics/up_and_running.adoc +++ b/doc/modules/ROOT/pages/basics/up_and_running.adoc @@ -152,6 +152,7 @@ The following Clojure build tools are supported so far - kbd:[M-2 C-c C-x j u] jack-in using leiningen. - kbd:[M-3 C-c C-x j u] jack-in using babashka. - kbd:[M-4 C-c C-x j u] jack-in using nbb. +- kbd:[M-5 C-c C-x j u] jack-in using basilisp. Here is an example of how to bind kbd:[F12] for quickly bringing up a babashka REPL: diff --git a/doc/modules/ROOT/pages/platforms/basilisp.adoc b/doc/modules/ROOT/pages/platforms/basilisp.adoc new file mode 100644 index 000000000..7fc31e061 --- /dev/null +++ b/doc/modules/ROOT/pages/platforms/basilisp.adoc @@ -0,0 +1,102 @@ += Basilisp Integration with CIDER +https://github.com/basilisp-lang/basilisp[basilisp] + +== Overview + +Basilisp aims to enable writing Clojure programs on Python with full Python interoperability. It is highly compatible with Clojure. + +To install Basilisp, run: + + $ pip install basilisp + +== Usage + +There are several ways to connect to Basilisp. + +* kbd:[M-x cider-jack-in] and kbd:[M-5 M-x cider-jack-in-universal] + +If you have created a `basilisp.edn` project file at your root of your project tree, you can jack in to the project `M-x cider-jack-in`. The `basilisp.edn` is similar to `deps.edn` for clojure-cli projects. It can be left empty just to mark the root of your project. + +If you don't have or want a basilisp project file, you can use universal jack in with a numerical argument of 5: + +- kbd:[M-5 M-x cider-jack-in-universal], or +- kbd:[M-5 C-c C-x j u], from within file in clojure-mode + +(Note: an alternative to kbd:[M-5] is kbd:[C-u 5]) + +You can also bind the universal jack-in to Basilisp to a function to use as a shortcut, for example + +[source,lisp] +---- +(global-set-key (kbd "") (lambda () + (interactive) + (cider-jack-in-universal 5))) +---- + +* kbd:[M-x cider-connect] + +You can start its bundled nREPL server: + + $ basilisp nrepl-server + +and connect to it afterward using `M-x cider-connect`. + +To see available options, type `basilisp nrepl-server -h` in a shell prompt. + +== Configuration + +The jack-in command can be configured via several defcustoms: + +* `cider-basilisp-command` (default is `basilisp`). + +If Basilisp is installed in a virtual environment, update this to the full path of the `basilisp` executable within that virtual environment. + +* `cider-basilisp-parameters` (default is `nrepl-server`). + +There at few ways to setup (custom) variables in Emacs + +- https://www.gnu.org/software/emacs/manual/html_node/emacs/Easy-Customization.html[Examining and Setting Variables] + +kbd:[C-h v cider-basilisp-command], and +kbd:[C-h v cider-basilisp-parameters] + +- https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html[Per-Diretory Local Variables] + +Uses `.dir-locals.el` to setup per mode variables. This file is typically stored at the root of the project. + +For example, to set the path to the basilisp executable within a virtual environment + +kbd:[M-x add-dir-local-variable] +Mode or subdirectory: `clojure-mode` +Add directory-local variable: `cider-basilisp-command` +Add cider-basilisp-command with value: `"c:/dev/venvs/312/Scripts/basilisp"` + +This should result to updating or creating a `.dir-local.el` file like below + +[source,lisp] +---- +;;; Directory Local Variables -*- no-byte-compile: t -*- +;;; For more information see (info "(emacs) Directory Variables") + +((clojure-mode . ((cider-basilisp-command . "c:/dev/venvs/312/Scripts/basilisp")))) +---- + +- https://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html[Specifying File Variables] + +It is best to put this in the top of your project's `basilisp.edn` file, and always jack-in from there + +For example, setting `cider-basilisp-command` to start basilisp from within a virtual environment + +kbd:[M-x add-dir-local-variable] +Add file-local variable: `cider-basilisp-command` +Add cider-basilisp-command with value: `"c:/dev/venvs/312/Scripts/basilisp"` + +This will result in the following in `basilisp.edn` + +[source,clojure] +---- +;; Local Variables: +;; cider-basilisp-command: "d:/dev/venvs/cider/312/Scripts/basilisp" +;; End: +{} +---- diff --git a/doc/modules/ROOT/pages/platforms/overview.adoc b/doc/modules/ROOT/pages/platforms/overview.adoc index 1903cd719..f1a7c14db 100644 --- a/doc/modules/ROOT/pages/platforms/overview.adoc +++ b/doc/modules/ROOT/pages/platforms/overview.adoc @@ -30,6 +30,7 @@ Right now CIDER the supports to some extent the following: * xref:platforms/clojureclr.adoc[ClojureCLR] * Lumo (via https://github.com/djblue/nrepl-cljs) * xref:platforms/other_platforms.adoc[scittle, joyride & friends] +* xref:platforms/basilisp.adoc[Basilisp] All of them are derived from Clojure, so supporting them didn't really require much work. diff --git a/test/integration/integration-tests.el b/test/integration/integration-tests.el index e0ed66b79..91722edad 100644 --- a/test/integration/integration-tests.el +++ b/test/integration/integration-tests.el @@ -183,6 +183,73 @@ If CLI-COMMAND is nil, then use the default." (cider-itu-poll-until (not (eq (process-status nrepl-proc) 'run)) 5) (expect (member (process-status nrepl-proc) '(exit signal)))))))))) + (it "to Basilisp" + ;; temporarily suspended on MS-Windows until the following is released on PyPi + ;; + ;; https://github.com/basilisp-lang/basilisp/pull/866 + (assume (not (eq system-type 'windows-nt)) "temporarily skipping on MS-Windows ...") + (with-cider-test-sandbox + (with-temp-dir temp-dir + ;; Create a project in temp dir + (let* ((project-dir temp-dir) + (basilisp-edn (expand-file-name "basilisp.edn" project-dir))) + (write-region "" nil basilisp-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 '())) + (nrepl-buf (process-buffer nrepl-proc))) + + ;; wait until the client successfully connects to the nREPL + ;; server. A high timeout is set because Basilisp usually needs to + ;; be compiled the first time it is run. + (cider-itu-poll-until (eq (gv-deref client-is-connected*) 'connected) 60) + + ;; 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 :basilisp? (some? sys/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 '()) + ;; The Basilisp nREPL server sends the message in three separate + ;; pieces, which is likely an area for improvement on the + ;; Basilisp side. + (expect eval-out :to-equal '("true" " " ":basilisp?")) + + ;; 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 clojure tools cli (default)" (jack-in-clojure-cli-test nil)) @@ -190,9 +257,9 @@ If CLI-COMMAND is nil, then use the default." (it "to clojure tools cli (alternative pwsh)" (jack-in-clojure-cli-test "pwsh"))) -(when (eq system-type 'windows-nt) - (it "to clojure tools cli (alternative deps.exe)" - (jack-in-clojure-cli-test "deps.exe"))) + (when (eq system-type 'windows-nt) + (it "to clojure tools cli (alternative deps.exe)" + (jack-in-clojure-cli-test "deps.exe"))) (it "to leiningen" (with-cider-test-sandbox