diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..f03a87f --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,120 @@ +version: 2.1 + +orbs: + win: circleci/windows@2.2.0 + +# Default actions to perform on each Emacs version +commands: + setup: + steps: + - checkout + - run: + name: Install Eldev + command: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/circle-eldev > x.sh && source ./x.sh + + macos-setup: + steps: + - checkout + - run: + name: Install Emacs latest + command: | + echo "HOMEBREW_NO_AUTO_UPDATE=1" >> $BASH_ENV + brew install homebrew/cask/emacs + - run: + name: Install Eldev + command: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/circle-eldev > x.sh && source ./x.sh + + setup-windows: + steps: + - checkout + - run: + name: Install Eldev + command: | + # Remove expired DST Root CA X3 certificate. Workaround + # for https://debbugs.gnu.org/cgi/bugreport.cgi?bug=51038 + # bug on Emacs 27.2. + gci cert:\LocalMachine\Root\DAC9024F54D8F6DF94935FB1732638CA6AD77C13 + gci cert:\LocalMachine\Root\DAC9024F54D8F6DF94935FB1732638CA6AD77C13 | Remove-Item + (iwr https://raw.github.com/doublep/eldev/master/webinstall/circle-eldev.ps1).Content | powershell -command - + test: + steps: + - run: + name: Run regression tests + command: eldev -dtT -p test + lint: + steps: + - run: + name: Run Elisp-lint + command: eldev lint + - run: + name: Byte-compile `.el' files + command: eldev -dtT compile --warnings-as-errors + +jobs: + test-ubuntu-emacs-28: + docker: + - image: silex/emacs:28-ci + entrypoint: bash + steps: + - setup + - test + + test-ubuntu-emacs-29: + docker: + - image: silex/emacs:29-ci + entrypoint: bash + steps: + - setup + - test + + test-ubuntu-emacs-30: + docker: + - image: silex/emacs:30-ci + entrypoint: bash + steps: + - setup + - test + + test-ubuntu-emacs-master: + docker: + - image: silex/emacs:master-ci + entrypoint: bash + steps: + - setup + - test + + test-macos-emacs-latest: + macos: + xcode: "14.0.0" + steps: + - macos-setup + - test + + test-windows-emacs-latest: + executor: win/default + steps: + - run: + name: Install Emacs latest + command: | + choco install emacs + - setup-windows + - test + + test-lint: + docker: + - image: silex/emacs:30-ci + steps: + - setup + - lint + +workflows: + version: 2 + ci-test-matrix: + jobs: + - test-ubuntu-emacs-28 + - test-ubuntu-emacs-29 + - test-ubuntu-emacs-30 + - test-ubuntu-emacs-master + - test-lint + - test-macos-emacs-latest + - test-windows-emacs-latest diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7c87286 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +github: bbatsov +patreon: bbatsov +custom: https://www.paypal.me/bbatsov diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e62e23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*~ +*\#*\# +*.\#* +*.elc +.cask +elpa* +.depend +TAGS +.DS_STORE +dist +.eldev +.vagrant/ +.dir-locals?.el + +# ELPA-generated files +/inf-clojure-autoloads.el +/inf-clojure-pkg.el diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fb15a..f3c3e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,60 +2,146 @@ ## master (unreleased) +## 3.3.0 (2025-05-25) + +### New features + +- [#202](https://github.com/clojure-emacs/inf-clojure/issues/202): Add ClojureCLR support. +- [#210](https://github.com/clojure-emacs/inf-clojure/pull/210) Include `inf-clojure-socket-repl` to create a socket REPL and connect to it from inside Emacs. +- [#217](https://github.com/clojure-emacs/inf-clojure/pull/217): Add `clojure-ts-mode` support. + +### Changes + +- Improve support for multiple forms in the same line by replacing `beginning-of-defun` fn. +- [#204](https://github.com/clojure-emacs/inf-clojure/issues/204): Scroll repl buffer on insert commands +- [#208](https://github.com/clojure-emacs/inf-clojure/pull/208) Display message after setting REPL. +- Require Emacs 28. +- Drop support for Lumo. + +## 3.2.1 (2022-07-22) + +### Bugs fixed + +- Address some small issues with NonGNU ELPA (e.g. missing maintainer metadata). + +## 3.2.0 (2022-07-15) + +### New features + +- [#168](https://github.com/clojure-emacs/inf-clojure/pull/197): New helper function `inf-clojure-switch-to-recent-buffer` to select the last buffer an inf-clojure process buffer was swapped to from. +- [#187](https://github.com/clojure-emacs/inf-clojure/pull/197): New defcustom `inf-clojure-enable-eldoc` to disable eldoc interaction. + +### Bugs fixed + +- [#185](https://github.com/clojure-emacs/inf-clojure/issues/185): Improve cmd string splitting. +- [#193](https://github.com/clojure-emacs/inf-clojure/pull/193): Set syntax table in REPL buffer. +- Fix `inf-clojure-display-version` (it wasn't extracting properly the package version). + +## 3.1.0 (2021-07-23) + +### New features + +- [#190](https://github.com/clojure-emacs/inf-clojure/pull/190): Helper function `inf-clojure-set-repl` to select inf-clojure process buffer. +- Auto-enable `inf-clojure-minor-mode` after invoking `inf-clojure`. This behaviour is controlled via `inf-clojure-auto-mode`. +- Include the project name automatically in the REPL buffer name. + +### Bugs fixed + +- [#152](https://github.com/clojure-emacs/inf-clojure/issues/152): Sanitize should only remove whitespace at the end of a command. +- [#188](https://github.com/clojure-emacs/inf-clojure/pull/188): Handle newlines between forms for `inf-clojure-eval-buffer`. +- [#189](https://github.com/clojure-emacs/inf-clojure/pull/189): Font-lock code inserted in the REPL from a source buffer. + +## 3.0.0 (2020-08-01) + +### New features + +- [#174](https://github.com/clojure-emacs/inf-clojure/pull/174): Invoke `inf-clojure` with a prefix argument to prevent using `inf-clojure-custom-startup` and `inf-clojure-custom-repl-type`. +- Made it possible to add user-defined REPL types (by modifying `inf-clojure-repl-features`). + +### Changes + +- **(Breaking)*- Restructure massively the configuration. See `inf-clojure-repl-features` for details. +- [#174](https://github.com/clojure-emacs/inf-clojure/pull/174): Set REPL type from startup form or prompt at startup, introduce `inf-clojure-custom-repl-type` defcustom. +- [#173](https://github.com/clojure-emacs/inf-clojure/issues/173): Use clojure-mode's project detection instead of duplicate version in inf-clojure. + +### Bugs fixed + +- [#178](https://github.com/clojure-emacs/inf-clojure/issues/178): Ensure a valid directory is used when starting process. + +## 2.2.0 (2020-04-15) + +### New features + +- [#170](https://github.com/clojure-emacs/inf-clojure/pull/170): Add insert defun and last sexp commands. +- [#160](https://github.com/clojure-emacs/inf-clojure/pull/160): Support [Joker](https://joker-lang.org/). + +### Bugs fixed + +- [#164](https://github.com/clojure-emacs/inf-clojure/pull/164): Fix for eldoc-mode on ClojureCLR. +- [#135](https://github.com/clojure-emacs/inf-clojure/pull/135): Improve command sanitation code. +- Fix `info-clojure-apropos`. + ## 2.1.0 (2018-01-02) ### New Features -* [#114](https://github.com/clojure-emacs/inf-clojure/pull/114): Introduce `inf-clojure-project-type` defcustom. -* [#117](https://github.com/clojure-emacs/inf-clojure/pull/117): Introduce `tools.deps` project type and `inf-clojure-tools-deps-cmd`. -* [#122](https://github.com/clojure-emacs/inf-clojure/pull/122): Introduce `inf-clojure-completions-fn` defcustom. +- [#114](https://github.com/clojure-emacs/inf-clojure/pull/114): Introduce `inf-clojure-project-type` defcustom. +- [#117](https://github.com/clojure-emacs/inf-clojure/pull/117): Introduce `tools.deps` project type and `inf-clojure-tools-deps-cmd`. +- [#122](https://github.com/clojure-emacs/inf-clojure/pull/122): Introduce `inf-clojure-completions-fn` defcustom. +- [#128](https://github.com/clojure-emacs/inf-clojure/pull/128): Expose `inf-clojure-apropos` as `C-c C-S-a` in `inf-clojure-mode` (the REPL). +- [#125](https://github.com/clojure-emacs/inf-clojure/pull/125): Avoid throwing an error for frequent operations like completion. +- [#130](https://github.com/clojure-emacs/inf-clojure/pull/130): Support loading directory locals in our buffers. +- [#129](https://github.com/clojure-emacs/inf-clojure/pull/129): Improve the completion bounds detection (now with keywords). +- [#132](https://github.com/clojure-emacs/inf-clojure/pull/132): Introduce inf-clojure-reload. ### Bugs Fixed -* [#79](https://github.com/clojure-emacs/inf-clojure/pull/82): Eldoc error when running boot repl. -* [#83](https://github.com/clojure-emacs/inf-clojure/pull/85): No such namespace: complete.core in lumo REPL. -* [#93](https://github.com/clojure-emacs/inf-clojure/pull/93): Slow response from inf-clojure (completions, arglists, ...). -* [#101](https://github.com/clojure-emacs/inf-clojure/pull/101): `inf-clojure-set-ns` hangs Emacs. -* [#119](https://github.com/clojure-emacs/inf-clojure/pull/119): Set inf-clojure-buffer REPL type on detect. -* [#120](https://github.com/clojure-emacs/inf-clojure/pull/120): Send REPL string always, even if empty. +- [#79](https://github.com/clojure-emacs/inf-clojure/pull/82): Eldoc error when running boot repl. +- [#83](https://github.com/clojure-emacs/inf-clojure/pull/85): No such namespace: complete.core in lumo REPL. +- [#93](https://github.com/clojure-emacs/inf-clojure/pull/93): Slow response from inf-clojure (completions, arglists, ...). +- [#101](https://github.com/clojure-emacs/inf-clojure/pull/101): `inf-clojure-set-ns` hangs Emacs. +- [#119](https://github.com/clojure-emacs/inf-clojure/pull/119): Set inf-clojure-buffer REPL type on detect. +- [#120](https://github.com/clojure-emacs/inf-clojure/pull/120): Send REPL string always, even if empty. +- [#128](https://github.com/clojure-emacs/inf-clojure/pull/128): Fix inf-clojure-apropos. +- [#131](https://github.com/clojure-emacs/inf-clojure/pull/131): Add macroexpand forms for Lumo. ## 2.0.1 (2017-05-18) ### Bugs Fixed -* [#77](https://github.com/clojure-emacs/inf-clojure/pull/77): Fix request "Eval expression:" if arglists return is `nil`. +- [#77](https://github.com/clojure-emacs/inf-clojure/pull/77): Fix request "Eval expression:" if arglists return is `nil`. ## 2.0.0 (2017-05-01) ### New Features -* [#63](https://github.com/clojure-emacs/inf-clojure/pull/69): Fix spurious process output on init. -* [#57](https://github.com/clojure-emacs/inf-clojure/pull/68): Add `inf-clojure-connect`. -* [#66](https://github.com/clojure-emacs/inf-clojure/pull/56): Add Planck support. -* [#51](https://github.com/clojure-emacs/inf-clojure/pull/51): Commands do not prompt by default anymore, unless they receive a non-nil prefix argument. -* [#44](https://github.com/clojure-emacs/inf-clojure/pull/44): Add REPL types and Lumo support. -* [#50](https://github.com/clojure-emacs/inf-clojure/pull/50): Rename defcustoms to `inf-clojure-*-form` where appropriate. -* [#34](https://github.com/clojure-emacs/inf-clojure/pull/34): Add support for socket REPL connections. -* New interactive command `inf-clojure-display-version`. -* [#42](https://github.com/clojure-emacs/inf-clojure/issues/42): Add a defcustom controlling the window in which the REPL buffer is displayed (`inf-clojure-repl-use-same-window`). -* Font-lock the code in the REPL. -* Handle properly ANSI color escape sequences in the REPL. -* [#41](https://github.com/clojure-emacs/inf-clojure/issues/41): Add a command to quit the REPL (it's bound to `C-c C-q`). -* [#29](https://github.com/clojure-emacs/inf-clojure/issues/29): Add a command to restart the REPL. -* [#31](https://github.com/clojure-emacs/inf-clojure/issues/31): Invoke different init command based on the project type (boot, lein or generic). +- [#63](https://github.com/clojure-emacs/inf-clojure/pull/69): Fix spurious process output on init. +- [#57](https://github.com/clojure-emacs/inf-clojure/pull/68): Add `inf-clojure-connect`. +- [#66](https://github.com/clojure-emacs/inf-clojure/pull/56): Add Planck support. +- [#51](https://github.com/clojure-emacs/inf-clojure/pull/51): Commands do not prompt by default anymore, unless they receive a non-nil prefix argument. +- [#44](https://github.com/clojure-emacs/inf-clojure/pull/44): Add REPL types and Lumo support. +- [#50](https://github.com/clojure-emacs/inf-clojure/pull/50): Rename defcustoms to `inf-clojure-*-form` where appropriate. +- [#34](https://github.com/clojure-emacs/inf-clojure/pull/34): Add support for socket REPL connections. +- New interactive command `inf-clojure-display-version`. +- [#42](https://github.com/clojure-emacs/inf-clojure/issues/42): Add a defcustom controlling the window in which the REPL buffer is displayed (`inf-clojure-repl-use-same-window`). +- Font-lock the code in the REPL. +- Handle properly ANSI color escape sequences in the REPL. +- [#41](https://github.com/clojure-emacs/inf-clojure/issues/41): Add a command to quit the REPL (it's bound to `C-c C-q`). +- [#29](https://github.com/clojure-emacs/inf-clojure/issues/29): Add a command to restart the REPL. +- [#31](https://github.com/clojure-emacs/inf-clojure/issues/31): Invoke different init command based on the project type (boot, lein or generic). ### Changes -* Display the REPL in a different window by default (it used to be displayed in the current window). -* [#26](https://github.com/clojure-emacs/inf-clojure/issues/26): Make switching to the REPL optional on `inf-clojure-load-file` (it's now controlled via a prefix argument). -* Removed the `inf-clojure` alias `run-clojure`. +- Display the REPL in a different window by default (it used to be displayed in the current window). +- [#26](https://github.com/clojure-emacs/inf-clojure/issues/26): Make switching to the REPL optional on `inf-clojure-load-file` (it's now controlled via a prefix argument). +- Removed the `inf-clojure` alias `run-clojure`. ### Bugs Fixed -* [#35](https://github.com/clojure-emacs/inf-clojure/issues/35): Fix prompt being included in input history. +- [#35](https://github.com/clojure-emacs/inf-clojure/issues/35): Fix prompt being included in input history. ## 1.4.0 (2016-01-17) ### New Features -* [#22](https://github.com/clojure-emacs/inf-clojure/pull/22): Add ElDoc support. +- [#22](https://github.com/clojure-emacs/inf-clojure/pull/22): Add ElDoc support. diff --git a/Eldev b/Eldev new file mode 100644 index 0000000..80ae7b1 --- /dev/null +++ b/Eldev @@ -0,0 +1,10 @@ +; -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*- + +(eldev-use-package-archive 'gnu) +(eldev-use-package-archive 'nongnu) +(eldev-use-package-archive 'melpa-stable) + +;; the tests depend on the assess library +(eldev-add-extra-dependencies 'test 'assess) + +(eldev-use-plugin 'autoloads) diff --git a/README.md b/README.md index 16f78b1..c6322c5 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,97 @@ -[![License GPL 3][badge-license]][copying] +[![Circle CI][circleci-badge]][circleci] [![MELPA][melpa-badge]][melpa-package] [![MELPA Stable][melpa-stable-badge]][melpa-stable-package] +[![NonGNU ELPA](https://elpa.nongnu.org/nongnu/inf-clojure.svg)](https://elpa.nongnu.org/nongnu/inf-clojure.html) +[![License GPL 3][badge-license]][copying] # inf-clojure This package provides basic interaction with a Clojure subprocess (REPL). It's based on ideas from the popular `inferior-lisp` package. -`inf-clojure` has two components - a nice Clojure REPL with -auto-completion and a minor mode (`inf-clojure-minor-mode`), which -extends `clojure-mode` with commands to evaluate forms directly in the -REPL. +`inf-clojure` has two components - a nice REPL buffer (`inf-clojure-mode`) and a REPL +interaction minor mode (`inf-clojure-minor-mode`), which extends `clojure-mode` +with commands to evaluate forms directly in the REPL. + +> [!IMPORTANT] +> +> This documentation tracks the `master` branch of `inf-clojure`. Some of +> the features and settings discussed here might not be available in +> older releases (including the current stable release). Please, consult +> the relevant git tag (e.g. 2.2.0) if you need documentation for a +> specific `inf-clojure` release. + +## Overview + +`inf-clojure` aims to expose the extensive self-documenting features of Clojure +REPLs via an Emacs package. `inf-clojure` is extremely simple and does not require special tooling. +It supports the following REPLs: + +- Clojure +- ClojureScript +- ClojureCLR (via [lein-clr](https://github.com/kumarshantanu/lein-clr)) +- [Planck](http://planck-repl.org/) +- [Joker](https://joker-lang.org/) +- [babashka](https://github.com/borkdude/babashka) `inf-clojure` provides a set of essential features for interactive -Clojure(Script) development: - -* REPL -* Interactive code evaluation -* Code completion -* Definition lookup -* Documentation lookup -* ElDoc -* Apropos -* Macroexpansion -* Support connecting to socket REPLs -* Support for Lumo -* Support for Planck +Clojure/ClojureScript/ClojureCLR development: + +- Enhanced REPL +- Interactive code evaluation +- Code completion +- Definition lookup +- Documentation lookup +- ElDoc +- Apropos +- Macroexpansion +- Reloading a namespace (via `require :reload`/`require :reload-all`) +- Connecting to socket REPLs For a more powerful/full-featured solution see [CIDER][]. +## Rationale + +`inf-clojure`'s goal is to provide the simplest possible way to interact with a +Clojure REPL. In Emacs terminology "inferior" process is a subprocess started +by Emacs (it being the "superior" process, of course). + +`inf-clojure` doesn't require much of setup, as at its core it simply runs a +terminal REPL process, pipes input to it, and processes its output. As the +Clojure socket REPL works in exactly the same manner `inf-clojure` can also +interact with it. + +Functionality like code completion and eldoc is powered by evaluation of +predefined code snippets that provide the necessary results. As different +Clojure REPLs have different capabilities, `inf-clojure` tracks the type of a +REPL and invokes the right code for each REPL type. + +`inf-clojure` is built on top of Emacs's +[comint](https://github.com/emacs-mirror/emacs/blob/master/lisp/comint.el). Unfortunately +`comint` is pretty light on official documentation, but there is a good +overview/tutorial +[here](https://www.masteringemacs.org/article/comint-writing-command-interpreter). + ## Installation -Available on all major `package.el` community maintained repos - +> [!IMPORTANT] +> +> `inf-clojure` requires Emacs 28 or newer. + +`inf-clojure` is available on the official [NonGNU ELPA](https://elpa.nongnu.org/nongnu/inf-clojure.html) `package.el` repo and on the community-maintained [MELPA Stable][] and [MELPA][] repos. -MELPA Stable is recommended as it has the latest stable version. +NonGNU ELPA and MELPA Stable are recommended as they have the latest stable version. MELPA has a development snapshot for users who don't mind breakage but -don't want to run from a git checkout. +don't want to run `inf-clojure` from a git checkout. You can install `inf-clojure` using the following command: M-x package-install [RET] inf-clojure [RET] -or if you'd rather keep it in your dotfiles: +or if you'd rather keep it in your Emacs config: -```el +```emacs-lisp (unless (package-installed-p 'inf-clojure) (package-refresh-contents) (package-install 'inf-clojure)) @@ -54,74 +101,208 @@ If the installation doesn't work try refreshing the package list: M-x package-refresh-contents -Add the following to your Emacs config to enable -`inf-clojure-minor-mode` for Clojure source buffers: +`inf-clojure-minor-mode` will be auto-enabled for Clojure source buffers after you do +`M-x inf-clojure`. You can disable this behavior by setting `inf-clojure-auto-mode` to +`nil`. + +You can also add the following to your Emacs config to enable +`inf-clojure-minor-mode` for Clojure source buffers, regardless of whether +there's an `inf-clojure` REPL running: -```el +```emacs-lisp (add-hook 'clojure-mode-hook #'inf-clojure-minor-mode) -``` -**Don't enable `inf-clojure-minor-mode` and `cider-mode` at the same -time. They have overlapping functionality and keybindings and the -result will be nothing short of havoc.** +;; or if you're a `clojure-ts-mode' user: -## Usage +(add-hook 'clojure-ts-mode-hook #'inf-clojure-minor-mode) +``` + +> [!WARNING] +> +> Don't enable `inf-clojure-minor-mode` and `cider-mode` at the same time. They +> have overlapping functionality and keybindings and the result will be nothing +> short of havoc. -Just invoke `M-x inf-clojure` or press `C-c C-z` within a Clojure source file. -This will start a REPL process for the current project and you can start -interacting with it. +## Basic Usage -`inf-clojure` has several custom variables which control the command used to -start a REPL for particular project type - `inf-clojure-lein-cmd`, -`inf-clojure-boot-cmd`, `inf-clojure-tools-deps-cmd` and -`inf-clojure-generic-cmd`. The `inf-clojure-project-type` can force a -particular project type, skipping the project detection, which can be useful -for projects that don't have standard layouts. +Just invoke `M-x inf-clojure` or press `C-c C-z` within a Clojure +source file. You should get a prompt with the supported REPL types and +common startup forms. You can select one of these or type in your own +custom startup. This will start a REPL process for the current project +and you can start interacting with it. -By default all those variables are set to strings (e.g. `lein repl`). -However, it is possible to use a cons pair like `("localhost" . 5555)` -to connect to a socket REPL like the one provided -with [planck](http://planck-repl.org/), which can be started from the -command line with `planck -n 5555`. +If you want to use a socket REPL server, use `M-x inf-clojure-socket-repl` +which will start a socket server and connect to it for you. -Use `C-u C-c C-z` to start a REPL with a different command/cons pair than -the default specified in `inf-clojure-program`. +If you've already started a socket REPL server, use `M-x inf-clojure-connect` +and enter its host and port numbers. -You can use `M-x inf-clojure-connect` (`C-c M-c`) to connect to a running -socket-repl. You will be prompted for host and port. +Inf-clojure aims to be very simple and offer tooling that the REPL +itself exposes. A few commands are: -You can set custom values to `inf-clojure` variables on a per-project basis using [directory -variables](https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html). +- eval last sexp (`C-x C-e`) +- show arglists for function (`C-c C-a`) +- show var documentation (`C-c C-v`) +- show source (`C-c C-s`) +- insert top level form into REPL (`C-c C-j d`) -For a list of all available commands in `inf-clojure-mode` (a.k.a. the REPL) and -`inf-clojure-minor-mode` you can either invoke `C-h f RET inf-clojure-mode` and -`C-h f RET inf-clojure-minor-mode` or simply browse their menus. +For a list of all available commands in `inf-clojure-mode` (a.k.a. the +REPL) and `inf-clojure-minor-mode` you can either invoke `C-h f RET +inf-clojure-mode` and `C-h f RET inf-clojure-minor-mode` or simply +browse their menus. Many `inf-clojure-minor-mode` commands by default act on the symbol at point. You can, however, change this behaviour by invoking such commands with a prefix argument. For instance: `C-u C-c C-v` will ask for the symbol you want to show the docstring for. -#### Caveats +## Configuration -Note that if you decide _NOT_ to use the socket repl, it is highly recommended -you disable output coloring and/or readline facilities: `inf-clojure` does not -filter out ASCII escape characters at the moment and will not behave correctly. +In the time-honoured Emacs tradition `inf-clojure`'s behaviour is extremely +configurable. + +You can set custom values to `inf-clojure` variables on a +per-project basis using [directory +variables](https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html) +or by setting them in in your [init file][Emacs init file]. + +You can see all the configuration options available using the command +`M-x customize-group RET inf-clojure`. + +### Startup + +While `inf-clojure` is capable of starting many common REPLs out of the box, it's +fairly likely you will want to set some custom REPL startup command +(e.g. because you need to include some `tools.deps` profile) and the REPL type +that goes with it. This is most easily achieved with the following `.dir-locals.el`: + +```emacs-lisp +((nil + (inf-clojure-custom-startup . "clojure -A:compliment") + (inf-clojure-custom-repl-type . clojure))) +``` + +> [!IMPORTANT] +> +> This file has to be in the directory in which you're invoking +> `inf-clojure` or a parent directory. + +There are two important configuration variables here: + +1. `inf-clojure-custom-startup`: Which startup command to use so + inf-clojure can run the inferior Clojure process (REPL). +2. `inf-clojure-custom-repl-type`: The type of the REPL started by the above command (e.g. `planck`). + +If these are set and you wish to prevent inf-clojure from using them, +use a prefix arg when invoking `inf-clojure` (`C-u M-x inf-clojure`). + +### REPL Features + +The supported REPL-features are in an alist called +`inf-clojure-repl-features` and it has the following shape: + +```emacs-lisp +'((cljs . ((doc . "(cljs.repl/doc %s)") + (source . "(cljs.repl/source %s)") + (arglists . "(try (->> '%s cljs.core/resolve cljs.core/meta :arglists) (catch :default _ nil))") + (apropos . "(cljs.repl/apropos \"%s\")") + (ns-vars . "(cljs.repl/dir %s)") + (set-ns . "(in-ns '%s)") + (macroexpand . "(cljs.core/macroexpand '%s)") + (macroexpand-1 . "(cljs.core/macroexpand-1 '%s)")))) +``` + +If you want to add a new REPL type, just do something like: + +``` emacs-lisp +(add-to-list 'inf-clojure-repl-features + (cons new-repl-type '((doc . "(myrepl/doc-command %s") + (source . "...") + ...))) +``` + +The `inf-clojure-repl-features` data structure is just an +alist of alists, so you can manipulate it in numerous ways. + +If you want to update a specific form there is a function +`inf-clojure-update-repl-feature` which can be used like so: + +```emacs-lisp +(inf-clojure-update-feature 'clojure 'completion "(incomplete.core/completions \"%s\")") +``` -You can disable coloring the following way for `boot`: +### `clojure-ts-mode` support -```el -((nil . ((inf-clojure-boot-cmd . "boot repl -C")))) +`inf-clojure` will try to use `clojure-ts-mode` by default if it's +available with fallback to `clojure-mode`. + +If you want to use `inf-clojure` with `clojure-mode` exclusively, you +can set it to: + +```emacs-lisp +(setopt inf-clojure-source-modes '(clojure-mode)) ``` -For leiningen, there are no command line switches and you need to add a custom [`project.clj` option](https://github.com/technomancy/leiningen/blob/master/sample.project.clj): +#### Caveats + +As `inf-clojure` is built on top of `comint` it has all the usual comint limitations - +namely it can't handle well some fancy terminal features (e.g. ANSI colours). +In general the "dumber" your terminal REPL is, the better (e.g. `clojure` vs `clj`). +Connecting to a socket REPL is one simple way to avoid dealing with this type of +problems. + +If you decide _not_ to use the socket REPL, it is highly recommended +you disable output coloring and/or `readline` facilities: `inf-clojure` does not +filter out ASCII escape characters at the moment and will not behave correctly. + +For [Leiningen][], there are no command-line switches and you need to add +a custom [`project.clj` +option](https://github.com/technomancy/leiningen/blob/master/sample.project.clj): ```clojure ... - :repl-options {:color false} +:repl-options {:color false} ... ``` +#### Clojure Command Line Socket REPL + +If you have the new [Clojure CLI tools][] installed you can use the `clojure` command: + +> [!IMPORTANT] +> +> Do not use `clj` because it adds readline support. + +``` shell +clojure -J-Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}" +``` + +Then either `C-c M-c RET localhost RET 5555` from within Emacs or add the following to your `.dir-locals.el`: + +```emacs-lisp +((nil . ((inf-clojure-custom-startup . ("localhost" . 5555))))) +``` + +#### Leiningen Socket REPL + +For Leiningen, add the following option to your `~/.lein/profiles.clj` or your `project.clj`: + +```clojure +:jvm-opts ["-Dclojure.server.repl={:port 5555 :accept clojure.core.server/repl}"] +``` + +Then run `lein repl` from within your project directory to start the +REPL. To connect, you can either `m-x inf-clojure-connect [RET] +localhost [RET] 5555` or you can put in a dir local file the +information on how connect: + +```emacs-lisp +((nil (inf-clojure-custom-startup "localhost" . 5555))) +``` + +The socket server REPL configuration options are described +[here](https://clojure.org/reference/repl_and_main#_launching_a_socket_server). + #### Multiple Process Support To run multiple Clojure processes, you start the first up @@ -131,6 +312,12 @@ process with another `inf-clojure`. It will be in a new buffer, named `*inf-clojure*`. You can switch between the different process buffers with `switch-to-buffer`. +> [!NOTE] +> +> If you're starting `inf-clojure` within a Clojure project directory the name +> of the project will be incorporated into the name of the REPL buffer - +> e.g. `*inf-clojure my-project*`. + Commands that send text from source buffers to Clojure processes (like `inf-clojure-eval-defun` or `inf-clojure-show-arglists`) have to choose a process to send to, when you have more than one Clojure process around. This is determined by the global variable `inf-clojure-buffer`. @@ -138,17 +325,17 @@ one Clojure process around. This is determined by the global variable `inf-cloju Suppose you have three inferior Clojures running: ``` - Buffer Process - ------ ------- - foo inf-clojure - bar inf-clojure<2> - *inf-clojure* inf-clojure<3> +Buffer Process +------ ------- +foo inf-clojure +bar inf-clojure<2> +*inf-clojure* inf-clojure<3> ``` If you do a `inf-clojure-eval-defun` command on some Clojure source code, what process do you send it to? -- If you're in a process buffer (foo, bar, or *inf-clojure*), +- If you're in a process buffer (foo, bar, or `*inf-clojure*`), you send it to that process. - If you're in some other buffer (e.g., a source file), you send it to the process attached to buffer `inf-clojure-buffer`. @@ -160,53 +347,56 @@ one process, this does the right thing. If you run multiple processes, you might need to change `inf-clojure-buffer` to whichever process buffer you want to use. -## Configuration options - -In the time-honoured Emacs tradition `inf-clojure`'s behaviour is extremely -configurable. - -You can see all the configuration options available using the command -`M-x customize-group RET inf-clojure`. +You can use the helpful function `inf-clojure-set-repl`. If called in +an `inf-clojure` REPL buffer, it will assign that buffer as the current +REPL (`(setq inf-clojure-buffer (current-buffer)`). If you are +not in an `inf-clojure` REPL buffer, it will offer a choice of +acceptable buffers to set as the REPL buffer. If called with a prefix, +it will always give the list even if you are currently in an +acceptable REPL buffer. + +> [!TIP] +> +> Renaming buffers will greatly improve the +> functionality of this list; the list "project-1: clojure repl", +> "project-2: cljs repl" is far more understandable than "inf-clojure", +> "inf-clojure<2>". #### REPL Type -An `inf-clojure` REPL can be of different types: Clojure, ClojureScript, Lumo -and Planck are all potentially valid options. - - At the moment, the default Clojure REPL, the Lumo REPL and the Planck REPL are -supported (standard ClojureScript is lacking mostly because some features -require to access the compiler state, -[cljs-tooling](https://github.com/clojure-emacs/cljs-tooling) is a good -candidate for enabling support). - -What does it mean that a REPL type is supported - well it means that `inf-clojure` -would use the proper code internally to power commands like definition lookup and friends. -Those differ from REPL to REPL and can't be implemented in a REPL-independent way. At -boot type `inf-clojure` tries to detect the type of the REPL that was started and uses -this type to dispatch the proper code for the respective REPL type. +An `inf-clojure` REPL has an associated type. The available types can be +obtained from `inf-clojure-repl-features`: -By default `inf-clojure` would start a standard Clojure REPL using -`lein` or `boot` but you can easily change this. To boot some other REPL just use the -right launch command (or connect to the REPL via a socket). For example, for -Lumo just add the following in your `.dir-locals.el`: +```emacs-lisp +(mapcar 'car inf-clojure-repl-features) -```el -((nil . ((inf-clojure-boot-cmd . "lumo -d")))) ;; inf-clojure-lein-cmd if you are using Leiningen +;; => (cljs planck joker clojure babashka) ``` +What does it mean that a REPL type is supported? Well, it means that +`inf-clojure` would use the proper Clojure(Script) code internally to power +commands like definition lookup and friends. Those differ from REPL to REPL and +can't be implemented in a REPL-independent way. The REPL type is inferred on +startup when using the `inf-clojure` command or is specified manually when using +`inf-clojure-connect`. + #### ElDoc `eldoc-mode` is supported in Clojure source buffers and `*inferior-clojure*` buffers which are running a Clojure REPL. -When ElDoc is enabled and there is an active REPL, it will show the -argument list of the function call you are currently editing in the -echo area. +When ElDoc is enabled and there is an active REPL, it will show the argument +list of the function call you are currently editing in the echo area. It +accomplishes this by evaluating forms to get the metadata for the vars under +your cursor. One side effect of this is that it can mess with repl vars like +`*1` and `*2`. You can disable inf-clojure's Eldoc functionality with `(setq +inf-clojure-enable-eldoc nil)`. -You can activate ElDoc with `M-x eldoc-mode` or by adding the -following to you Emacs config: +ElDoc should be enabled by default in Emacs 26.1+. If it is not active by +default, you can activate ElDoc with `M-x eldoc-mode` or by adding the following +to you Emacs config: -```el +```emacs-lisp (add-hook 'clojure-mode-hook #'eldoc-mode) (add-hook 'inf-clojure-mode-hook #'eldoc-mode) ``` @@ -216,32 +406,67 @@ You can leave it enabled, it just won't show anything in the echo area. #### Code Completion -Code completion is particularly open to customization. Not only you can `setq` -the customary `inf-clojure-completion-form`, `inf-clojure-completion-form-lumo` -and `inf-clojure-completion-form-planck` - the form to send to the REPL - but -you can also use `inf-clojure-completions-fn` for specifying a function that -given the REPL response should return elisp data compatible with -[`completion-at-point-functions`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Completion-in-Buffers.html). -For more info run `M-x describe-variable RET inf-clojure-completions-fn`. -Another option is to have a look at -[how cider does it](https://github.com/clojure-emacs/cider/blob/3e9ed12e8cfbad04d7618e649322765dc9bff5d6/cider-interaction.el#L595). +Code completion is a tricky aspect if you are trying to be as close to +a generic REPL as possible. Some runtimes (e.g. Planck) +explicitly provide completion functions in their REPL namespaces. For +clojure, you will need to have a library on your classpath. If you are +using a recent version of Leiningen, you already have +[incomplete](https://github.com/nrepl/incomplete). You +could alternatively use `compliment {:mvn/version "0.3.10"}`. + +```emacs-lisp +;; for incomplete +(inf-clojure-update-feature 'clojure 'completion "(incomplete.core/completions \"%s\")") + +;; or +;; for compliment +(inf-clojure-update-feature 'clojure 'completion "(compliment.core/completions \"%s\")") +``` -#### Lumo Setup +If you give a form for the completion form, it is your responsibility +to ensure that this namespace is on the classpath and required. If +using Leiningen, this is done for you with `incomplete`. If adding +`compliment`, the following sample `deps.edn` can conveniently add the dep +to your program: -For an optimal Lumo experience the `-d` needs to be passed to Lumo -when launched from the command line. This disable `readline` support -in order to play nicely with emacs. +```clojure +{:aliases {:compliment {:extra-deps {compliment {:mvn/version "0.3.10"}}}}} +``` -For example, you can use the following command (assuming `cp` contains -the classpath) in your `.dir-locals.el`: +Use the startup command: `clojure -A:compliment`. Then require the ns +once so that the completion machinery will work: `(require +'compliment.core)`. Now tab completion should work. + +For more advanced customization, code completion is particularly open +to customization. Not only you can `setq` the customary +`inf-clojure-completion-form`, +`inf-clojure-completion-form-planck` and +`inf-clojure-completion-form-joker` - the form to send to the REPL - +but you can also use `inf-clojure-completions-fn` for specifying a +function that given the REPL response should return Elisp data +compatible with +[`completion-at-point-functions`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Completion-in-Buffers.html). -```el -((nil . (eval . (setq inf-clojure-boot-cmd (concat "lumo -d -c " - (f-read (concat (inf-clojure-project-root) "cp"))))))) -``` +For more info run `M-x describe-variable RET +inf-clojure-completions-fn`. Another option is to have a look at [how +CIDER does +it](https://github.com/clojure-emacs/cider/blob/3e9ed12e8cfbad04d7618e649322765dc9bff5d6/cider-interaction.el#L595). ## Troubleshooting +### Things seem broken + +Inf-clojure is intentionally quite simple and just sends commands to a +REPL on your behalf to provide features. In order to do this +inf-clojure largely needs to know the REPL type so it can format the +correct calls. Most end up in `(planck.repl/doc [symbol])` or +`(cljs.repl/doc ...)` so its important that the REPL type is set +correctly. This REPL type exists in the process buffer (REPL) and the +source buffers as a cache. If you have problems, run `M-x +inf-clojure-set-repl-type` from the source buffer to set the REPL type +in both buffers. To see how simple inf-clojure is, look at +`inf-clojure-repl-features` to see largely how things are laid out. + ### REPL not responsive in Windows OS In Windows, the REPL is not returning anything. For example, type `(+ @@ -253,7 +478,7 @@ The explanation of this problem and solution can be found [here](https://groups. The solution is to create a file named `.jline.rc` in your `$HOME` directory and add this line to that file: -``` +```ini jline.terminal=unsupported ``` @@ -261,7 +486,7 @@ jline.terminal=unsupported Standard Emacs debugging turns out to be difficult when an asynchronous process is involved. In this case try to enable logging: -```el +```emacs-lisp (setq inf-clojure-log-activity t) ``` @@ -269,7 +494,7 @@ This creates `.inf-clojure.log` in the project directory so that you can `tail - ## License -Copyright © 2014-2018 Bozhidar Batsov and [contributors][]. +Copyright © 2014-2025 Bozhidar Batsov and [contributors][]. Distributed under the GNU General Public License; type C-h C-c to view it. @@ -279,9 +504,12 @@ Distributed under the GNU General Public License; type C-h C-c to vie [melpa-package]: http://melpa.org/#/inf-clojure [melpa-stable-package]: http://stable.melpa.org/#/inf-clojure [COPYING]: http://www.gnu.org/copyleft/gpl.html -[badge-travis]: https://travis-ci.org/clojure-emacs/inf-clojure.svg?branch=master +[circleci]: https://circleci.com/gh/clojure-emacs/inf-clojure +[circleci-badge]: https://circleci.com/gh/clojure-emacs/inf-clojure.svg?style=svg [CIDER]: https://github.com/clojure-emacs/cider [Leiningen]: http://leiningen.org [contributors]: https://github.com/clojure-emacs/inf-clojure/contributors [melpa]: http://melpa.org [melpa stable]: http://stable.melpa.org +[Emacs init file]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Init-File.html +[Clojure cli tools]: https://clojure.org/guides/getting_started diff --git a/inf-clojure.el b/inf-clojure.el index 351c3b1..37f91ed 100644 --- a/inf-clojure.el +++ b/inf-clojure.el @@ -1,15 +1,16 @@ -;;; inf-clojure.el --- Run an external Clojure process in an Emacs buffer -*- lexical-binding: t; -*- +;;; inf-clojure.el --- Basic interaction with a Clojure REPL -*- lexical-binding: t; -*- -;; Copyright © 2014-2018 Bozhidar Batsov +;; Copyright © 2014-2025 Bozhidar Batsov -;; Authors: Bozhidar Batsov +;; Authors: Bozhidar Batsov ;; Olin Shivers +;; Maintainer: Bozhidar Batsov ;; URL: http://github.com/clojure-emacs/inf-clojure -;; Keywords: processes, clojure -;; Version: 2.1.0 -;; Package-Requires: ((emacs "24.4") (clojure-mode "5.6")) +;; Keywords: processes, comint, clojure +;; Version: 3.3.0 +;; Package-Requires: ((emacs "28.1") (clojure-mode "5.11")) -;; This file is part of GNU Emacs. +;; This file is not part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -35,7 +36,7 @@ ;; REPL. ;; ;; `inf-clojure` provides a set of essential features for interactive -;; Clojure(Script) development: +;; Clojure/ClojureScript/ClojureCLR development: ;; ;; * REPL ;; * Interactive code evaluation @@ -46,8 +47,8 @@ ;; * Apropos ;; * Macroexpansion ;; * Support connecting to socket REPLs -;; * Support for Lumo ;; * Support for Planck +;; * Support for Joker ;; ;; For a more powerful/full-featured solution see https://github.com/clojure-emacs/cider. ;; @@ -63,13 +64,247 @@ (require 'comint) (require 'clojure-mode) +(require 'clojure-ts-mode nil :no-error) (require 'eldoc) (require 'thingatpt) (require 'ansi-color) (require 'cl-lib) (require 'subr-x) +(require 'project) + +(defvar inf-clojure-startup-forms '((lein . "lein repl") + (boot . "boot repl") + (clojure . "clojure") + (cljs . "clojure -M -m cljs.main -r") + (lein-clr . "lein clr repl") + (planck . "planck -d") + (babashka . "bb") + (joker . "joker"))) + +(defvar inf-clojure-repl-features + '((cljs . ((doc . "(cljs.repl/doc %s)") + (source . "(cljs.repl/source %s)") + (arglists . "(try (->> '%s cljs.core/resolve cljs.core/meta :arglists) (catch :default _ nil))") + (apropos . "(cljs.repl/apropos \"%s\")") + (ns-vars . "(cljs.repl/dir %s)") + (set-ns . "(in-ns '%s)") + (macroexpand . "(cljs.core/macroexpand '%s)") + (macroexpand-1 . "(cljs.core/macroexpand-1 '%s)"))) + (planck . ((load . "(load-file \"%s\")") + (doc . "(planck.repl/doc %s)") + (source . "(planck.repl/source %s)") + (arglists . "(planck.repl/get-arglists \"%s\")") + (apropos . "(doseq [var (sort (planck.repl/apropos \"%s\"))] (println (str var)))") + (ns-vars . "(planck.repl/dir %s)") + (set-ns . "(in-ns '%s)") + (macroexpand . "(macroexpand '%s)") + (macroexpand-1 . "(macroexpand-1 '%s)") + (completion . "(seq (js->clj (#'planck.repl/get-completions \"%s\")))"))) + (joker . ((load . "(load-file \"%s\")") + (doc . "(joker.repl/doc %s)") + (arglists . + "(try + (:arglists + (joker.core/meta + (joker.core/resolve + (joker.core/read-string \"%s\")))) + (catch Error _ nil))") + (set-ns . "(in-ns '%s)") + (macroexpand . "(macroexpand '%s)") + (macroexpand-1 . "(macroexpand-1 '%s)"))) + (babashka . ((load . "(clojure.core/load-file \"%s\")") + (doc . "(clojure.repl/doc %s)") + (source . "(clojure.repl/source %s)") + (arglists . + "(try (-> '%s clojure.core/resolve clojure.core/meta :arglists) + (catch Throwable e nil))") + (apropos . "(doseq [var (sort (clojure.repl/apropos \"%s\"))] (println (str var)))") + (ns-vars . "(clojure.repl/dir %s)") + (set-ns . "(clojure.core/in-ns '%s)") + (macroexpand . "(clojure.core/macroexpand '%s)") + (macroexpand-1 . "(clojure.core/macroexpand-1 '%s)"))) + (node-babashka . ((load . "(clojure.core/load-file \"%s\")") + (doc . "(clojure.repl/doc %s)") + (source . "(clojure.repl/source %s)") + (arglists . + "(try (-> '%s clojure.core/resolve clojure.core/meta :arglists) + (catch Throwable e nil))") + (apropos . "(doseq [var (sort (clojure.repl/apropos \"%s\"))] (println (str var)))") + (ns-vars . "(clojure.repl/dir %s)") + (set-ns . "(clojure.core/in-ns '%s)") + (macroexpand . "(clojure.core/macroexpand '%s)") + (macroexpand-1 . "(clojure.core/macroexpand-1 '%s)"))) + (clojure . ((load . "(clojure.core/load-file \"%s\")") + (doc . "(clojure.repl/doc %s)") + (source . "(clojure.repl/source %s)") + (arglists . + "(try + (:arglists + (clojure.core/meta + (clojure.core/resolve + (clojure.core/read-string \"%s\")))) + (catch #?(:clj Throwable :cljr Exception) e nil))") + (apropos . "(doseq [var (sort (clojure.repl/apropos \"%s\"))] (println (str var)))") + (ns-vars . "(clojure.repl/dir %s)") + (set-ns . "(clojure.core/in-ns '%s)") + (macroexpand . "(clojure.core/macroexpand '%s)") + (macroexpand-1 . "(clojure.core/macroexpand-1 '%s)"))) + (lein-clr . ((load . "(clojure.core/load-file \"%s\")") + (doc . "(clojure.repl/doc %s)") + (source . "(clojure.repl/source %s)") + (arglists . + "(try + (:arglists + (clojure.core/meta + (clojure.core/resolve + (clojure.core/read-string \"%s\")))) + (catch Exception e nil))") + (apropos . "(doseq [var (sort (clojure.repl/apropos \"%s\"))] (println (str var)))") + (ns-vars . "(clojure.repl/dir %s)") + (set-ns . "(clojure.core/in-ns '%s)") + (macroexpand . "(clojure.core/macroexpand '%s)") + (macroexpand-1 . "(clojure.core/macroexpand-1 '%s)"))))) + +(defvar-local inf-clojure-repl-type nil + "Symbol to define your REPL type. +Its root binding is nil and it can be further customized using +either `setq-local` or an entry in `.dir-locals.el`." ) + +(defvar inf-clojure-buffer nil + "The current `inf-clojure' process buffer. + +MULTIPLE PROCESS SUPPORT +=========================================================================== +To run multiple Clojure processes, you start the first up +with \\[inf-clojure]. It will be in a buffer named *inf-clojure*. +Rename this buffer with \\[rename-buffer]. You may now start up a new +process with another \\[inf-clojure]. It will be in a new buffer, +named *inf-clojure*. You can switch between the different process +buffers with \\[switch-to-buffer]. + +Commands that send text from source buffers to Clojure processes -- +like `inf-clojure-eval-defun' or `inf-clojure-show-arglists' -- have to choose a +process to send to, when you have more than one Clojure process around. This +is determined by the global variable `inf-clojure-buffer'. Suppose you +have three inferior Clojures running: + Buffer Process + foo `inf-clojure' + bar inf-clojure<2> + *inf-clojure* inf-clojure<3> +If you do a \\[inf-clojure-eval-defun] command on some Clojure source code, +what process do you send it to? + +- If you're in a process buffer (foo, bar, or *inf-clojure*), + you send it to that process. +- If you're in some other buffer (e.g., a source file), you + send it to the process attached to buffer `inf-clojure-buffer'. +This process selection is performed by function `inf-clojure-proc'. + +Whenever \\[inf-clojure] fires up a new process, it resets +`inf-clojure-buffer' to be the new process's buffer. If you only run +one process, this does the right thing. If you run multiple +processes, you might need to change `inf-clojure-buffer' to +whichever process buffer you want to use.") + +(defun inf-clojure--get-feature (repl-type feature no-error) + "Get FEATURE for REPL-TYPE from repl-features. +If no-error is truthy don't error if feature is not present." + (let ((feature-form (alist-get feature (alist-get repl-type inf-clojure-repl-features)))) + (cond (feature-form feature-form) + (no-error nil) + (t (error "%s not configured for %s" feature repl-type))))) + +(defun inf-clojure-get-feature (proc feature &optional no-error) + "Get FEATURE based on repl type for PROC." + (let* ((repl-type (or (with-current-buffer (process-buffer proc) + inf-clojure-repl-type) + (error "REPL type is not known")))) + (inf-clojure--get-feature repl-type feature no-error))) + +(defun inf-clojure--update-feature (repl-type feature form) + "Return a copy of the datastructure containing the repl features. +Given a REPL-TYPE (`clojure', `planck', ...) and a FEATURE (`doc', +`apropos', ...) and a FORM this will return a new datastructure +that can be set as `inf-clojure-repl-features'." + (let ((original (alist-get repl-type inf-clojure-repl-features))) + (if original + (cons (cons repl-type (cons (cons feature form) (assoc-delete-all feature original))) + (assoc-delete-all repl-type inf-clojure-repl-features)) + (error "Attempted to update %s form of unknown REPL type %s" + (symbol-name feature) + (symbol-name repl-type))))) + +(defun inf-clojure-update-feature (repl-type feature form) + "Mutate the repl features to the new FORM. +Given a REPL-TYPE (`clojure', `planck', ...) and a FEATURE (`doc', +`apropos', ...) and a FORM this will set +`inf-clojure-repl-features' with these new values." + (setq inf-clojure-repl-features (inf-clojure--update-feature repl-type feature form))) + +(defun inf-clojure-proc (&optional no-error) + "Return the current inferior Clojure process. +When NO-ERROR is non-nil, don't throw an error when no process +has been found. See also variable `inf-clojure-buffer'." + (or (get-buffer-process (if (derived-mode-p 'inf-clojure-mode) + (current-buffer) + inf-clojure-buffer)) + (unless no-error + (error "No Clojure subprocess; see variable `inf-clojure-buffer'")))) + +(defun inf-clojure-repl-p (&optional buf) + "Indicates if BUF is an `inf-clojure' REPL. +If BUF is nil then defaults to the current buffer. +Checks the mode and that there is a live process." + (let ((buf (or buf (current-buffer)))) + (and (with-current-buffer buf (derived-mode-p 'inf-clojure-mode)) + (get-buffer-process buf) + (process-live-p (get-buffer-process buf))))) + +(defun inf-clojure-repls () + "Return a list of all `inf-clojure' REPL buffers." + (let (repl-buffers) + (dolist (b (buffer-list)) + (when (inf-clojure-repl-p b) + (push (buffer-name b) repl-buffers))) + repl-buffers)) + +(defun inf-clojure--prompt-repl-buffer (prompt) + "Prompt the user to select an `inf-clojure' repl buffer. +PROMPT is a string to prompt the user. +Returns nil when no buffer is selected." + (let ((repl-buffers (inf-clojure-repls))) + (if (> (length repl-buffers) 0) + (when-let* ((repl-buffer (completing-read prompt repl-buffers nil t))) + (get-buffer repl-buffer)) + (user-error "No buffers have an inf-clojure process")))) + +(defun inf-clojure-set-repl (always-ask) + "Set an `inf-clojure' buffer as the active (default) REPL. +If in a REPL buffer already, use that unless a prefix is used (or +ALWAYS-ASK). Otherwise get a list of all active `inf-clojure' +REPLS and offer a choice. It's recommended to rename REPL +buffers after they are created with `rename-buffer'." + (interactive "P") + (when-let* ((new-repl-buffer + (if (or always-ask + (not (inf-clojure-repl-p))) + (inf-clojure--prompt-repl-buffer "Select default REPL: ") + (current-buffer)))) + (setq inf-clojure-buffer new-repl-buffer) + (message "Current inf-clojure REPL set to %s" new-repl-buffer))) + +(defvar inf-clojure--repl-type-lock nil + "Global lock for protecting against proc filter race conditions. +See http://blog.jorgenschaefer.de/2014/05/race-conditions-in-emacs-process-filter.html") + +(defun inf-clojure--prompt-repl-type () + "Set the REPL type to one of the available implementations." + (interactive) + (let ((types (mapcar #'car inf-clojure-repl-features))) + (intern + (completing-read "Set REPL type: " + (sort (mapcar #'symbol-name types) #'string-lessp))))) - (defgroup inf-clojure nil "Run an external Clojure process (REPL) in an Emacs buffer." :prefix "inf-clojure-" @@ -77,7 +312,8 @@ :link '(url-link :tag "GitHub" "https://github.com/clojure-emacs/inf-clojure") :link '(emacs-commentary-link :tag "Commentary" "inf-clojure")) -(defconst inf-clojure-version "2.1.0" +(defconst inf-clojure-version + (package-get-version) "The current version of `inf-clojure'.") (defcustom inf-clojure-prompt-read-only t @@ -94,15 +330,35 @@ mode. Default is whitespace followed by 0 or 1 single-letter colon-keyword \(as in :a, :c, etc.)" :type 'regexp) +(defcustom inf-clojure-source-modes '(clojure-ts-mode clojure-mode) + "Used to determine if a buffer contains Clojure source code. + +Any buffer with one of these major modes, it's considered a Clojure +source file by all `inf-clojure' commands." + :type '(repeat symbol) + :safe #'symbolp) + +(defun inf-clojure--modeline-info () + "Return modeline info for `inf-clojure-minor-mode'. +Either \"no process\" or \"buffer-name(repl-type)\"" + (if (and (bufferp inf-clojure-buffer) + (buffer-live-p inf-clojure-buffer)) + (with-current-buffer inf-clojure-buffer + (format "%s(%s)" (buffer-name (current-buffer)) inf-clojure-repl-type)) + "no process")) + (defvar inf-clojure-mode-map - (let ((map (copy-keymap comint-mode-map))) - (define-key map "\C-x\C-e" #'inf-clojure-eval-last-sexp) - (define-key map "\C-c\C-l" #'inf-clojure-load-file) - (define-key map "\C-c\C-a" #'inf-clojure-show-arglists) - (define-key map "\C-c\C-v" #'inf-clojure-show-var-documentation) - (define-key map "\C-c\C-s" #'inf-clojure-show-var-source) - (define-key map "\C-c\M-o" #'inf-clojure-clear-repl-buffer) - (define-key map "\C-c\C-q" #'inf-clojure-quit) + (let ((map (make-sparse-keymap))) + (set-keymap-parent map comint-mode-map) + (define-key map (kbd "C-x C-e") #'inf-clojure-eval-last-sexp) + (define-key map (kbd "C-c C-l") #'inf-clojure-load-file) + (define-key map (kbd "C-c C-a") #'inf-clojure-show-arglists) + (define-key map (kbd "C-c C-v") #'inf-clojure-show-var-documentation) + (define-key map (kbd "C-c C-s") #'inf-clojure-show-var-source) + (define-key map (kbd "C-c C-S-a") #'inf-clojure-apropos) + (define-key map (kbd "C-c M-o") #'inf-clojure-clear-repl-buffer) + (define-key map (kbd "C-c C-q") #'inf-clojure-quit) + (define-key map (kbd "C-c C-z") #'inf-clojure-switch-to-recent-buffer) (easy-menu-define inf-clojure-mode-menu map "Inferior Clojure REPL Menu" '("Inf-Clojure REPL" @@ -113,6 +369,7 @@ mode. Default is whitespace followed by 0 or 1 single-letter colon-keyword ["Show arglists" inf-clojure-show-arglists t] ["Show documentation for var" inf-clojure-show-var-documentation t] ["Show source for var" inf-clojure-show-var-source t] + ["Apropos" inf-clojure-apropos t] "--" ["Clear REPL" inf-clojure-clear-repl-buffer] ["Restart" inf-clojure-restart] @@ -121,26 +378,36 @@ mode. Default is whitespace followed by 0 or 1 single-letter colon-keyword ["Version" inf-clojure-display-version])) map)) +(defvar inf-clojure-insert-commands-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "d") #'inf-clojure-insert-defun) + (define-key map (kbd "C-d") #'inf-clojure-insert-defun) + (define-key map (kbd "e") #'inf-clojure-insert-last-sexp) + (define-key map (kbd "C-e") #'inf-clojure-insert-last-sexp) + map)) + (defvar inf-clojure-minor-mode-map (let ((map (make-sparse-keymap))) - (define-key map "\M-\C-x" #'inf-clojure-eval-defun) ; Gnu convention - (define-key map "\C-x\C-e" #'inf-clojure-eval-last-sexp) ; Gnu convention - (define-key map "\C-c\C-e" #'inf-clojure-eval-last-sexp) - (define-key map "\C-c\C-c" #'inf-clojure-eval-defun) ; SLIME/CIDER style - (define-key map "\C-c\C-b" #'inf-clojure-eval-buffer) - (define-key map "\C-c\C-r" #'inf-clojure-eval-region) - (define-key map "\C-c\C-n" #'inf-clojure-eval-form-and-next) - (define-key map "\C-c\C-z" #'inf-clojure-switch-to-repl) - (define-key map "\C-c\C-i" #'inf-clojure-show-ns-vars) - (define-key map "\C-c\C-A" #'inf-clojure-apropos) - (define-key map "\C-c\C-m" #'inf-clojure-macroexpand) - (define-key map "\C-c\C-l" #'inf-clojure-load-file) - (define-key map "\C-c\C-a" #'inf-clojure-show-arglists) - (define-key map "\C-c\C-v" #'inf-clojure-show-var-documentation) - (define-key map "\C-c\C-s" #'inf-clojure-show-var-source) - (define-key map "\C-c\M-n" #'inf-clojure-set-ns) - (define-key map "\C-c\C-q" #'inf-clojure-quit) - (define-key map "\C-c\M-c" #'inf-clojure-connect) + (define-key map (kbd "C-M-x") #'inf-clojure-eval-defun) ; Gnu convention + (define-key map (kbd "C-x C-e") #'inf-clojure-eval-last-sexp) ; Gnu convention + (define-key map (kbd "C-c C-e") #'inf-clojure-eval-last-sexp) + (define-key map (kbd "C-c C-c") #'inf-clojure-eval-defun) ; SLIME/CIDER style + (define-key map (kbd "C-c C-b") #'inf-clojure-eval-buffer) + (define-key map (kbd "C-c C-r") #'inf-clojure-eval-region) + (define-key map (kbd "C-c M-r") #'inf-clojure-reload) + (define-key map (kbd "C-c C-n") #'inf-clojure-eval-form-and-next) + (define-key map (kbd "C-c C-j") inf-clojure-insert-commands-map) + (define-key map (kbd "C-c C-z") #'inf-clojure-switch-to-repl) + (define-key map (kbd "C-c C-i") #'inf-clojure-show-ns-vars) + (define-key map (kbd "C-c C-S-a") #'inf-clojure-apropos) + (define-key map (kbd "C-c C-m") #'inf-clojure-macroexpand) + (define-key map (kbd "C-c C-l") #'inf-clojure-load-file) + (define-key map (kbd "C-c C-a") #'inf-clojure-show-arglists) + (define-key map (kbd "C-c C-v") #'inf-clojure-show-var-documentation) + (define-key map (kbd "C-c C-s") #'inf-clojure-show-var-source) + (define-key map (kbd "C-c M-n") #'inf-clojure-set-ns) + (define-key map (kbd "C-c C-q") #'inf-clojure-quit) + (define-key map (kbd "C-c M-c") #'inf-clojure-connect) (easy-menu-define inf-clojure-minor-mode-menu map "Inferior Clojure Minor Mode Menu" '("Inf-Clojure" @@ -150,6 +417,7 @@ mode. Default is whitespace followed by 0 or 1 single-letter colon-keyword ["Eval buffer" inf-clojure-eval-buffer t] "--" ["Load file..." inf-clojure-load-file t] + ["Reload file... " inf-clojure-reload t] "--" ["Switch to REPL" inf-clojure-switch-to-repl t] ["Set REPL ns" inf-clojure-set-ns t] @@ -165,6 +433,33 @@ mode. Default is whitespace followed by 0 or 1 single-letter colon-keyword ["Quit REPL" inf-clojure-quit])) map)) +;;;###autoload +(defcustom inf-clojure-mode-line + '(:eval (format " inf-clojure[%s]" (inf-clojure--modeline-info))) + "Mode line lighter for cider mode. + +The value of this variable is a mode line template as in +`mode-line-format'. See Info Node `(elisp)Mode Line Format' for details +about mode line templates. + +Customize this variable to change how `inf-clojure-minor-mode' +displays its status in the mode line. The default value displays +the current REPL. Set this variable to nil to disable the +mode line entirely." + :type 'sexp + :risky t) + +(defcustom inf-clojure-enable-eldoc t + "Var that allows disabling `eldoc-mode' in `inf-clojure'. + +Set to nil to disable eldoc. Eldoc can be quite useful by +displaying function signatures in the modeline, but can also +cause multiple prompts to appear in the REPL and mess with *1, +*2, etc." + :type 'boolean + :safe #'booleanp + :package-version '(inf-clojure . "3.2.0")) + ;;;###autoload (define-minor-mode inf-clojure-minor-mode "Minor mode for interacting with the inferior Clojure process buffer. @@ -172,9 +467,11 @@ mode. Default is whitespace followed by 0 or 1 single-letter colon-keyword The following commands are available: \\{inf-clojure-minor-mode-map}" - :lighter "" :keymap inf-clojure-minor-mode-map - (setq comint-input-sender 'inf-clojure--send-string) - (inf-clojure-eldoc-setup) + :lighter inf-clojure-mode-line + :keymap inf-clojure-minor-mode-map + (setq-local comint-input-sender 'inf-clojure--send-string) + (when inf-clojure-enable-eldoc + (inf-clojure-eldoc-setup)) (make-local-variable 'completion-at-point-functions) (add-to-list 'completion-at-point-functions #'inf-clojure-completion-at-point)) @@ -189,135 +486,40 @@ number (e.g. (\"localhost\" . 5555))." (stringp (car x)) (numberp (cdr x)))) -(defcustom inf-clojure-project-type nil - "Defines the project type. - -If this is `nil`, the project will be automatically detected." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.1.0")) - -(defcustom inf-clojure-lein-cmd "lein repl" - "The command used to start a Clojure REPL for Leiningen projects. - -Alternatively you can specify a TCP connection cons pair, instead -of command, consisting of a host and port -number (e.g. (\"localhost\" . 5555)). That's useful if you're -often connecting to a remote REPL process." - :type '(choice (string) - (cons string integer)) - :risky #'stringp - :safe #'inf-clojure--endpoint-p - :package-version '(inf-clojure . "2.0.0")) - -(define-obsolete-variable-alias 'inf-clojure-program 'inf-clojure-lein-cmd "2.0.0") - -(defcustom inf-clojure-boot-cmd "boot repl -C" - "The command used to start a Clojure REPL for Boot projects. - -Alternatively you can specify a TCP connection cons pair, instead -of command, consisting of a host and port -number (e.g. (\"localhost\" . 5555)). That's useful if you're -often connecting to a remote REPL process." - :type '(choice (string) - (cons string integer)) - :risky #'stringp - :safe #'inf-clojure--endpoint-p - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-tools-deps-cmd "clj" - "The command used to start a Clojure REPL for tools.deps projects. - -Alternatively you can specify a TCP connection cons pair, instead -of command, consisting of a host and port -number (e.g. (\"localhost\" . 5555)). That's useful if you're -often connecting to a remote REPL process." - :type '(choice (string) - (cons string integer)) - :risky #'stringp - :safe #'inf-clojure--endpoint-p - :package-version '(inf-clojure . "2.1.0")) - -(defcustom inf-clojure-generic-cmd "lein repl" - "The command used to start a Clojure REPL outside Lein/Boot projects. - -Alternatively you can specify a TCP connection cons pair, instead -of command, consisting of a host and port -number (e.g. (\"localhost\" . 5555)). That's useful if you're -often connecting to a remote REPL process." - :type '(choice (string) - (cons string integer)) - :risky #'stringp - :safe #'inf-clojure--endpoint-p - :package-version '(inf-clojure . "2.0.0")) - -;;;; Lumo -;;;; ==== - -(defcustom inf-clojure--lumo-repl-form - "(find-ns 'lumo.repl)" - "Form to invoke in order to verify that we launched a Lumo REPL." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -;;;; Planck -;;;; ==== - -(defcustom inf-clojure--planck-repl-form - "(find-ns 'planck.repl)" - "Form to invoke in order to verify that we launched a Planck REPL." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defvar-local inf-clojure-repl-type nil - "Symbol to define your REPL type. -Its root binding is nil and it can be further customized using -either `setq-local` or an entry in `.dir-locals.el`." ) - -(defvar inf-clojure--repl-type-lock nil - "Global lock for protecting against proc filter race conditions. -See http://blog.jorgenschaefer.de/2014/05/race-conditions-in-emacs-process-filter.html") - -(defun inf-clojure--detect-repl-type (proc) - "Identifies the current REPL type for PROC." - (when (not inf-clojure--repl-type-lock) - (let ((inf-clojure--repl-type-lock t)) - (cond - ((inf-clojure--some-response-p proc inf-clojure--lumo-repl-form) 'lumo) - ((inf-clojure--some-response-p proc inf-clojure--planck-repl-form) 'planck) - (t 'clojure))))) - -(defun inf-clojure--set-repl-type (proc) - "Set the REPL type if has not already been set. -It requires a REPL PROC for inspecting the correct type." - (with-current-buffer inf-clojure-buffer - (if (not inf-clojure-repl-type) - (setq inf-clojure-repl-type (inf-clojure--detect-repl-type proc)) - inf-clojure-repl-type))) - -(defun inf-clojure--single-linify (string) - "Convert a multi-line STRING in a single-line STRING. -It also reduces/adds redundant whitespace for readability. Note -that this function will transform the empty string in \" \" (it -adds an empty space)." - (replace-regexp-in-string "[ \\|\n]+" " " string)) - -(defun inf-clojure--trim-newline-right (string) - "Trim newlines (only) in STRING." - (if (string-match "\n+\\'" string) - (replace-match "" t t string) - string)) +(defcustom inf-clojure-custom-startup + nil + "Form to be used to start `inf-clojure'. +Can be a cons pair of (host . port) where host is a string and +port is an integer, or a string to startup an interpreter like +\"planck\"." + :type '(choice (cons string integer) (const nil))) + +(defcustom inf-clojure-custom-repl-type + nil + "REPL type to use for `inf-clojure' process buffer. +Should be a symbol that is a key in `inf-clojure-repl-features'." + :package-version '(inf-clojure . "3.0.0") + :type '(choice (const :tag "clojure" clojure) + (const :tag "cljs" cljs) + (const :tag "planck" planck) + (const :tag "joker" joker) + (const :tag "babashka" babashka) + (const :tag "determine at startup" nil))) + +(defvar inf-clojure-custom-repl-name nil + "A string to be used as the repl buffer name.") + +(defun inf-clojure--whole-comment-line-p (string) + "Return non-nil iff STRING is a whole line semicolon comment." + (string-match-p "^\s*;" string)) (defun inf-clojure--sanitize-command (command) "Sanitize COMMAND for sending it to a process. An example of things that this function does is to add a final newline at the end of the form. Return an empty string if the sanitized command is empty." - (let* ((linified (inf-clojure--single-linify command)) - (sanitized (inf-clojure--trim-newline-right linified))) - (if (or (string-blank-p linified) (string-blank-p sanitized)) + (let ((sanitized (string-trim-right command))) + (if (string-blank-p sanitized) "" (concat sanitized "\n")))) @@ -329,46 +531,49 @@ always be preferred over `comint-send-string`. It delegates to `comint-simple-send` so it always appends a newline at the end of the string for evaluation. Refer to `comint-simple-send` for customizations." - (inf-clojure--set-repl-type proc) - (comint-simple-send proc string)) - -(defcustom inf-clojure-load-form "(clojure.core/load-file \"%s\")" - "Format-string for building a Clojure expression to load a file. -This format string should use `%s' to substitute a file name and + (let ((sanitized (inf-clojure--sanitize-command string))) + (inf-clojure--log-string sanitized "----CMD->") + (comint-send-string proc sanitized))) + +(defcustom inf-clojure-reload-form "(require '%s :reload)" + "Format-string for building a Clojure expression to reload a file. +Reload forces loading of all the identified libs even if they are +already loaded. +This format string should use `%s' to substitute a namespace and should result in a Clojure form that will be sent to the inferior Clojure to load that file." :type 'string :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) + :package-version '(inf-clojure . "2.2.0")) -(define-obsolete-variable-alias 'inf-clojure-load-command 'inf-clojure-load-form "2.0.0") +;; :reload forces loading of all the identified libs even if they are + ;; already loaded +;; :reload-all implies :reload and also forces loading of all libs that the +;; identified libs directly or indirectly load via require or use -(defcustom inf-clojure-load-form-lumo "(clojure.core/load-file \"%s\")" - "Lumo format-string for building a Clojure expression to load a file. -This format string should use `%s' to substitute a file name and -should result in a Clojure form that will be sent to the inferior -Clojure to load that file." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-load-form-planck "(load-file \"%s\")" - "Planck format-string for building a Clojure expression to load a file. -This format string should use `%s' to substitute a file name and +(defun inf-clojure-reload-form (_proc) + "Return the form to query the Inf-Clojure PROC for reloading a namespace. +If you are using REPL types, it will pickup the most appropriate +`inf-clojure-reload-form` variant." + inf-clojure-reload-form) + +(defcustom inf-clojure-reload-all-form "(require '%s :reload-all)" + "Format-string for building a Clojure expression to :reload-all a file. +Reload-all implies :reload and also forces loading of all libs +that the identified libs directly or indirectly load via require +or use. +This format string should use `%s' to substitute a namespace and should result in a Clojure form that will be sent to the inferior Clojure to load that file." :type 'string :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) + :package-version '(inf-clojure . "2.2.0")) -(defun inf-clojure-load-form () - "Return the form to query inferior Clojure for a var's documentation. +(defun inf-clojure-reload-all-form (_proc) + "Return the form to query the Inf-Clojure PROC for :reload-all of a namespace. If you are using REPL types, it will pickup the most appropriate -`inf-clojure-var-doc-form` variant." - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-load-form-lumo) - (`planck inf-clojure-load-form-planck) - (_ inf-clojure-load-form))) +`inf-clojure-reload-all-form` variant." + inf-clojure-reload-all-form) (defcustom inf-clojure-prompt "^[^=> \n]+=> *" "Regexp to recognize prompts in the Inferior Clojure mode." @@ -391,41 +596,46 @@ This should usually be a combination of `inf-clojure-prompt' and :safe #'booleanp :package-version '(inf-clojure . "2.0.0")) -(defvar inf-clojure-buffer nil - "The current inf-clojure process buffer. - -MULTIPLE PROCESS SUPPORT -=========================================================================== -To run multiple Clojure processes, you start the first up -with \\[inf-clojure]. It will be in a buffer named `*inf-clojure*'. -Rename this buffer with \\[rename-buffer]. You may now start up a new -process with another \\[inf-clojure]. It will be in a new buffer, -named `*inf-clojure*'. You can switch between the different process -buffers with \\[switch-to-buffer]. - -Commands that send text from source buffers to Clojure processes -- -like `inf-clojure-eval-defun' or `inf-clojure-show-arglists' -- have to choose a -process to send to, when you have more than one Clojure process around. This -is determined by the global variable `inf-clojure-buffer'. Suppose you -have three inferior Clojures running: - Buffer Process - foo inf-clojure - bar inf-clojure<2> - *inf-clojure* inf-clojure<3> -If you do a \\[inf-clojure-eval-defun] command on some Clojure source code, -what process do you send it to? - -- If you're in a process buffer (foo, bar, or *inf-clojure*), - you send it to that process. -- If you're in some other buffer (e.g., a source file), you - send it to the process attached to buffer `inf-clojure-buffer'. -This process selection is performed by function `inf-clojure-proc'. - -Whenever \\[inf-clojure] fires up a new process, it resets -`inf-clojure-buffer' to be the new process's buffer. If you only run -one process, this does the right thing. If you run multiple -processes, you might need to change `inf-clojure-buffer' to -whichever process buffer you want to use.") +(defcustom inf-clojure-auto-mode t + "Automatically enable `inf-clojure-minor-mode'. +All buffers in `clojure-mode' will automatically be in +`inf-clojure-minor-mode' unless set to nil." + :type 'boolean + :safe #'booleanp + :package-version '(inf-clojure . "3.1.0")) + +(defun inf-clojure--get-preferred-major-modes () + "Return list of preferred major modes that are actually available." + (cl-remove-if-not (lambda (mode) (featurep mode)) + inf-clojure-source-modes)) + +(defun inf-clojure--clojure-buffer-p () + "Return TRUE if the current buffer is a Clojure buffer." + (derived-mode-p (inf-clojure--get-preferred-major-modes))) + +(defun inf-clojure--clojure-buffers () + "Return a list of all existing `clojure-mode' buffers." + (cl-remove-if-not (lambda (buffer) + (with-current-buffer buffer + (inf-clojure--clojure-buffer-p))) + (buffer-list))) + +(defun inf-clojure-enable-on-existing-clojure-buffers () + "Enable inf-clojure's minor mode on existing Clojure buffers. +See command `inf-clojure-minor-mode'." + (interactive) + (dolist (mode (inf-clojure--get-preferred-major-modes)) + (add-hook (derived-mode-hook-name mode) #'inf-clojure-minor-mode)) + (dolist (buffer (inf-clojure--clojure-buffers)) + (with-current-buffer buffer + (inf-clojure-minor-mode +1)))) + +(defun inf-clojure-disable-on-existing-clojure-buffers () + "Disable command `inf-clojure-minor-mode' on existing Clojure buffers." + (interactive) + (dolist (buffer (inf-clojure--clojure-buffers)) + (with-current-buffer buffer + (inf-clojure-minor-mode -1)))) (define-derived-mode inf-clojure-mode comint-mode "Inferior Clojure" "Major mode for interacting with an inferior Clojure process. @@ -446,45 +656,51 @@ Customization: Entry to this mode runs the hooks on `comint-mode-hook' and You can send text to the inferior Clojure process from other buffers containing Clojure source. - `inf-clojure-switch-to-repl' switches the current buffer to the Clojure process buffer. + `inf-clojure-switch-to-repl' switches the current buffer to the Clojure + process buffer. `inf-clojure-eval-defun' sends the current defun to the Clojure process. `inf-clojure-eval-region' sends the current region to the Clojure process. Prefixing the inf-clojure-eval/defun/region commands with - a \\[universal-argument] causes a switch to the Clojure process buffer after sending - the text. + a \\[universal-argument] causes a switch to the Clojure process buffer after + sending the text. Commands:\\ -\\[comint-send-input] after the end of the process' output sends the text from the - end of process to point. -\\[comint-send-input] before the end of the process' output copies the sexp ending at point - to the end of the process' output, and sends it. -\\[comint-copy-old-input] copies the sexp ending at point to the end of the process' output, - allowing you to edit it before sending it. -If `comint-use-prompt-regexp' is nil (the default), \\[comint-insert-input] on old input - copies the entire old input to the end of the process' output, allowing - you to edit it before sending it. When not used on old input, or if - `comint-use-prompt-regexp' is non-nil, \\[comint-insert-input] behaves according to - its global binding. +\\[comint-send-input] after the end of the process' output sends the text from + the end of process to point. +\\[comint-send-input] before the end of the process' output copies the sexp + ending at point to the end of the process' output, and sends it. +\\[comint-copy-old-input] copies the sexp ending at point to the end of the + process' output,allowing you to edit it before sending it. +If `comint-use-prompt-regexp' is nil (the default), \\[comint-insert-input] on + old input copies the entire old input to the end of the process' output, + allowing you to edit it before sending it. When not used on old input, or if + `comint-use-prompt-regexp' is non-nil, \\[comint-insert-input] behaves + according to its global binding. \\[backward-delete-char-untabify] converts tabs to spaces as it moves back. \\[clojure-indent-line] indents for Clojure; with argument, shifts rest of expression rigidly with the current line. -\\[indent-sexp] does \\[clojure-indent-line] on each line starting within following expression. -Paragraphs are separated only by blank lines. Semicolons start comments. -If you accidentally suspend your process, use \\[comint-continue-subjob] -to continue it." +\\[indent-sexp] does \\[clojure-indent-line] on each line starting within + following expression. Paragraphs are separated only by blank lines. + Semicolons start comments. If you accidentally suspend your process, + use \\[comint-continue-subjob] to continue it." (setq comint-input-sender 'inf-clojure--send-string) (setq comint-prompt-regexp inf-clojure-comint-prompt-regexp) (setq mode-line-process '(":%s")) + ;; NOTE: Using Tree-sitter based syntax highlighting in comint + ;; buffer is currently not possible. (clojure-mode-variables) (clojure-font-lock-setup) - (inf-clojure-eldoc-setup) + (when inf-clojure-enable-eldoc + (inf-clojure-eldoc-setup)) (setq comint-get-old-input #'inf-clojure-get-old-input) (setq comint-input-filter #'inf-clojure-input-filter) (setq-local comint-prompt-read-only inf-clojure-prompt-read-only) (add-hook 'comint-preoutput-filter-functions #'inf-clojure-preoutput-filter nil t) (add-hook 'completion-at-point-functions #'inf-clojure-completion-at-point nil t) - (ansi-color-for-comint-mode-on)) + (ansi-color-for-comint-mode-on) + (when inf-clojure-auto-mode + (inf-clojure-enable-on-existing-clojure-buffers))) (defun inf-clojure-get-old-input () "Return a string containing the sexp ending at point." @@ -509,108 +725,334 @@ to continue it." (defun inf-clojure-preoutput-filter (str) "Preprocess the output STR from interactive commands." + (inf-clojure--log-string str "<-RES----") (cond ((string-prefix-p "inf-clojure-" (symbol-name (or this-command last-command))) ;; Remove subprompts and prepend a newline to the output string (inf-clojure-chomp (concat "\n" (inf-clojure-remove-subprompts str)))) (t str))) -(defvar inf-clojure-project-root-files - '("project.clj" "build.boot" "deps.edn") - "A list of files that can be considered project markers.") - -(defun inf-clojure-project-root () - "Retrieve the root directory of a project if available. - -Fallback to `default-directory.' if not within a project." - (or (car (remove nil - (mapcar (lambda - (file) - (locate-dominating-file default-directory file)) - inf-clojure-project-root-files))) - default-directory)) - -(defun inf-clojure-project-type () - "Determine the type, either leiningen or boot of the current project." - (or inf-clojure-project-type - (let ((default-directory (inf-clojure-project-root))) - (cond ((file-exists-p "project.clj") "lein") - ((file-exists-p "build.boot") "boot") - ((file-exists-p "deps.edn") "tools.deps") - (t "generic"))))) - -(defun inf-clojure-cmd (project-type) - "Determine the command `inf-clojure' needs to invoke for the PROJECT-TYPE." - (pcase project-type - ("lein" inf-clojure-lein-cmd) - ("boot" inf-clojure-boot-cmd) - ("tools.deps" inf-clojure-tools-deps-cmd) - (_ inf-clojure-generic-cmd))) - (defun inf-clojure-clear-repl-buffer () "Clear the REPL buffer." (interactive) - (let ((comint-buffer-maximum-size 0)) - (comint-truncate-buffer))) + (with-current-buffer (if (derived-mode-p 'inf-clojure-mode) + (current-buffer) + inf-clojure-buffer) + (let ((comint-buffer-maximum-size 0)) + (comint-truncate-buffer)))) + +(defun inf-clojure--swap-to-buffer-window (to-buffer) + "Switch to `TO-BUFFER''s window." + (let ((pop-up-frames + ;; Be willing to use another frame + ;; that already has the window in it. + (or pop-up-frames + (get-buffer-window to-buffer t)))) + (pop-to-buffer to-buffer '(display-buffer-reuse-window . ())))) + +(defun inf-clojure-switch-to-repl (eob-p) + "Switch to the inferior Clojure process buffer. +With prefix argument EOB-P, positions cursor at end of buffer." + (interactive "P") + (if (get-buffer-process inf-clojure-buffer) + (inf-clojure--swap-to-buffer-window inf-clojure-buffer) + (call-interactively #'inf-clojure)) + (when eob-p + (push-mark) + (goto-char (point-max)))) + +(defun inf-clojure-switch-to-recent-buffer () + "Switch to the most recently used `inf-clojure-minor-mode' buffer." + (interactive) + (let ((recent-inf-clojure-minor-mode-buffer (seq-find (lambda (buf) + (with-current-buffer buf (bound-and-true-p inf-clojure-minor-mode))) + (buffer-list)))) + (if recent-inf-clojure-minor-mode-buffer + (inf-clojure--swap-to-buffer-window recent-inf-clojure-minor-mode-buffer) + (message "inf-clojure: No recent buffer known.")))) + +(defun inf-clojure-quit (&optional buffer) + "Kill the REPL buffer and its underlying process. + +You can pass the target BUFFER as an optional parameter +to suppress the usage of the target buffer discovery logic." + (interactive) + (let ((target-buffer (or buffer (inf-clojure-select-target-repl)))) + (when (get-buffer-process target-buffer) + (delete-process target-buffer)) + (kill-buffer target-buffer))) + +(defun inf-clojure-restart (&optional buffer) + "Restart the REPL buffer and its underlying process. + +You can pass the target BUFFER as an optional parameter +to suppress the usage of the target buffer discovery logic." + (interactive) + (let* ((target-buffer (or buffer (inf-clojure-select-target-repl))) + (target-buffer-name (buffer-name target-buffer))) + ;; TODO: Try to recycle the old buffer instead of killing and recreating it + (inf-clojure-quit target-buffer) + (call-interactively #'inf-clojure) + (rename-buffer target-buffer-name))) + +(defun inf-clojure--project-name (dir) + "Extract a project name from a project DIR. +The name is simply the final segment of the path." + (file-name-nondirectory (directory-file-name dir))) + +(defun inf-clojure--project-dir () + "Return current Clojure project root." + (let ((project-vc-extra-root-markers '("project.clj" ; Leiningen + "build.gradle" ; Gradle + "build.gradle.kts" ; Gradle + "deps.edn" ; Clojure CLI (a.k.a. tools.deps) + "shadow-cljs.edn" ; shadow-cljs + "bb.edn" ; babashka + "nbb.edn" ; nbb + "basilisp.edn" ; Basilisp (Python) + ))) + (project-root (project-current)))) + +(defvar clojure-ts-mode-syntax-table) ;;;###autoload -(defun inf-clojure (cmd) - "Run an inferior Clojure process, input and output via buffer `*inf-clojure*'. -If there is a process already running in `*inf-clojure*', just switch -to that buffer. -With argument, allows you to edit the command line (default is value -of `inf-clojure-*-cmd'). Runs the hooks from -`inf-clojure-mode-hook' (after the `comint-mode-hook' is run). -\(Type \\[describe-mode] in the process buffer for a list of commands.)" - (interactive (list (if current-prefix-arg - (read-string "Run Clojure: " (inf-clojure-cmd (inf-clojure-project-type))) - (inf-clojure-cmd (inf-clojure-project-type))))) - (if (not (comint-check-proc "*inf-clojure*")) +(defun inf-clojure (cmd &optional suppress-message) + "Run an inferior Clojure process, input and output via buffer *inf-clojure*. +If there is a process already running in *inf-clojure*, just +switch to that buffer. + +CMD is a string which serves as the startup command or a cons of +host and port. + + Prompts user for REPL startup command and REPL type if not +inferrable from startup command. Uses `inf-clojure-custom-repl-type' +and `inf-clojure-custom-startup' if those are set. +Use a prefix to prevent using these when they +are set. + +Prints a message that it has connected to the host and port +unless SUPPRESS-MESSAGE is truthy. + + Runs the hooks from `inf-clojure-mode-hook' (after the +`comint-mode-hook' is run). \(Type \\[describe-mode] in the +process buffer for a list of commands.)" + (interactive (list (or (unless current-prefix-arg + inf-clojure-custom-startup) + (completing-read "Select Clojure REPL startup command: " + (mapcar #'cdr inf-clojure-startup-forms) + nil + 'confirm-after-completion)))) + (let* ((project-dir (inf-clojure--project-dir)) + (process-buffer-name (or + inf-clojure-custom-repl-name + (if project-dir + (format "inf-clojure %s" (inf-clojure--project-name project-dir)) + "inf-clojure"))) + ;; comint adds the asterisks to both sides + (repl-buffer-name (format "*%s*" process-buffer-name))) + ;; Create a new comint buffer if needed + (unless (comint-check-proc repl-buffer-name) ;; run the new process in the project's root when in a project folder - (let ((default-directory (inf-clojure-project-root)) + (let ((default-directory (or project-dir default-directory)) (cmdlist (if (consp cmd) (list cmd) - (split-string cmd)))) - (message "Starting Clojure REPL via `%s'..." cmd) - (set-buffer (apply #'make-comint - "inf-clojure" (car cmdlist) nil (cdr cmdlist))) - (inf-clojure-mode))) - (setq inf-clojure-buffer "*inf-clojure*") - (if inf-clojure-repl-use-same-window - (pop-to-buffer-same-window "*inf-clojure*") - (pop-to-buffer "*inf-clojure*"))) + (split-string-and-unquote cmd))) + (repl-type (or (unless prefix-arg + inf-clojure-custom-repl-type) + (car (rassoc cmd inf-clojure-startup-forms)) + (inf-clojure--prompt-repl-type)))) + (unless suppress-message + (message "Starting Clojure REPL via `%s'..." cmd)) + (with-current-buffer (apply #'make-comint + process-buffer-name (car cmdlist) nil (cdr cmdlist)) + (inf-clojure-mode) + (set-syntax-table (pcase (car (inf-clojure--get-preferred-major-modes)) + ('clojure-ts-mode clojure-ts-mode-syntax-table) + (_ clojure-mode-syntax-table))) + (setq-local inf-clojure-repl-type repl-type) + (hack-dir-local-variables-non-file-buffer)))) + ;; update the default comint buffer and switch to it + (setq inf-clojure-buffer (get-buffer repl-buffer-name)) + (if inf-clojure-repl-use-same-window + (pop-to-buffer-same-window repl-buffer-name) + (pop-to-buffer repl-buffer-name)) + repl-buffer-name)) + +;;;###autol +(defun inf-clojure-connect (host port &optional suppress-message) + "Connect to a running socket REPL server via `inf-clojure'. +HOST is the host the process is running on, PORT is where it's +listening. SUPPRESS-MESSAGE is optional and if truthy will +prevent showing the startup message." + (interactive "shost: \nnport: ") + (inf-clojure (cons host port) suppress-message)) + +(defvar-local inf-clojure-socket-callback nil + "Used to transfer state between the socket process buffer & REPL buffer.") + +(defvar-local inf-clojure-socket-buffer nil + "Used to kill the associated socket buffer when it's REPL buffer is killed.") + +(defun inf-clojure-socket-filter (process output) + "A filter that gets triggered each time the socket receives new OUTPUT. +This function prints out the output received but also +watches for a prompt using the `inf-clojure-prompt' regexp, once +this happens a callback is triggered if available. The callback +is intended to be used to trigger a `inf-clojure-connect' once we +can determine that a socket REPL is ready to receive a +connection. + +PROCESS is the process object that is being filtered. + +OUTPUT is the latest data received from the process" + (let ((server-buffer (process-buffer process))) + (when (buffer-live-p server-buffer) + (with-current-buffer server-buffer + (insert output))) + (let ((prompt-displayed (string-match inf-clojure-prompt output))) + (when prompt-displayed + (with-current-buffer server-buffer + (when inf-clojure-socket-callback + (funcall inf-clojure-socket-callback))))))) + +(defun inf-clojure-socket-repl-sentinel (process _event) + "Ensures socket REPL are cleaned up when the REPL buffer is closed. + +PROCESS is the process object that is connected to a socket REPL. + +EVENT is the event that triggered this function to be called." + (when (not (process-live-p process)) + (let ((repl-buffer (process-buffer process))) + (with-current-buffer repl-buffer + (when inf-clojure-socket-buffer + (kill-buffer inf-clojure-socket-buffer)))))) + +(defvar inf-clojure-socket-repl-startup-forms + '((lein . "JVM_OPTS='-Dclojure.server.repl={:port %d :accept clojure.core.server/repl}' lein repl") + (boot . "export BOOT_JVM_OPTIONS='-Dclojure.server.repl=\"{:port %d :accept clojure.core.server/repl}\"' boot repl") + (clojure . "clojure -J-Dclojure.server.repl=\"{:port %d :accept clojure.core.server/repl}\"") + (cljs . "clojure -J-Dclojure.server.repl=\"{:port %d :accept cljs.server.browser/repl}\"") + (lein-clr . "JVM_OPTS='-Dclojure.server.repl={:port %d :accept clojure.core.server/repl}' lein clr repl") + (planck . "planck -n %d") + (babashka . "bb socket-repl %d"))) + +(defcustom inf-clojure-socket-repl-port + nil + "Port to be used when creating a socket REPL via `inf-clojure-socket-repl'. +If left as nil a random port will be selected between 5500-6000." + :type '(choice integer (const nil)) + :package-version '(inf-clojure . "3.3")) ;;;###autoload -(defun inf-clojure-connect (host port) - "Connect to a running socket-repl via `inf-clojure'. -HOST is the host the process is running on, PORT is where it's listening." - (interactive "shost: \nnport: ") - (inf-clojure (cons host port))) +(defun inf-clojure-socket-repl (cmd) + "Start a socket REPL server and connects to it via `inf-clojure-connect'. +CMD is the command line instruction used to start the socket +REPL. It should be a string with \"%d\" in it to take a random +port. Set `inf-clojure-custom-startup' or choose from the +defaults provided in `inf-clojure-socket-repl-startup-forms'." + (interactive (list (or (unless current-prefix-arg + inf-clojure-custom-startup) + (completing-read "Select Clojure socket REPL startup command: " + (mapcar #'cdr inf-clojure-socket-repl-startup-forms) + nil + 'confirm-after-completion)))) + (let* ((host "localhost") + (port (or inf-clojure-socket-repl-port (+ 5500 (random 500)))) + (project-dir (inf-clojure--project-dir)) + (repl-type (or (unless prefix-arg + inf-clojure-custom-repl-type) + (car (rassoc cmd inf-clojure-socket-repl-startup-forms)) + (inf-clojure--prompt-repl-type))) + (project-name (inf-clojure--project-name (or project-dir "standalone"))) + (socket-process-name (format "*%s-%s-socket-server*" project-name repl-type)) + (socket-buffer (get-buffer-create + (format "*%s-%s-socket*" project-name repl-type))) + (socket-cmd (format cmd port)) + (sock (let ((default-directory (or project-dir default-directory))) + (start-file-process-shell-command + socket-process-name socket-buffer + socket-cmd)))) + (with-current-buffer socket-buffer + (setq-local + inf-clojure-socket-callback + (lambda () + (let* ((inf-clojure-custom-repl-type repl-type) + (created-repl-buffer (inf-clojure-connect host port :suppress-message))) + (with-current-buffer (get-buffer created-repl-buffer) + (setq-local inf-clojure-socket-buffer socket-buffer) + (set-process-sentinel + (get-buffer-process (current-buffer)) + #'inf-clojure-socket-repl-sentinel)))))) + (set-process-filter sock #'inf-clojure-socket-filter) + (message "Starting %s socket REPL server at %s:%d with %s" repl-type host port socket-cmd))) + + +(defun inf-clojure--forms-without-newlines (str) + "Remove newlines between toplevel forms. +STR is a string of contents to be evaluated. When sending +multiple forms to a REPL, each newline triggers a prompt. +So we replace all newlines between top level forms but not inside +of forms." + (condition-case nil + (with-temp-buffer + (progn + ;; Activate preferred major mode. + (funcall (car (inf-clojure--get-preferred-major-modes))) + (insert str) + (whitespace-cleanup) + (goto-char (point-min)) + (while (not (eobp)) + (while (looking-at "\n") + (delete-char 1)) + ;; NOTE: There is no special API for that in + ;; `clojure-ts-mode', so probably for now lets keep this + ;; `clojure-mode' function. + (unless (eobp) + (clojure-forward-logical-sexp)) + (unless (eobp) + (forward-char) + ;; Remove an empty line at the end of the buffer. + (when (eobp) + (delete-char -1)))) + (buffer-substring-no-properties (point-min) (point-max)))) + (scan-error str))) (defun inf-clojure-eval-region (start end &optional and-go) "Send the current region to the inferior Clojure process. -Prefix argument AND-GO means switch to the Clojure buffer afterwards." +Sends substring between START and END. Prefix argument AND-GO +means switch to the Clojure buffer afterwards." (interactive "r\nP") - ;; drops newlines at the end of the region - (let ((str (replace-regexp-in-string - "[\n]+\\'" "" - (buffer-substring-no-properties start end)))) - (inf-clojure--send-string (inf-clojure-proc) str)) + (let* ((str (buffer-substring-no-properties start end)) + ;; newlines over a socket repl between top level forms cause + ;; a prompt to be returned. so here we dump the region into a + ;; temp buffer, and delete all newlines between the forms + (formatted (inf-clojure--forms-without-newlines str))) + (inf-clojure--send-string (inf-clojure-proc) formatted)) (when and-go (inf-clojure-switch-to-repl t))) (defun inf-clojure-eval-string (code) "Send the string CODE to the inferior Clojure process to be executed." (inf-clojure--send-string (inf-clojure-proc) code)) +(defun inf-clojure--defun-at-point (&optional bounds) + "Return text or range of defun at point. +If BOUNDS is truthy return a dotted pair of beginning and end of +current defun else return the string.." + (save-excursion + (end-of-defun) + (let ((end (point)) + (case-fold-search t) + (func (if bounds #'cons #'buffer-substring-no-properties))) + (beginning-of-defun-raw) + (funcall func (point) end)))) + (defun inf-clojure-eval-defun (&optional and-go) "Send the current defun to the inferior Clojure process. Prefix argument AND-GO means switch to the Clojure buffer afterwards." (interactive "P") (save-excursion - (end-of-defun) - (let ((end (point)) (case-fold-search t)) - (beginning-of-defun) - (inf-clojure-eval-region (point) end and-go)))) + (let ((bounds (inf-clojure--defun-at-point t))) + (inf-clojure-eval-region (car bounds) (cdr bounds) and-go)))) (defun inf-clojure-eval-buffer (&optional and-go) "Send the current buffer to the inferior Clojure process. @@ -635,30 +1077,46 @@ Prefix argument AND-GO means switch to the Clojure buffer afterwards." (inf-clojure-eval-last-sexp) (forward-sexp)) -(defun inf-clojure-switch-to-repl (eob-p) - "Switch to the inferior Clojure process buffer. -With prefix argument EOB-P, positions cursor at end of buffer." - (interactive "P") - (if (get-buffer-process inf-clojure-buffer) - (let ((pop-up-frames - ;; Be willing to use another frame - ;; that already has the window in it. - (or pop-up-frames - (get-buffer-window inf-clojure-buffer t)))) - (pop-to-buffer inf-clojure-buffer)) - (inf-clojure (inf-clojure-cmd (inf-clojure-project-type)))) - (when eob-p - (push-mark) - (goto-char (point-max)))) +(defun inf-clojure-insert-and-eval (form) + "Insert FORM into process and evaluate. +Indent FORM. FORM is expected to have been trimmed." + (let ((clojure-process (inf-clojure-proc))) + ;; ensure the repl buffer scrolls. See similar fix in CIDER: + ;; https://github.com/clojure-emacs/cider/pull/2590 + (with-selected-window (or (get-buffer-window inf-clojure-buffer) + (selected-window)) + (with-current-buffer (process-buffer clojure-process) + (comint-goto-process-mark) + (let ((beginning (point))) + (insert form) + (let ((end (point))) + (goto-char beginning) + (indent-sexp end) + ;; font-lock the inserted code + (font-lock-ensure beginning end) + (goto-char end))) + (comint-send-input t t))))) + +(defun inf-clojure-insert-defun () + "Send current defun to process." + (interactive) + (inf-clojure-insert-and-eval (string-trim (inf-clojure--defun-at-point)))) +(defun inf-clojure-insert-last-sexp () + "Send last sexp to process." + (interactive) + (inf-clojure-insert-and-eval + (buffer-substring-no-properties (save-excursion (backward-sexp) (point)) + (point)))) -;;; Now that inf-clojure-eval-/defun/region takes an optional prefix arg, -;;; these commands are redundant. But they are kept around for the user -;;; to bind if he wishes, for backwards functionality, and because it's -;;; easier to type C-c e than C-u C-c C-e. +;; Now that inf-clojure-eval-/defun/region takes an optional prefix arg, +;; these commands are redundant. But they are kept around for the user +;; to bind if he wishes, for backwards functionality, and because it's +;; easier to type C-c e than C-u C-c C-e. (defun inf-clojure-eval-region-and-go (start end) - "Send the current region to the inferior Clojure, and switch to its buffer." + "Send the current region to the inferior Clojure, and switch to its buffer. +START and END are the beginning and end positions in the buffer to send." (interactive "r") (inf-clojure-eval-region start end t)) @@ -667,345 +1125,78 @@ With prefix argument EOB-P, positions cursor at end of buffer." (interactive) (inf-clojure-eval-defun t)) -(defvar inf-clojure-prev-l/c-dir/file nil +(defvar inf-clojure-prev-loaded-dir-and-file nil "Record last directory and file used in loading or compiling. This holds a cons cell of the form `(DIRECTORY . FILE)' describing the last `inf-clojure-load-file' command.") -(defcustom inf-clojure-source-modes '(clojure-mode) - "Used to determine if a buffer contains Clojure source code. -If it's loaded into a buffer that is in one of these major modes, it's -considered a Clojure source file by `inf-clojure-load-file'. -Used by this command to determine defaults." - :type '(repeat symbol)) - (defun inf-clojure-load-file (&optional switch-to-repl file-name) - "Load a Clojure file FILE-NAME into the inferior Clojure process. + "Load a Clojure file into the inferior Clojure process. -The prefix argument SWITCH-TO-REPL controls whether to switch to REPL after the file is loaded or not." +The prefix argument SWITCH-TO-REPL controls whether to switch to +REPL after the file is loaded or not. If the argument FILE-NAME +is present it will be used instead of the current file." (interactive "P") - (let ((file-name (or file-name - (car (comint-get-source "Load Clojure file: " inf-clojure-prev-l/c-dir/file - ;; nil because doesn't need an exact name - inf-clojure-source-modes nil))))) + (let* ((proc (inf-clojure-proc)) + (file-name (or file-name + (car (comint-get-source "Load Clojure file: " inf-clojure-prev-loaded-dir-and-file + ;; nil because doesn't need an exact name + (inf-clojure--get-preferred-major-modes) nil)))) + (load-form (inf-clojure-get-feature proc 'load))) (comint-check-source file-name) ; Check to see if buffer needs saved. - (setq inf-clojure-prev-l/c-dir/file (cons (file-name-directory file-name) + (setq inf-clojure-prev-loaded-dir-and-file (cons (file-name-directory file-name) (file-name-nondirectory file-name))) - (inf-clojure--send-string (inf-clojure-proc) - (format (inf-clojure-load-form) file-name)) + (inf-clojure--send-string proc (format load-form file-name)) (when switch-to-repl (inf-clojure-switch-to-repl t)))) -(defun inf-clojure-connected-p () - "Return t if inferior Clojure is currently connected, nil otherwise." - (not (null inf-clojure-buffer))) - - -;;; Documentation functions: function doc, var doc, arglists, and -;;; describe symbol. -;;; =========================================================================== - -;;; Command forms -;;; ============= - -(defcustom inf-clojure-var-doc-form - "(clojure.repl/doc %s)" - "Form to query inferior Clojure for a var's documentation." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(define-obsolete-variable-alias 'inf-clojure-var-doc-command 'inf-clojure-var-doc-form "2.0.0") - -(defcustom inf-clojure-var-doc-form-lumo - "(lumo.repl/doc %s)" - "Lumo form to query inferior Clojure for a var's documentation." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-var-doc-form-planck - "(planck.repl/doc %s)" - "Planck form to query inferior Clojure for a var's documentation." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defun inf-clojure-var-doc-form () - "Return the form to query inferior Clojure for a var's documentation. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-var-doc-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-var-doc-form-lumo) - (`planck inf-clojure-var-doc-form-planck) - (_ inf-clojure-var-doc-form)))) - -(defcustom inf-clojure-var-source-form - "(clojure.repl/source %s)" - "Form to query inferior Clojure for a var's source." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-var-source-form-planck - "(planck.repl/source %s)" - "Planck form to query inferior Clojure for a var's source." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) +(declare-function clojure-ts-find-ns "clojure-ts-mode") -(defcustom inf-clojure-var-source-form-lumo - "(lumo.repl/source %s)" - "Lumo form to query inferior Clojure for a var's source." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) +(defun inf-clojure--find-ns () + "Return the namespace of the current Clojure buffer. -(defun inf-clojure-var-source-form () - "Return the form to query inferior Clojure for a var's source. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-var-source-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-var-source-form-lumo) - (`planck inf-clojure-var-source-form-planck) - (_ inf-clojure-var-source-form)))) - -(define-obsolete-variable-alias 'inf-clojure-var-source-command 'inf-clojure-var-source-form "2.0.0") - -(defcustom inf-clojure-arglists-form - "(try - (:arglists - (clojure.core/meta - (clojure.core/resolve - (clojure.core/read-string \"%s\")))) - (catch Throwable t nil))" - "Form to query inferior Clojure for a function's arglists." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) +This function delegates its job to an appropritate function, considering +`inf-clojure-source-modes'." + (pcase (car (inf-clojure--get-preferred-major-modes)) + ('clojure-ts-mode (clojure-ts-find-ns)) + (_ (clojure-find-ns)))) -(define-obsolete-variable-alias 'inf-clojure-arglist-command 'inf-clojure-arglists-form "2.0.0") +(defun inf-clojure-reload (arg) + "Send a query to the inferior Clojure for reloading the namespace. +See variable `inf-clojure-reload-form' and variable +`inf-clojure-reload-all-form'. -(defcustom inf-clojure-arglists-form-lumo - "(let [old-value lumo.repl/*pprint-results*] - (set! lumo.repl/*pprint-results* false) - (js/setTimeout #(set! lumo.repl/*pprint-results* old-value) 0) - (lumo.repl/get-arglists \"%s\"))" - "Lumo form to query inferior Clojure for a function's arglists." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-arglists-form-planck - "(planck.repl/get-arglists \"%s\")" - "Planck form to query inferior Clojure for a function's arglists." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.1.0")) - -(defun inf-clojure-arglists-form () - "Return the form to query inferior Clojure for arglists of a var. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-arglists-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-arglists-form-lumo) - (`planck inf-clojure-arglists-form-planck) - (_ inf-clojure-arglists-form)))) - -(defcustom inf-clojure-completion-form - "(complete.core/completions \"%s\")" - "Form to query inferior Clojure for completion candidates." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(define-obsolete-variable-alias 'inf-clojure-completion-command 'inf-clojure-completion-form "2.0.0") - -(defcustom inf-clojure-completion-form-lumo - "(let [ret (atom nil)] - (lumo.repl/get-completions \"%s\" - (fn [res] (reset! ret (map str res)))) - @ret)" - "Lumo form to query inferior Clojure for completion candidates." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-completion-form-planck - "(planck.repl/get-completions \"%s\")" - "Planck form to query inferior Clojure for completion candidates." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defun inf-clojure-completion-form () - "Return the form to query inferior Clojure for a var's documentation. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-completion-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-completion-form-lumo) - (`planck inf-clojure-completion-form-planck) - (_ inf-clojure-completion-form)))) - -(defcustom inf-clojure-ns-vars-form - "(clojure.repl/dir %s)" - "Form to show the public vars in a namespace." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-ns-vars-form-lumo - "(lumo.repl/dir %s)" - "Lumo form to show the public vars in a namespace." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-ns-vars-form-planck - "(planck.repl/dir %s)" - "Planck form to show the public vars in a namespace." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defun inf-clojure-ns-vars-form () - "Return the form to query inferior Clojure for public vars in a namespace. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-ns-vars-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-ns-vars-form-lumo) - (`planck inf-clojure-ns-vars-form-planck) - (_ inf-clojure-ns-vars-form)))) - -(define-obsolete-variable-alias 'inf-clojure-ns-vars-command 'inf-clojure-ns-vars-form "2.0.0") - -(defcustom inf-clojure-set-ns-form - "(clojure.core/in-ns '%s)" - "Form to set the namespace of the inferior Clojure process." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) +The prefix argument ARG can change the behavior of the command: -(defcustom inf-clojure-set-ns-form-planck - "(in-ns '%s)" - "Planck form to set the namespace of the inferior Clojure process." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-set-ns-form-lumo - "(in-ns '%s)" - "Lumo form to set the namespace of the inferior Clojure process." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defun inf-clojure-set-ns-form () - "Return the form to set the ns of the inferior Clojure process. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-set-ns-form` variant." - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`planck inf-clojure-set-ns-form-planck) - (`lumo inf-clojure-set-ns-form-lumo) - (_ inf-clojure-set-ns-form))) - -(define-obsolete-variable-alias 'inf-clojure-set-ns-command 'inf-clojure-set-ns-form "2.0.0") - -(defcustom inf-clojure-apropos-form - "(doseq [var (sort (clojure.repl/apropos \"%s\"))] - (println (str var)))" - "Form to invoke apropos." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-apropos-form-lumo - "(lumo.repl/apropos \"%s\")" - "Planck form to invoke apropos." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-apropos-form-planck - "(doseq [var (sort (planck.repl/apropos \"%s\"))] - (println (str var)))" - "Planck form to invoke apropos." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defun inf-clojure-apropos-form () - "Return the form to query inferior Clojure for public vars in a namespace. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-ns-vars-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`lumo inf-clojure-apropos-form-lumo) - (`planck inf-clojure-apropos-form-planck) - (_ inf-clojure-apropos-form)))) - -(define-obsolete-variable-alias 'inf-clojure-apropos-command 'inf-clojure-apropos-form "2.0.0") - -(defcustom inf-clojure-macroexpand-form - "(clojure.core/macroexpand '%s)" - "Form to invoke macroexpand." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-macroexpand-form-planck - "(macroexpand '%s)" - "Planck form to invoke macroexpand." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defun inf-clojure-macroexpand-form () - "Return the form for macroexpansion in the inferior Clojure process. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-macroexpand-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`planck inf-clojure-macroexpand-form-planck) - (_ inf-clojure-macroexpand-form)))) - -(define-obsolete-variable-alias 'inf-clojure-macroexpand-command 'inf-clojure-macroexpand-form "2.0.0") - -(defcustom inf-clojure-macroexpand-1-form - "(clojure.core/macroexpand-1 '%s)" - "Form to invoke macroexpand-1." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) - -(defcustom inf-clojure-macroexpand-1-form-planck - "(macroexpand-1 '%s)" - "Planck form to invoke macroexpand-1." - :type 'string - :safe #'stringp - :package-version '(inf-clojure . "2.0.0")) + - \\`C-u' \\[inf-clojure-reload]: prompts for a namespace name. + - \\`M--' \\[inf-clojure-reload]: executes (require ... :reload-all). + - \\`M--' \\`C-u' \\[inf-clojure-reload]: reloads all AND prompts." + (interactive "P") + (let* ((proc (inf-clojure-proc)) + (reload-all-p (or (equal arg '-) (equal arg '(-4)))) + (prompt-p (or (equal arg '(4)) (equal arg '(-4)))) + (ns (if prompt-p + (car (inf-clojure-symprompt "Namespace" (inf-clojure--find-ns))) + (inf-clojure--find-ns))) + (form (if (not reload-all-p) + (inf-clojure-reload-form proc) + (inf-clojure-reload-all-form proc)))) + (inf-clojure--send-string proc (format form ns)))) -(defun inf-clojure-macroexpand-1-form () - "Return the form for macroexpand-1 in the inferior Clojure process. -If you are using REPL types, it will pickup the most approapriate -`inf-clojure-macroexpand-1-form` variant." - (inf-clojure--sanitize-command - (pcase (inf-clojure--set-repl-type (inf-clojure-proc)) - (`planck inf-clojure-macroexpand-1-form-planck) - (_ inf-clojure-macroexpand-1-form)))) +(defun inf-clojure-connected-p () + "Return t if inferior Clojure is currently connected, nil otherwise." + (not (null inf-clojure-buffer))) -(define-obsolete-variable-alias 'inf-clojure-macroexpand-1-command 'inf-clojure-macroexpand-1-form "2.0.0") + ;;; Ancillary functions ;;; =================== -;;; Reads a string from the user. (defun inf-clojure-symprompt (prompt default) + "Read a string from the user. + +It allows to specify a PROMPT string and a DEFAULT string to +display." (list (let* ((prompt (if default (format "%s (default %s): " prompt default) (concat prompt ": "))) @@ -1013,7 +1204,7 @@ If you are using REPL types, it will pickup the most approapriate (if (zerop (length ans)) default ans)))) -;;; Adapted from function-called-at-point in help.el. +;; Adapted from function-called-at-point in help.el. (defun inf-clojure-fn-called-at-pt () "Return the name of the function called in the current call. The value is nil if it can't find one." @@ -1039,20 +1230,24 @@ The value is nil if it can't find one." See function `inf-clojure-var-doc-form'. When invoked with a prefix argument PROMPT-FOR-SYMBOL, it prompts for a symbol name." (interactive "P") - (let ((var (if prompt-for-symbol - (car (inf-clojure-symprompt "Var doc" (inf-clojure-symbol-at-point))) - (inf-clojure-symbol-at-point)))) - (inf-clojure--send-string (inf-clojure-proc) (format (inf-clojure-var-doc-form) var)))) + (let* ((proc (inf-clojure-proc)) + (var (if prompt-for-symbol + (car (inf-clojure-symprompt "Var doc" (inf-clojure-symbol-at-point))) + (inf-clojure-symbol-at-point))) + (doc-form (inf-clojure-get-feature proc 'doc))) + (inf-clojure--send-string proc (format doc-form var)))) (defun inf-clojure-show-var-source (prompt-for-symbol) "Send a command to the inferior Clojure to give source for VAR. See variable `inf-clojure-var-source-form'. When invoked with a prefix argument PROMPT-FOR-SYMBOL, it prompts for a symbol name." (interactive "P") - (let ((var (if prompt-for-symbol - (car (inf-clojure-symprompt "Var source" (inf-clojure-symbol-at-point))) - (inf-clojure-symbol-at-point)))) - (inf-clojure--send-string (inf-clojure-proc) (format (inf-clojure-var-source-form) var)))) + (let* ((proc (inf-clojure-proc)) + (var (if prompt-for-symbol + (car (inf-clojure-symprompt "Var source" (inf-clojure-symbol-at-point))) + (inf-clojure-symbol-at-point))) + (source-form (inf-clojure-get-feature proc 'source))) + (inf-clojure--send-string proc (format source-form var)))) ;;;; Response parsing ;;;; ================ @@ -1080,10 +1275,10 @@ STRING if present." (concat tag "\n") (concat (prin1-to-string tag) "\n"))) (let ((print-escape-newlines t)) - (prin1-to-string string))) + (prin1-to-string (substring-no-properties string)))) nil (expand-file-name inf-clojure--log-file-name - (inf-clojure-project-root)) + (inf-clojure--project-dir)) 'append 'no-annoying-write-file-in-minibuffer))) @@ -1098,6 +1293,17 @@ are going to match those." (length string)) (or (string-match prompt string) (length string)))) +(defun inf-clojure--get-redirect-buffer () + "Get the redirection buffer, creating it if necessary. + +It is the buffer used for processing REPL responses, see variable +\\[inf-clojure--redirect-buffer-name]." + (or (get-buffer inf-clojure--redirect-buffer-name) + (let ((buffer (generate-new-buffer inf-clojure--redirect-buffer-name))) + (with-current-buffer buffer + (hack-dir-local-variables-non-file-buffer) + buffer)))) + ;; Originally from: ;; https://github.com/glycerine/lush2/blob/master/lush2/etc/lush.el#L287 (defun inf-clojure--process-response (command process &optional beg-regexp end-regexp) @@ -1108,20 +1314,20 @@ If BEG-REGEXP is nil, the result string will start from (point) in the results buffer. If END-REGEXP is nil, the result string will end at (point-max) in the results buffer. It cuts out the output from and including the `inf-clojure-prompt`." - (let ((work-buffer inf-clojure--redirect-buffer-name) + (let ((redirect-buffer-name inf-clojure--redirect-buffer-name) (sanitized-command (inf-clojure--sanitize-command command))) (when (not (string-empty-p sanitized-command)) (inf-clojure--log-string command "----CMD->") - (with-current-buffer (get-buffer-create work-buffer) + (with-current-buffer (inf-clojure--get-redirect-buffer) (erase-buffer) - (comint-redirect-send-command-to-process sanitized-command work-buffer process nil t) - ;; Wait for the process to complete - (set-buffer (process-buffer process)) + (comint-redirect-send-command-to-process sanitized-command redirect-buffer-name process nil t)) + ;; Wait for the process to complete + (with-current-buffer (process-buffer process) (while (and (null comint-redirect-completed) (accept-process-output process 1 0 t)) - (sleep-for 0.01)) - ;; Collect the output - (set-buffer work-buffer) + (sleep-for 0.01))) + ;; Collect the output + (with-current-buffer redirect-buffer-name (goto-char (point-min)) (let* ((buffer-string (buffer-substring-no-properties (point-min) (point-max))) (boundaries (inf-clojure--string-boundaries buffer-string inf-clojure-prompt beg-regexp end-regexp)) @@ -1129,13 +1335,11 @@ output from and including the `inf-clojure-prompt`." (end-pos (car (cdr boundaries))) (prompt-pos (car (cdr (cdr boundaries)))) (response-string (substring buffer-string beg-pos (min end-pos prompt-pos)))) - (inf-clojure--log-string buffer-string "<-BUF----") - (inf-clojure--log-string boundaries "<-BND----") - (inf-clojure--log-string response-string "<-RES----") + (inf-clojure--log-string buffer-string "<-RES----") response-string))))) (defun inf-clojure--nil-string-match-p (string) - "Return true iff STRING is not nil. + "Return non-nil iff STRING is not nil. This function also takes into consideration weird escape character and matches if nil is anywhere within the input string." @@ -1170,7 +1374,7 @@ for evaluation, therefore FORM should not include it." (when response (funcall match-p response)))) (defun inf-clojure--some-response-p (proc form) - "Return true iff PROC's response after evaluating FORM is not nil." + "Return non-nil iff PROC's response after evaluating FORM is not nil." (inf-clojure--process-response-match-p (lambda (string) (not (inf-clojure--nil-string-match-p (string-trim string)))) @@ -1182,10 +1386,11 @@ for evaluation, therefore FORM should not include it." (defun inf-clojure-arglists (fn) "Send a query to the inferior Clojure for the arglists for function FN. See variable `inf-clojure-arglists-form'." - (thread-first - (format (inf-clojure-arglists-form) fn) - (inf-clojure--process-response (inf-clojure-proc) "(" ")") - (inf-clojure--some))) + (when-let* ((proc (inf-clojure-proc 'no-error)) + (arglists-form (inf-clojure-get-feature proc 'arglists))) + (thread-first (format arglists-form fn) + (inf-clojure--process-response proc "(" ")") + (inf-clojure--some)))) (defun inf-clojure-show-arglists (prompt-for-symbol) "Show the arglists for function FN in the mini-buffer. @@ -1196,18 +1401,21 @@ prefix argument PROMPT-FOR-SYMBOL, it prompts for a symbol name." (car (inf-clojure-symprompt "Arglists" (inf-clojure-fn-called-at-pt))) (inf-clojure-fn-called-at-pt))) (eldoc (inf-clojure-arglists fn))) - (when eldoc - (message "%s: %s" fn eldoc)))) + (if eldoc + (message "%s: %s" fn eldoc) + (message "Arglists not supported for this repl")))) (defun inf-clojure-show-ns-vars (prompt-for-ns) "Send a query to the inferior Clojure for the public vars in NS. See variable `inf-clojure-ns-vars-form'. When invoked with a prefix argument PROMPT-FOR-NS, it prompts for a namespace name." (interactive "P") - (let ((ns (if prompt-for-ns - (car (inf-clojure-symprompt "Ns vars" (clojure-find-ns))) - (clojure-find-ns)))) - (inf-clojure--send-string (inf-clojure-proc) (format (inf-clojure-ns-vars-form) ns)))) + (let* ((proc (inf-clojure-proc)) + (ns (if prompt-for-ns + (car (inf-clojure-symprompt "Ns vars" (inf-clojure--find-ns))) + (inf-clojure--find-ns))) + (ns-vars-form (inf-clojure-get-feature proc 'ns-vars))) + (inf-clojure--send-string proc (format ns-vars-form ns)))) (defun inf-clojure-set-ns (prompt-for-ns) "Set the ns of the inferior Clojure process to NS. @@ -1215,41 +1423,38 @@ See variable `inf-clojure-set-ns-form'. It defaults to the ns of the current buffer. When invoked with a prefix argument PROMPT-FOR-NS, it prompts for a namespace name." (interactive "P") - (let ((ns (if prompt-for-ns - (car (inf-clojure-symprompt "Set ns to" (clojure-find-ns))) - (clojure-find-ns)))) + (let* ((proc (inf-clojure-proc)) + (ns (if prompt-for-ns + (car (inf-clojure-symprompt "Set ns to" (inf-clojure--find-ns))) + (inf-clojure--find-ns))) + (set-ns-form (inf-clojure-get-feature proc 'set-ns))) (when (or (not ns) (equal ns "")) (user-error "No namespace selected")) - (inf-clojure--send-string (inf-clojure-proc) (format (inf-clojure-set-ns-form) ns)))) + (inf-clojure--send-string proc (format set-ns-form ns)))) -(defun inf-clojure-apropos (var) - "Send a form to the inferior Clojure to give apropos for VAR. -See variable `inf-clojure-apropos-form'." +(defun inf-clojure-apropos (expr) + "Send an expression to the inferior Clojure for apropos. +EXPR can be either a regular expression or a stringable +thing. See variable `inf-clojure-apropos-form'." (interactive (inf-clojure-symprompt "Var apropos" (inf-clojure-symbol-at-point))) - (inf-clojure--send-string (inf-clojure-proc) (format (inf-clojure-apropos-form) var))) + (let* ((proc (inf-clojure-proc)) + (apropos-form (inf-clojure-get-feature proc 'apropos))) + (inf-clojure--send-string proc (format apropos-form expr)))) (defun inf-clojure-macroexpand (&optional macro-1) - "Send a form to the inferior Clojure to give apropos for VAR. + "Send a form to the inferior Clojure for macro expansion. See variable `inf-clojure-macroexpand-form'. -With a prefix arg MACRO-1 uses `inf-clojure-macroexpand-1-form'." +With a prefix arg MACRO-1 uses function `inf-clojure-macroexpand-1-form'." (interactive "P") - (let ((last-sexp (buffer-substring-no-properties (save-excursion (backward-sexp) (point)) (point)))) + (let* ((proc (inf-clojure-proc)) + (last-sexp (buffer-substring-no-properties (save-excursion (backward-sexp) (point)) (point))) + (macroexpand-form (inf-clojure-get-feature proc + (if macro-1 + 'macroexpand-1 + 'macroexpand)))) (inf-clojure--send-string - (inf-clojure-proc) - (format (if macro-1 - (inf-clojure-macroexpand-1-form) - (inf-clojure-macroexpand-form)) - last-sexp)))) - - -(defun inf-clojure-proc () - "Return the current inferior Clojure process. -See variable `inf-clojure-buffer'." - (let ((proc (get-buffer-process (if (derived-mode-p 'inf-clojure-mode) - (current-buffer) - inf-clojure-buffer)))) - (or proc - (error "No Clojure subprocess; see variable `inf-clojure-buffer'")))) + proc + (format macroexpand-form last-sexp)))) (defun inf-clojure--list-or-nil (data) "Return DATA if and only if it is a list." @@ -1265,18 +1470,6 @@ every other EXPR will be discarded and nil will be returned." (inf-clojure--read-or-nil) (inf-clojure--list-or-nil))) -(defun inf-clojure-completions (expr) - "Return completions for the Clojure expression starting with EXPR. - -Under the hood it calls the function -\\[inf-clojure-completions-fn] passing in the result of -evaluating \\[inf-clojure-completion-form] at the REPL." - (when (not (string-blank-p expr)) - (let ((proc (inf-clojure-proc)) - (completion-form (format (inf-clojure-completion-form) (substring-no-properties expr)))) - (funcall inf-clojure-completions-fn - (inf-clojure--process-response completion-form proc "(" ")"))))) - (defcustom inf-clojure-completions-fn 'inf-clojure-list-completions "The function that parses completion results. @@ -1299,20 +1492,44 @@ which is able to parse results in list form only. You can peek at its implementation for getting to know some utility functions you might want to use in your customization." :type 'function - :safe #'functionp :package-version '(inf-clojure . "2.1.0")) -(defconst inf-clojure-clojure-expr-break-chars " \t\n\"\'`><,;|&{()[]") +(defun inf-clojure-completions (expr) + "Return completions for the Clojure expression starting with EXPR. + +Under the hood it calls the function +\\[inf-clojure-completions-fn] passing in the result of +evaluating \\[inf-clojure-completion-form] at the REPL." + (let* ((proc (inf-clojure-proc 'no-error)) + (completion-form (inf-clojure-get-feature proc 'completion t))) + (when (and proc completion-form (not (string-blank-p expr))) + (let ((completion-expr (format completion-form (substring-no-properties expr)))) + (funcall inf-clojure-completions-fn + (inf-clojure--process-response completion-expr proc "(" ")")))))) + +(defconst inf-clojure-clojure-expr-break-chars "^][ \"'`><,;|&{()@\\^" + "A list of characters that serve as expression boundaries. + +See `inf-clojure-completion-bounds-of-expr-at-point'.") + +(defun inf-clojure--kw-to-symbol (kw) + "Convert the keyword KW to a symbol." + (when kw + (replace-regexp-in-string "\\`:+" "" kw))) (defun inf-clojure-completion-bounds-of-expr-at-point () "Return bounds of expression at point to complete." (when (not (memq (char-syntax (following-char)) '(?w ?_))) (save-excursion - (let ((end (point))) - (skip-chars-backward (concat "^" inf-clojure-clojure-expr-break-chars)) - (let ((first-char (substring-no-properties (thing-at-point 'symbol) 0 1))) - (when (string-match-p "[^0-9]" first-char) - (cons (point) end))))))) + (let* ((end (point)) + (skipped-back (skip-chars-backward inf-clojure-clojure-expr-break-chars)) + (start (+ end skipped-back)) + (chars (or (thing-at-point 'symbol) + (inf-clojure--kw-to-symbol (buffer-substring start end))))) + (when (> (length chars) 0) + (let ((first-char (substring-no-properties chars 0 1))) + (when (string-match-p "[^0-9]" first-char) + (cons (point) end)))))))) (defun inf-clojure-completion-expr-at-point () "Return expression at point to complete." @@ -1324,7 +1541,7 @@ you might want to use in your customization." "Retrieve the list of completions and prompt the user. Returns the selected completion or nil." (let ((bounds (inf-clojure-completion-bounds-of-expr-at-point))) - (when bounds + (when (and bounds (inf-clojure-get-feature (inf-clojure-proc) 'completion 'no-error)) (list (car bounds) (cdr bounds) (if (fboundp 'completion-table-with-cache) (completion-table-with-cache #'inf-clojure-completions) @@ -1396,7 +1613,9 @@ Return the number of nested sexp the point was over or after." (defun inf-clojure-eldoc () "Backend function for eldoc to show argument list in the echo area." + ;; todo: this never gets unset once connected and is a lie (when (and (inf-clojure-connected-p) + inf-clojure-enable-eldoc ;; don't clobber an error message in the minibuffer (not (member last-command '(next-error previous-error)))) (let* ((info (inf-clojure-eldoc-info-in-current-sexp)) @@ -1418,9 +1637,9 @@ Return the number of nested sexp the point was over or after." (message "inf-clojure (version %s)" inf-clojure-version)) (defun inf-clojure-select-target-repl () - "Find or select an inf-clojure buffer to operate on. + "Find or select an ‘inf-clojure’ buffer to operate on. -Useful for commands that can invoked outside of an inf-clojure buffer +Useful for commands that can invoked outside of an ‘inf-clojure’ buffer \\(e.g. from a Clojure buffer\\)." ;; if we're in a inf-clojure buffer we simply return in (if (eq major-mode 'inf-clojure-mode) @@ -1436,32 +1655,8 @@ Useful for commands that can invoked outside of an inf-clojure buffer (t (get-buffer (completing-read "Select target inf-clojure buffer: " (mapcar #'buffer-name repl-buffers)))))))) -(defun inf-clojure-quit (&optional buffer) - "Kill the REPL buffer and its underlying process. - -You can pass the target BUFFER as an optional parameter -to suppress the usage of the target buffer discovery logic." - (interactive) - (let ((target-buffer (or buffer (inf-clojure-select-target-repl)))) - (when (get-buffer-process target-buffer) - (delete-process target-buffer)) - (kill-buffer target-buffer))) - -(defun inf-clojure-restart (&optional buffer) - "Restart the REPL buffer and its underlying process. - -You can pass the target BUFFER as an optional parameter -to suppress the usage of the target buffer discovery logic." - (interactive) - (let* ((target-buffer (or buffer (inf-clojure-select-target-repl))) - (target-buffer-name (buffer-name target-buffer))) - ;; TODO: Try to recycle the old buffer instead of killing and recreating it - (inf-clojure-quit target-buffer) - (inf-clojure (inf-clojure-cmd (inf-clojure-project-type))) - (rename-buffer target-buffer-name))) - (defun inf-clojure--response-match-p (form match-p proc) - "Return MATCH-P on the result of sending FORM to PROC. + "Send FORM and apply MATCH-P on the result of sending it to PROC. Note that this function will add a \n to the end of the string for evaluation, therefore FORM should not include it." (funcall match-p (inf-clojure--process-response form proc nil))) diff --git a/test/inf-clojure-tests.el b/test/inf-clojure-tests.el new file mode 100644 index 0000000..af6dea9 --- /dev/null +++ b/test/inf-clojure-tests.el @@ -0,0 +1,161 @@ +;;; inf-clojure-tests.el --- Tests for Inf-Clojure -*- lexical-binding: t; -*- +;; +;; Copyright © 2014-2021 Bozhidar Batsov + +;; Authors: Bozhidar Batsov +;; Andrea Richiardi + +;; This file is not part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: +;; +;; Tests for inf-clojure. + +;;; Code: + +(message "Running tests on Emacs %s" emacs-version) + +(require 'buttercup) +(require 'assess) ;; requires Emacs 26, due to a dependency on seq.el +(require 'inf-clojure) + +(cl-defmacro ict-with-assess-buffers ((&rest varlist) &body body) + `(assess-with-temp-buffers (,@varlist) + (clojure-mode) + (inf-clojure-minor-mode) + ,@body)) + +(defun ict-bounds-string (bounds) + (buffer-substring (car bounds) (cdr bounds))) + +(describe "inf-clojure--kw-to-symbol" + (it "returns symbol form of the given keyword" + (expect (inf-clojure--kw-to-symbol "symbol") :to-equal "symbol") + (expect (inf-clojure--kw-to-symbol ":clj.core/str") :to-equal "clj.core/str") + (expect (inf-clojure--kw-to-symbol "::keyword") :to-equal "keyword") + (expect (inf-clojure--kw-to-symbol nil) :to-equal nil))) + +(describe "completion bounds at point" + (it "computes bounds for plain-text" + (ict-with-assess-buffers + ((a (insert "plain-text"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal "plain-text")))) + + (it "computes bounds for @deref" + (ict-with-assess-buffers + ((a (insert "@deref"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal "deref")))) + + (it "computes bounds for ^:keyword" + (ict-with-assess-buffers + ((a (insert "^:keyword"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal ":keyword")))) + + (it "computes bounds for ::keyword" + (ict-with-assess-buffers + ((a (insert "::keyword"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal "::keyword")))) + + (it "computes bounds for [^:keyword (combined break chars and keyword)" + (ict-with-assess-buffers + ((a (insert "[^:keyword"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal ":keyword")))) + + (it "computes no bounds for point directly after a break expression" + (ict-with-assess-buffers + ((a (insert "@"))) + (with-current-buffer a + (expect (inf-clojure-completion-bounds-of-expr-at-point) + :to-be nil)))) + + (it "computes bounds for [symbol" + (ict-with-assess-buffers + ((a (insert "[symbol"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal "symbol")))) + + (it "computes bounds for (@deref (multiple break chars)" + (ict-with-assess-buffers + ((a (insert "(@deref"))) + (with-current-buffer a + (expect (ict-bounds-string (inf-clojure-completion-bounds-of-expr-at-point)) + :to-equal "deref"))))) + +(describe "inf-clojure--sanitize-command" + (it "sanitizes the command correctly" + (expect (inf-clojure--sanitize-command "(doc println)") :to-equal "(doc println)\n")) + + (it "trims newline at the right of a command" + (expect (inf-clojure--sanitize-command "(doc println)\n\n\n\n") :to-equal "(doc println)\n")) + + (it "returns empty string when the command is empty" + (expect (inf-clojure--sanitize-command " ") :to-equal "")) + + (it "only removes whitespace at the end of the command - fix 152" + (expect (inf-clojure--sanitize-command "1 5") :to-equal "1 5\n"))) + +(describe "inf-clojure--forms-without-newlines" + (it "removes newlines between toplevel forms" + (expect (inf-clojure--forms-without-newlines + "(def foo 3)\n\n\n(def bar 4)") + :to-equal "(def foo 3)\n(def bar 4)")) + (it "doesn't remove newlines inside forms or strings" + (expect (inf-clojure--forms-without-newlines + " + +(defn foo [] + + :foo) + + +(def thing \"this + +is a string\") + +(defn bar [])") + ;; note no leading newline, newlines inside defn remain, + ;; newlines inside string remain + :to-equal "(defn foo [] + + :foo) +(def thing \"this + +is a string\") +(defn bar [])"))) + + +(describe "inf-clojure--update-feature" + (it "updates new forms correctly" + (let ((inf-clojure-repl-features (inf-clojure--update-feature 'cljs 'doc "new doc"))) + (expect (inf-clojure--get-feature 'cljs 'doc nil) + :to-equal "new doc"))) + (describe "if the repl type is unknown" + (it "signals an error" + (expect (inf-clojure--update-feature 'not-found 'doc "new doc") + :to-throw)))) + +;;; inf-clojure-tests.el ends here diff --git a/todo.org b/todo.org new file mode 100644 index 0000000..811c287 --- /dev/null +++ b/todo.org @@ -0,0 +1,85 @@ +* Core + +** DONE set repl type on connection not first command +For some reason ~inf-clojure--set-repl-type~ is called in: +1. inf-clojure--send-string +2. inf-clojure-reload-form +3. inf-clojure-reload-all-form + +Seems better to do this on the two different connection methods and then be done with it? + +** DONE do we need repl type in both source buffer and connection? + these can get out of sync and lead to confusing errors when closing a repl and opening a new one. It seems like we keep the repl-type in the source buffer to prevent a single ~(with-current-buffer (process-buffer proc) inf-clojure-repl-type)~ + +** DONE Better dispatch for the implementations +Right now the functions are kinda clunky cond statements: +#+BEGIN_SRC emacs-lisp + (defun inf-clojure-cljs-features-dispatch (feature) + (case feature + ;; ((load) "(cljs.core/load-file \"%s\")") + ((doc) "(cljs.repl/doc %s)") + ((source) "(cljs.repl/source %s)") + ((arglists) "(try (->> '%s cljs.core/resolve cljs.core/meta :arglists) (catch :default _ nil))") + ((apropos) "(cljs.repl/apropos \"%s\")") + ((ns-vars) "(cljs.repl/dir %s)") + ((set-ns) "(cljs.core/in-ns '%s)") + ((macroexpand) "(cljs.core/macroexpand '%s)") + ((macroexpand-1) "(cljs.core/macroexpand-1 '%s)") + ;; ((completion) inf-clojure-completion-form-lumo) + )) +#+END_SRC + +I really want something similar to ~(defprotocol Inf-clojure-REPL (doc ...)(source ...))~ rather than just this super open ended dispatch on symbols. I just don't know enough elisp at the moment. Also, the current function version prevents introspection and providing a way of listing the repl's capabilities without duplicating the keys. Or maybe a hack of sending all of the known keys in and seeing which return strings. Still not great. + +** TODO Nicer interface to create a command +Right now everything is just a format string with a _single_ ~%s~ in it and that's called with ~(format feature-form e)~ where ~e~ is a symbol, or a form, or a whatever makes sense for that type of feature. This isn't super elegant although it does keep inf-clojure honest in that _all_ it does it format commands to ask a simple repl. But there could probably be a better way. + +** DONE Simpler way to define an implementation +This first pass is very mechanical and just rearranging so we can easily see which features are where. In the future we should look into just providing the repl namespace and seeing how far we can get with that. For instance, an API like ~(inf-clojure-register 'bb "bb.repl")~ and this would tell us where ~source~, ~doc~, ~apropos~ ...etc live. No reason to duplicate all of these. + +** DONE ability to update repl commands + we had this feature originally but now they are all literals. This is almost entirely fine but one problem. It would be common to toss clojure completions on the class path and then add the completions form for clojure. + +This should come back but only in a sense: if you don't have this on the classpath its obviously unacceptable to throw errors every time the user hits tab. Do we need some state recording if this is on the classpath or not maybe? + +#+BEGIN_SRC emacs-lisp + (defcustom inf-clojure-completion-form + "(complete.core/completions \"%s\")" + "Form to query inferior Clojure for completion candidates." + :type 'string + :safe #'stringp + :package-version '(inf-clojure . "2.0.0")) +#+END_SRC +** TODO Multiple connections +As proven by CIDER, multiple connections are just a pain. Scoping, navigating into dependencies, etc. This is a monster lurking + +** TODO navigation to source +The source primitive is quite nice but we most likely need a way to navigate to source. Possibly punt on this and just suggest people use with clojure-lsp? + +** TODO PREPL +Be nice to implement this now that we have parseedn in elisp to understand edn. + +** DONE inhibit custom repl-type and startup form +its nice to have these in dir-locals to just start up. but if you normally have ~clojure -m cljs.main -r~ as the startup command but you want to crank up a clj repl there's no way without removing those dir locals. +* Nice-to-haves +** TODO Put repl type in modeline +Rather than just ~*inf-clojure*~ we could put the repl type. Make it easy to follow and makes it easy to see when it gets it wrong. + +** TODO How do CIDER and inf-clojure play nice on the same emacs? +inf-clojure and CIDER are fighting over the keymappings. I've been doing a bit of a kludge to remove CIDER's tentacles from my clojure files for developing: +#+BEGIN_SRC emacs-lisp + (seq-doseq (buffer (buffer-list)) + (with-current-buffer buffer + (cider-mode -1)) + (remove-hook 'clojure-mode-hook #'cider-mode)) +#+END_SRC +Seems a bit heavy handed but its working for me so far. + +** TODO is disabling color still required? + in the readme it mentions that color should be turned off. in my usage I haven't run into this problem at all. perhaps no longer true? +** TODO nice startup +There's some project detection but that's becoming less and less useful as time goes on. Shadow, lein, deps.edn can all easily be mixed in the same project. And then lumo, planck, or bb scripts could live side by side. Rather than trying to guess the project type, I think i'd like to mimic geiser's style of handling multiple scheme backends. Perhaps ~m-x inf-clojure-run-planck~ and similar could help out. + +Some considerations: +- is this path aware? IE, don't show an option to run planck, lumo, etc, if they aren't visible or installed? +- should it have a rebuild function so that user registered implementations can show up in the ~m-x~ menu as well? pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy