1. Overview

Elin is a Clojure development environment, primarily written in Babashka.

This project is being developed as the successor to vim-iced and, as same as vim-iced, is heavily inspired by CIDER.

The goal of this project is to provide a server program that serves as the foundation for a Clojure development environment and its interface, enabling it to theoretically offer a Clojure development environment to any editor.

Joeun smiled. “Elin, the wild apple that grows in the mountains. What a nice name.”

2. Getting Started

2.1. Requirements

2.2. Installation

This is an example of installation using vim-plug.

vim-plug
Plug 'liquidz/elin'
dein
call dein#add('liquidz/elin')

Elin does not map any keys by default. If you want to use the default key mappings, see Key mappings.

2.3. Quick start

For quick start, you don’t need to create any project! Just open a Clojure file and run the following command.

$ vim foo.clj

# Execute the following command in Vim/Neovim
# :ElinInstantConnect babashka

You will to connect REPL, so let’s execute the next command.

:ElinEval (+ 1 2 3 4 5)

2.3.1. Start in your project

In the example above, we used a simple case with Babashka, but if you already have a Clojure project, you can get started by following the steps below.

First, start the REPL with the following command. Alternatively, you can also start the REPL within the Elin server using the ElinJackIn command.

$ clj -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.3.0"} cider/cider-nrepl {:mvn/version "0.50.2"}}}' -M -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]" --interactive

Once the REPL is running, connect to the REPL using the ElinConnect command.

3. Evaluation

The evaluation of S-expression is the most important element in REPL driven development.

3.1. Ranges

There are 3 main ranges to evaluation in elin.

  1. current expression

  2. current list

  3. current top list

See the following figure for the concrete ranges.

  (defn plus [foo bar]
    (+ foo bar))       ; When the cursor is on foo
;      <->               Current expression
;   <--------->          Current list
; <------------------>   Current top list

If you enable default key mappings, following key mappings are available.

Feature Default key mapping

Evaluate current expression

<Leader>ei

Evaluate current list

<Leader>ee

Evaluate current top list

<Leader>et

3.2. Results

For example, in ElinEvalCurrentTopList, the handler.evaluate/evaluate-current-top-list handler is called, so the following interceptors are executed.

interceptor.evaluate/output-eval-result-to-cmdline will output the evaluated result to command-line. interceptor.evaluate/set-eval-result-to-virtual-text will show the evaluated result as a popup at the end of line. However, the displayed result is only the returned value, so for example, the contents output by println are not displayed.

The contents output to standard output are displayed on the Information buffer.

3.2.1. Yank

interceptor.evaluate/yank-eval-result will yank the evaluated result to numbered registers. Like vim’s behavior, elin shifts the previous result of register 1 into register 2, 2 into 3, and so forth.

4. Completion

Elin provides only omni completion by default. It is set to omnifunc for clojure filetype automatically.

Vim has a <C-x><C-o> key mapping for omni completion.

4.1. Auto completion

Auto completion feature is provided as external elin plugins like follows.

Plugin Description

Completion source for neoclide/coc.nvim

Completion source for hrsh7th/nvim-cmp

5. Formatting

Elin does not provide a default formatter for now. The reason is that for format checks in CI, external tools such as cljfmt, cljstyle, or clojure-lsp are commonly used, and a formatter that only works within the editor is often unnecessary.

However, if you blocked when saving files in namespaces with large amounts of code due to the formatting entire buffer, you can use the elin-format plugin to format only the current form.

6. Navigation

6.1. Jump to definition

Elin provides the following command to jump to the definition of the symbol under the cursor.

Command Default key mapping

<C-]>

This command supports jumping to:

  • Qualified symbols

  • Local bindings

  • Protocol implementations

6.2. Refer usages

For browsing locations that refers to the symbol under the cursor, the following commannds are useful.

Command Default key mapping

<Leader>br

<Leader>blr

When only one location is found, the cursor will jump to the location immediately. Otherwise, the locations will be added to the location list.

6.3. Other navigations

6.3.1. Cycle source and test code

You can cycle source file and test file for current namespace.

Command Default key mapping

tt

For example, when you are in foo.core, ElinCycleSourceAndTest command will open the file which has foo.core-test namespace. If there is no corresponding file, elin suggests pseudo file path to create new namespace.

7. Lookup

There are some commands to lookup the documentation of the symbol under the cursor.

Command Default key mapping Description

K

Lookup docstring

<Leader>hs

Lookup source code

<Leader>hc

Lookup ClojureDocs

7.1. Lookup results

By default, the results of ElinLookup and ElinShowSource are shown in a popup, and the results of ElinShowClojureDocs are shown in information buffer and temporal buffer.

The result format is defined by configuration file as a mustache format.

Command Template

8. Namespace

Elin provides the following commands for namespace operations.

Command Default key mapping Description

<Leader>en

Evaluate ns form in the current buffer

<Leader>ran

Allow selection of the available namespaces in Selectors, and add the selected one to the ns form.

<Leader>ram

Add the missing libspec for the symbol under the cursor to the ns form. If there are multiple candidates, selectors will be used to select one.

9. Testing

Elin can integrate with clojure.test, and provides following test commands.

Command Default key mapping Description

<Leader>tt

Run a test under cursor

<Leader>tf

Run a test under cursor with focusing current testing form

<Leader>tn

Run all tests in current namespace

<Leader>tl

Rerun the last tests

<Leader>tr

Rerun the last failed tests

9.1. Testing results

Elin operates testing results with the following interceptors by default.

Interceptor Operation

Show results in command line

Record results in Temporal buffer and Information buffer

Set error positions to quickfix

Place signs to the error positions

9.2. Testing on plain nREPL

Elin will use cider-nrepl's testing feature if it is enabled, but it can provide equivalent test integration even if cider-nrepl is not enabled.

Therefore, when running tests, you don’t need to be concerned about whether cider-nrepl is enabled on the connected nREPL server. You can run tests and view the results using the same keymap.

10. ClojureScript

Elin only supports shadow-cljs for now.

10.1. shadow-cljs

To start CLJS REPL with shadow-cljs, you need following steps.

  1. Start to watch

    • $ npx shadow-cljs watch YOUR-BUILD-ID

  2. Access shadow-cljs’s HTTP server in your browser

  3. Connect to the nREPL

    • Execute ElinConnect command to connect to the nREPL.

    • For shadow-cljs, cljs-repl will be started automatically after connection.

    • You don’t need to specify your build ID anymore.

11. Skeleton

Elin provides code skeleton when you open new clojure files.

Currently, following extensions are supported.

  • *.clj

  • *.cljs

  • *.cljc

11.1. Skeleton template

Skeleton feature is provided by the interceptor.autocmd/skeleton interceptor, and the template is defined by configuration file as a mustache format.

The default templates are as follows:

File extension Template

*.clj

*.cljs

*.cljc

12. Debugging

12.1. Macro

Expanding macro is important for writing/debugging macros. Elin provides following commands.

Command Default key mapping

<Leader>em

Expanding results are shown in the Temporal buffer and Information buffer.

12.2. #dbg and #break

Elin supports CIDER’s #dbg and #break reader literals. The easiest way is to put #dbg to your code, and evaluate it.

(defn fib [n]
  #dbg (loop [a 0 b 1 n n]
         (if (<= n 0)
           a
           (recur b (+ a b) (dec n)))))

Once you evaluate (fib 10), debugger will launch.

This feature is implemented by the interceptor.debug/process-debugger interceptor.

13. Buffer

13.1. Information buffer

All information on Elin, such as the content of standard output during evaluation and test results, is recorded in this buffer. To show/clear the information buffer, use the following commands.

Command Default key mapping

ElinToggleInfoBuffer

<Leader>ss

ElinClearInfoBuffer

<Leader>sl

13.2. Temporal buffer

This buffer temporarily displays content that is newly recorded in the Information buffer when the Information buffer is not currently visible. It is used, for example, to display test results or macro expansion results.

This buffer automatically closes when the Information buffer is displayed.

14. Configuration

14.1. Vim/Neovim

14.1.1. Key mappings

Elin does not map any keys by default. If you want to use the default key mappings, set g:elin_enable_default_key_mappings to true.

let g:elin_enable_default_key_mappings = v:true

Default key mappings use <Leader> as a prefix. If you want to change it to <LocalLeader>, set g:elin_default_key_mapping_leader.

let g:elin_default_key_mapping_leader = '<LocalLeader>'

14.1.2. Selectors

To choose an item from multiple candidates, Elin uses a selector. The behavior of the selector differs between Vim and Neovim.

Vim

It has its own implementation, and its behavior can be modified through selector plugins. You can change which selector plugin to use by setting the selector name in g:elin#internal#select#selector.

" Requires junegunn/fzf.vim
let g:elin#internal#select#selector = 'fzf'
Neovim

Elin uses vim.ui.select(). Therefore, you can use your preferred plugin to modify the behavior of vim.ui.select() like follows.

14.1.3. Status line

elin#status function returns the nREPL connection status text, and you can use it in the status line.

let g:lightline = {
      \   'tabline': {
      \     'right': [['filetype', 'bufnum' ], ['elin'] ]
      \   },
      \   'component_function': {
      \     'elin': 'elin#status',
      \   },
      \ }

14.1.4. Integration with other plugins

There are some Plugin that can be used with Elin.

Plugin Description URL

coc.nvim

Completion source

nvim-cmp

Completion source

telescope.nvim

File selector

14.2. Server configuration files

All of elin server’s behavior is defined in resources/config.edn, and all of these can be modified through the configuration file.

The configuration files are loaded in the following order. Settings loaded later take precedence.

  1. Default configuration

  2. Plugin configuration

  3. User configuration

    • $XDG_CONFIG_HOME/elin/config.edn

    • $HOME/.config/elin/config.edn

  4. Project local configuration

    • .elin/config.edn

14.3. Server configuration structure

In the configuration file, settings are primarily written on a per-component basis.

{;; Log settings
 :log { ... }

 ;; HTTP server component settings
 :http-server { ... }

 ;; Interceptor component settings
 :interceptor { ... }

 ;; Handler component settings
 :handler { ... }

 ;; Clj-kondo component settings
 :clj-kondo { ... }}

14.3.1. Handler and Interceptor

As a user, you will configure mainly Handler and Interceptor, which are the core components of Elin. Both are configured using four keys: includes, excludes, config-map, or uses.

includes

:includes enables the specified handlers/interceptors.

e.g.
{:handler {:includes [elin.handler.connect/connect]}}
excludes

:excludes disables handlers/interceptors that were enabled by previously loaded settings. If both :includes and :excludes are specified within the same configuration, :includes should be prioritized.

e.g.
{:handler {:excludes [elin.handler.connect/connect]}}
config-map

:config-map specifies the settings related to handlers/interceptors.

e.g.
{:handler {:config-map {elin.handler.connect/connect {:param "value"}}}}

In the case of handlers, you can specify interceptors that are only active when the handler is being processed.

{:handler {:config-map {elin.handler.connect/connect {:interceptor {:includes [dummy/interceptor]}}}}}
uses

:uses is syntactic sugar for :includes and :config-map, and the following two are equivalent.

uses
{:uses [foo {:bar "baz"}]}
includes and config-map
{:includes [foo]
 :config-map {foo {:bar "baz"}}}

14.4. Overriding default behaviors

As explained in Server configuration files, the default settings can be overridden by user or project-local configurations.

14.4.1. Overriding config-map

Let’s focus on the feature that displays evaluation results as virtual text. By default, the highlight setting for results displayed as virtual text is DiffText, so you can override this configuration with your preferred highlight setting.

First, you need to investigate how this feature is configured. By tracing the handler from ElinEvalCurrentTopList, you will find that handler.evaluate/evaluate-current-top-list handler is called. In the help section under Using interceptors, you can see that interceptor.evaluate/set-eval-result-to-virtual-text is set as the interceptor to be executed.

Looking at the help for the interceptor, you’ll discover that you can change the highlight setting using the highlight key. Now, following Server configuration files, create a user configuration file and save it with the following content. This will change the default highlight setting from DiffText to Title in interceptor.evaluate/set-eval-result-to-virtual-text.

{:interceptor {:config-map {elin.interceptor.evaluate/set-eval-result-to-virtual-text {:highlight "Title"}}}}

14.4.2. Replacing the interceptor

ElinLookup displays results in a popup by default, but you can change it to append them to the Information buffer instead.

By tracing the handler from ElinLookup, you will find that handler.lookup/lookup handler is called. In the help section under Using interceptors, you can see that interceptor.handler/show-result-as-popup is set as the interceptor to be executed.

On the other hand, Built-in Interceptors includes interceptor.handler/append-result-to-info-buffer. Similar to interceptor.handler/show-result-as-popup, this interceptor is executed on calling handlers, so by replacing it, you can change the behavior of ElinLookup.

{:handler {:uses [elin.handler.lookup/lookup
                  {:interceptor {:excludes [elin.interceptor.handler/show-result-as-popup]
                                 :uses [elin.interceptor.handler/append-result-to-info-buffer
                                        {:show-temporarily? true}]}}]}}

15. Plugin

The plugins are tagged with the elin-clj-plugin topic on GitHub, so you can view the list from the following link.

15.1. Your plugin

For Vim/Neovim, it searches for elin/plugin.edn in the runtime path and handle the directory containing the plugin.edn file as a plugin.

elin/plugin.edn
{:name "YOUR PLUGIN NAME"

 ;; OPTIONAL: Your plugin configuration
 :export {:handler { ... }
          :interceptor { ... }}}

The Elin server adds to the classpath the directory containing plugin.edn and applies the configurations defined under the :export key in plugin.edn. If :export is not defined, the Elin server only adds the directory to the classpath and takes no further action.

For the actual processing, please refer to the code for handlers/interceptors in the Elin core or existing plugins.

16. Handlers

Handlers process requests from the host editor.

16.1. Built-in Handlers

16.1.1. handler.callback/callback

No document.

16.1.2. handler.complete/complete

Returns comletion candidates.

16.1.3. handler.connect/connect

Connect to nREPL server.

Default key mapping: <Leader>'

16.1.4. handler.connect/disconnect

Disconnect from nREPL server.

16.1.5. handler.connect/instant

Launch nREPL server of the specified project and connect to it.

16.1.6. handler.connect/jack-in

Launch nREPL server according to the project detected from the current file and connect to it.

Default key mapping: <Leader>"

16.1.7. handler.connect/switch

Switch the current nREPL connection to another connected one.

16.1.8. handler.debug/disable-debug-log

No document.

16.1.9. handler.debug/enable-debug-log

No document.

16.1.10. handler.debug/nrepl-request

Request any message to nREPL server. This handler is for debugging.

16.1.11. handler.evaluate/evaluate

Evaluate specified code.

16.1.12. handler.evaluate/evaluate-at-mark

Evaluate top list at mark.

Default key mapping: <Leader>ea

16.1.13. handler.evaluate/evaluate-current-buffer

Evaluate current buffer.

Default key mapping: <Leader>eb

16.1.14. handler.evaluate/evaluate-current-expr

Evaluate current expression.

Default key mapping: <Leader>ei

16.1.15. handler.evaluate/evaluate-current-list

Evaludate current list.

Default key mapping: <Leader>ee

16.1.16. handler.evaluate/evaluate-current-top-list

Evaludate current top list.

Default key mapping: <Leader>et

16.1.17. handler.evaluate/evaluate-namespace-form

Evaluate ns form in current buffer.

Default key mapping: <Leader>en

16.1.18. handler.evaluate/expand-1-current-list

Expand macro once.

Default key mapping: <Leader>em

16.1.19. handler.evaluate/interrupt

Interrupt running evaluation.

Default key mapping: <Leader>eq

16.1.20. handler.evaluate/print-last-result

Print last evaluation result to InfoBuffer.

Default key mapping: <Leader>ep

16.1.21. handler.evaluate/reload

Reload all changed files with tonsky/clj-reload.

Default key mapping: <Leader>enr

16.1.22. handler.evaluate/reload-all

Reload all files.

Default key mapping: <Leader>enR

16.1.23. handler.evaluate/undef

Undefine symbol at cursor position.

Default key mapping: <Leader>eu

16.1.24. handler.evaluate/undef-all

Undefine all symbols in current namespace.

Default key mapping: <Leader>eU

16.1.25. handler.lookup/lookup

Look up symbol at cursor position.

Default key mapping: K

16.1.26. handler.lookup/show-clojuredocs

Show clojuredocs of symbol at cursor position.

Default key mapping: <Leader>hc

16.1.27. handler.lookup/show-source

Show source code of symbol at cursor position.

Default key mapping: <Leader>hs

16.1.28. handler.namespace/add-libspec

Add libspec to namespace form.

Default key mapping: <Leader>ran

16.1.29. handler.namespace/add-missing-libspec

Add missing libspec to namespace form.

Default key mapping: <Leader>ram

16.1.30. handler.navigate/cycle-source-and-test

Cycle source code and test code.

Default key mapping: tt

16.1.31. handler.navigate/jump-to-definition

Jump to the definition of the symbol under the cursor.

Default key mapping: <C-]>

16.1.32. handler.navigate/local-references

No document.

Default key mapping: <Leader>blr

16.1.33. handler.navigate/references

Find the places where the symbol under the cursor is used, and jump if there is only one. If there are multiple, add them to the location list.

Default key mapping: <Leader>br

17. Interceptors

Interceptors intercept various processes and change their behavior.

17.1. Built-in Interceptors

17.1.1. interceptor.autocmd/clj-kondo-analyzing

Executed on autocmd fired.

No document.

17.1.2. interceptor.autocmd/deinitialize

Executed on autocmd fired.

No document.

17.1.3. interceptor.autocmd/ns-load

Executed on autocmd fired.

No document.

17.1.4. interceptor.autocmd/skeleton

Executed on autocmd fired.

Set skeleton to current new buffer.

Mustache template variables
Variable Description

{{path}}

File path

{{ns}}

Inferred namespace

{{source-ns}}

Source file namespace (only available on test file )

{{test?}}

true if the file is test file

17.1.5. interceptor.autocmd/switch-connection

Executed on autocmd fired.

No document.

17.1.6. interceptor.code-change/show-code-changed-result

Executed on changing code.

No document.

17.1.7. interceptor.connect/cleanup-jacked-in-process

Executed on disconnection.

No document.

17.1.8. interceptor.connect/connected

Executed on connection.

No document.

17.1.9. interceptor.connect/detect-clojure-port

Executed on connection.

No document.

17.1.10. interceptor.connect/raw-message-channel

Executed on connection.

No document.

17.1.11. interceptor.connect.shadow-cljs/detect-shadow-cljs-port

Executed on connection.

No document.

17.1.12. interceptor.debug/initialize-debugger

Executed on connection.

No document.

17.1.13. interceptor.debug/process-debugger

Executed on communicating with nREPL server.

No document.

17.1.14. interceptor.evaluate/eval-with-context

Executed on evaluation.

No document.

17.1.15. interceptor.evaluate/output-eval-result-to-cmdline

Executed on evaluation.

Output evaluated result to cmdline.

17.1.16. interceptor.evaluate/set-eval-result-to-virtual-text

Executed on evaluation.

Set evaluated result to virtual text.

Configuration
key type description

format

string

Format of virtual text. It can contain the following placeholders: result.

highlight

string

Highlight group for virtual text.

align

string

Alignment of virtual text. Possible values are: after, right.

close-after

integer

Close virtual text after the specified number of milliseconds.

17.1.17. interceptor.evaluate/unwrap-comment-form

Executed on evaluation.

No document.

17.1.18. interceptor.evaluate/yank-eval-result

Executed on evaluation.

Yank evaluated result.

17.1.19. interceptor.handler/append-result-to-info-buffer

Executed on calling handler.

Interceptor to show handler result temporarily.

17.1.20. interceptor.handler/handling-error

Executed on calling handler.

No document.

17.1.21. interceptor.handler/jump-to-file

Executed on calling handler.

Interceptor to jump to specified file.

17.1.22. interceptor.handler/show-result-as-popup

Executed on calling handler.

Interceptor to show handler result as popup.

17.1.23. interceptor.nrepl/eval-ns

Executed on requesting to nREPL server.

Interceptor to delete ns keyword from nREPL request on evaluating ns form.

17.1.24. interceptor.nrepl/normalize-path

Executed on requesting to nREPL server.

Interceptor to normalize path on nREPL response.

17.1.25. interceptor.nrepl/nrepl-output

Executed on communicating with nREPL server.

Interceptor to intercept nREPL output. This interceptor executes interceptors with e.c.interceptor/output kind.

17.1.26. interceptor.nrepl/output-result-to-cmdline

Executed on requesting to nREPL server.

Interceptor to output nREPL result as message.

17.1.27. interceptor.nrepl/progress

Executed on requesting to nREPL server.

Interceptor to show progress popup on nREPL request.

17.1.28. interceptor.output/print-output

Executed on output from nREPL server.

Interceptor to print output on nREPL to InfoBuffer.

Output format can be configured like below:

{:interceptor {:config-map {elin.interceptor.output/print-output
                            {:format "{{text}}"}}}}

Available variables: - type: Output type - text: Output text

17.1.29. interceptor.test/append-test-result-to-info-buffer

No document.

17.1.30. interceptor.test/apply-test-result-to-quickfix

No document.

17.1.31. interceptor.test/focus-current-testing

Executed on testing.

Re evaluate the current top list with focusing on the current testing form.

17.1.32. interceptor.test/output-test-result-to-cmdline

No document.

17.1.33. interceptor.test/parse-test-result

Executed on testing.

No document.

17.1.34. interceptor.test/store-last-failed-test-query

No document.

17.1.35. interceptor.test/update-test-result-sign

No document.

18. Host

18.1. Vim/Neovim

18.1.1. Commands

ElinConnect

Calls handler.connect/connect handler.

ElinInstantConnect

Calls handler.connect/instant handler.

ElinDisconnect
ElinJackIn

Calls handler.connect/jack-in handler.

ElinSwitchConnection

Calls handler.connect/switch handler.

ElinEval

Calls handler.evaluate/evaluate handler.

ElinEvalCurrentExpr
ElinEvalCurrentList
ElinEvalCurrentTopList
ElinEvalCurrentBuffer
ElinEvalNsForm
ElinEvalAtMark
ElinEvalInContext

Calls handler.evaluate/evaluate-current-list handler with the following interceptors:

ElinPrintLastResult
ElinInterrupt
ElinUndef

Calls handler.evaluate/undef handler.

ElinUndefAll
ElinReload

Calls handler.evaluate/reload handler.

ElinReloadAll
ElinMacroExpand1CurrentList
ElinAddLibspec
ElinAddMissingLibspec
ElinJumpToDefinition
ElinReferences
ElinLocalReferences
ElinLookup

Calls handler.lookup/lookup handler.

ElinShowSource
ElinShowClojureDocs
ElinTestUnderCursor
ElinTestFocusedCurrentTesting

Calls handler.test/run-test-under-cursor handler with the following interceptors:

ElinTestInNs
ElinTestLast
ElinTestLastFailed
ElinCycleSourceAndTest
ElinEnableDebugLog
ElinDisableDebugLog

19. Cheatsheet

Based on default key mappings.
See elin-mapping.txt for whole default key mappings.

Connection

<Leader>'

Make connection to nREPL

<Leader>ee

Evaluate current list

<Leader>et

Evaluate current top list

<Leader>eb

Evaluate current buffer

<Leader>eq

Interrupt code evaluation

<Leader>enr

Reload all changed files

<Leader>em

Evaluate macroexpand-1 for current list

<Leader>tt

Run test under cursor

<Leader>tn

Run tests in current namespace

<Leader>tl

Re run last test

<Leader>tr

Re run last failed tests

<C-]>

Jump cursor to the definition of symbol under cursor

tt

Cycle between source file and test file

<Leader>br

Jump to references of symbol under cursor

K

Show documents for the symbol under cursor

<Leader>hs

Show source for the symbol under cursor

<Leader>hc

Show ClojureDocs for the symbol under cursor

<Leader>ran

Add libspec to current ns form

<Leader>ram

Add missing libspec to current ns form

Others

<Leader>ss

Show/close information buffer

<Leader>sl

Clear contents in information buffer