boonmee is a language server for Clojure that focuses on features relating to host interop.
It is an attempt to bring first-class 'intellisense' to ClojureScript projects.
Goals:
- For now, focus on interop - there are other great tools that lint Clojure code already (clj-kondo, joker etc)
- Tooling-agnostic - you should be able to integrate boonmee into any IDE/editor tool
- All analysis should be static, and side-effect free (eg, does not evaluate any code)
Right now boonmee only works on ClojureScript code (my personal frustration), but there are plans to target the JVM as well.
You can read this blog post about boonmee and its implementation details.
The biggest strength of Clojure is the fact that it is a hosted language.
Every Clojure codebase I have worked on leverages a host library at its core.
And yet, most linting/editor tools (outside of Cursive for the JVM) consider the host language as an afterthought.
- Quickinfo (@jsdoc documentation, type signature, fn metadata etc)
- Code completions (require, fn calls)
- Code navigation (jump to definition)
- Warn on deprecated methods (via @jsdoc convention)
- Warn on undefined es6 method call
- Incorrect arity on es6 method call
- Basic type-checking
Download a binary from the releases page.
Binaries get built via GitHub Actions
Refer to the CI job on how to compile boonmee as a native image from source.
boonmee requires NodeJS, and the TypeScript standalone server (tsserver
):
npm install -g typescript
By default, boonmee will use the tsserver
found on your $PATH
. However, you can also specify a custom path:
./boonmee --tsserver=/path/to/tsserver
Interaction with boonmee happens via stdio:
./boonmee
Refer to the Example RPC section for some examples of client requests.
If you would like to use boonmee directly from a Clojure project, bring in the following dependency:
[wavejumper/boonmee "0.1.0-alpha2"]
(require '[boonmee.client.clojure :as boonmee])
(require '[clojure.core.async :as async])
(def client (boonmee/client {}))
(async/put! (:req-ch client) {}) ;; Make a request
(async/<!! (:resp-ch client)) ;; Wait until there is a response...
(boonmee/stop client)
See boonmee.el in this repo
WIP emacs client, currently supports:
- Quickinfo (
M-x boonmee-quickinfo
) - Code completions
- Code navigation (
M-x boonmee-goto-definition
)
In your init.el
, add something like:
(add-hook 'clojure-mode-hook (lambda() (boonmee-mode t)))
Note: boonmee analyses NPM dependencies found in a node_modules
directory at your project's root.
If you rely on cljsjs packages you're out of luck.
If you are a shadow-cljs user, using boonmee should be a seamless experience.
boonmee's functionality comes from the TypeScript compiler.
That means a @types/*
package should be installed as a dev dependency, if the library you require is written in vanilla JavaScript:
npm install --save-dev @types/react
The DefinitelyTyped/DefinitelyTyped repo has many type definitions for popular npm dependencies.
TODO: infer/suggest possible @types/
stubs.
The --env
switch tells boonmee which environment your ClojureScript project is targeting.
This enables intellisense for js/...
globals.
Options are: browser
(default) or node
.
./boonmee --env=node
Note for the node
env you will also need to npm install --save-dev @types/node
Specs for the boonmee protocol can be found in the boonmee.protocol namespace.
Here's our example Clojure source code:
(ns tonal.core
(:require ["@tonaljs/tonal" :refer [Midi]]))
(Midi/m ) ;; [4 7], left incomplete for our completions example
(Midi/midiToFreq 400) ;; [7 10], for our quickinfo and definitions example
Examples relate to the tonaljs npm package
For more examples, refer to boonmee's integration tests
{
"command": "completions",
"type": "request",
"requestId": "12345",
"arguments": {
"projectRoot": "/path/to/project/root",
"file": "/path/to/core.cljs",
"line": 4,
"offset": 7
}
}
{
"command": "completionInfo",
"type": "response",
"success": true,
"interop": {
"fragments": [
"m"
],
"isGlobal": false,
"prevLocation": [
4,
1
],
"nextLocation": [
7,
1
],
"sym": "Midi",
"usage": "method"
},
"data": {
"isGlobalCompletion": false,
"isMemberCompletion": true,
"isNewIdentifierLocation": false,
"entries": [
{
"name": "freqToMidi",
"kind": "property",
"kindModifiers": "declare",
"sortText": "0"
},
{
"name": "isMidi",
"kind": "property",
"kindModifiers": "declare",
"sortText": "0"
},
{
"name": "midiToFreq",
"kind": "property",
"kindModifiers": "declare",
"sortText": "0"
},
{
"name": "midiToNoteName",
"kind": "property",
"kindModifiers": "declare",
"sortText": "0"
},
{
"name": "toMidi",
"kind": "property",
"kindModifiers": "declare",
"sortText": "0"
}
]
},
"requestId": "12345"
}
{
"command": "quickinfo",
"type": "request",
"requestId": "12345",
"arguments": {
"file": "/path/to/core.cljs",
"projectRoot": "/path/to/root",
"line": 7,
"offset": 10
}
}
{
"command": "quickinfo",
"type": "response",
"success": true,
"data": {
"kind": "property",
"kindModifiers": "declare",
"displayString": "(property) midiToFreq: (midi: number, tuning?: number) => number",
"documentation": "",
"tags": []
},
"interop": {
"fragments": [
"midiToFreq"
],
"sym": "Midi",
"isGlobal": false,
"usage": "method",
"prevLocation": [
7,
1
],
"nextLocation": [
7,
18
]
},
"requestId": "12345"
}
{
"command": "definition",
"type": "request",
"requestId": "12345",
"arguments": {
"file": "/path/to/core/core.cljs",
"projectRoot": "/path/to/project/root",
"line": 7,
"offset": 10
}
}
{
"command": "definition",
"data": {
"contextEnd": {
"line": 69,
"offset": 35
},
"contextStart": {
"line": 69,
"offset": 5
},
"end": {
"line": 69,
"offset": 15
},
"file": "/path/to/tonal/node_modules/@tonaljs/midi/dist/index.d.ts",
"start": {
"line": 69,
"offset": 5
}
},
"interop": {
"fragments": [
"midiToFreq"
],
"isGlobal": false,
"nextLocation": [
7,
18
],
"prevLocation": [
7,
1
],
"sym": "Midi",
"usage": "method"
},
"requestId": "12345",
"success": true,
"type": "response"
}