Skip to content

wavejumper/boonmee

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clojure CI Release Clojars Project

boonmee

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.

Why

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.

Features

Editor functionality:

  • Quickinfo (@jsdoc documentation, type signature, fn metadata etc)
  • Code completions (require, fn calls)
  • Code navigation (jump to definition)

Linting (WIP):

  • Warn on deprecated methods (via @jsdoc convention)
  • Warn on undefined es6 method call
  • Incorrect arity on es6 method call
  • Basic type-checking

Installation

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.

Dependencies

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

Usage

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)

Editor integration

Emacs (WIP)

emacs plugin

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)))

ClojureScript

NPM deps

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.

@types

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.

Globals

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

Protocol

Specs for the boonmee protocol can be found in the boonmee.protocol namespace.

Example RPC

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

Completions

Request

{
  "command": "completions",
  "type": "request",
  "requestId": "12345",
  "arguments": {
    "projectRoot": "/path/to/project/root",
    "file": "/path/to/core.cljs",
    "line": 4,
    "offset": 7
  }
}

Response

{
  "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"
}

Quickinfo

Request

{
  "command": "quickinfo",
  "type": "request",
  "requestId": "12345",
  "arguments": {
    "file": "/path/to/core.cljs",
    "projectRoot": "/path/to/root",
    "line": 7,
    "offset": 10
  }
}

Response

{
  "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"
}

Definitions

Request

{
  "command": "definition",
  "type": "request",
  "requestId": "12345",
  "arguments": {
    "file": "/path/to/core/core.cljs",
    "projectRoot": "/path/to/project/root",
    "line": 7,
    "offset": 10
  }
}

Response

{
  "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"
}