Skip to content

The Calva Extension API

Calva exposes an API for use from other VS Code extensions (such as Joyride). The API is in an experimental state, while we are figuring out what is a good shape for this API. It is also rather small, and will grow to expose more of Calva's functionality.

Accessing

To access the API the Calva extension needs to be activated. The API is exposed under the v0 key on the extension's exports, and split up into submodules, like repl, and ranges.

When using Joyride you can use its unique require API, for which one of the benefits is better lookup IDE support. When using the API from regular ClojureScript, you'll pick it up from the Calva extension instance. (Which you can do from Joyride as well, but why would you?). Here is how you access the API, with an example of usage as a bonus:

(ns ... (:require ["ext://betterthantomorrow.calva$v0" :as calva]))
;; OR
(require '["ext://betterthantomorrow.calva$v0" :as calva])

(calva/repl.currentSessionKey) => "cljs" ; or "clj", depending
(def calvaExt (vscode/extensions.getExtension "betterthantomorrow.calva"))

(def calva (-> calvaExt
             .-exports
             .-v0
             (js->clj :keywordize-keys true)))

((get-in calva [:repl :currentSessionKey])) => "cljs" ; or "clj", depending
const calvaExt = vscode.extensions.getExtension("betterthantomorrow.calva");

const calva = calvaExt.exports.v0;

const sessionKey = calva.repl.currentSessionKey()

repl

The repl module contains provides access to Calva's REPL connection.

repl.currentSessionKey()

Use repl.currentSessionKey() find out which REPL/session Calva's REPL is currently connected to (depends on the active file). Returns either "clj", or "cljs", or nil if no REPL is connected.

(def session-key (calva/repl.currentSessionKey))
(def session-key ((get-in [:repl :currentSessionKey] calvaApi)))
const sessionKey = calva.repl.currentSessionKey()

repl.evaluateCode()

This function lets you evaluate Clojure code through Calva's nREPL connection. Calling it returns a promise that resolves to a Result object. It's signature looks like so (TypeScript):

export async function evaluateCode(
  sessionKey: 'clj' | 'cljs' | 'cljc' | undefined,
  code: string,
  output?: {
    stdout: (m: string) => void;
    stderr: (m: string) => void;
  }
): Promise<Result>;

Where Result is:

type Result = {
  result: string;
  ns: string;
  output: string;
  errorOutput: string;
};

As you can see, the required arguments to the function are sessionKey and code. sessionKey should be "clj", "cljs", "cljc", or undefined depending on which of Calva's REPL sessions/connections that should be used. It will depend on your project, and how you connect to it, which session keys are valid. Use cljc to request whatever REPL session "cljc" files are connected to. Use undefined to use the current REPL connection Calva would use (depends on which file is active).

An example:

(-> (p/let [evaluation (calva/repl.evaluateCode "clj" "(+ 2 40)")]
      (println (.-result evaluation)))
    (p/catch (fn [e]
               (println "Evaluation error:" e))))
(def evaluate (get-in [:repl :evaluateCode] calvaApi))
(-> (p/let [evaluation (evaluate "clj" "(+ 2 40)")]
      (println (.-result evaluation)))
    (p/catch (fn [e]
               (println "Evaluation error:" e))))
try {
  const evaluation = await calvaApi.repl.evaluateCode("clj", "(+ 2 40)");
  console.log(evaluation.result);
} catch (e) {
  console.error("Evaluation error:", e);
}

Handling Output

The output member on the Result object will have any output produced during evaluation. (The errorOutput member should contain error output produced, but currently some Calva bug makes this not work.) By default the stdout and stderr output is not printed anywhere.

If you want to do something with either regular output or error output during, or after, evaluation, you'll need to provide the output argument to evaluateCode(). (The stderr callback function works, so this is the only way to get at any error output, until the above mentioned Calva bug is fixed.)

An example:

(def oc (joyride.core/output-channel)) ;; Assuming Joyride is used
(def evaluate (fn [code]
                (calva/repl.evaluateCode
                 "clj"
                 code
                 #js {:stdout #(.append oc %)
                      :stderr #(.append oc (str "Error: " %))})))

(-> (p/let [evaluation (evaluate "(println :foo) (+ 2 40)")]
      (.appendLine oc (str "=> " (.-result evaluation))))
    (p/catch (fn [e]
               (.appendLine oc (str "Evaluation error: " e)))))
(def oc (joyride.core/output-channel)) ;; Assuming Joyride is used
(def evaluate (fn [code]
                ((get-in [:repl :evaluateCode] calvaApi)
                 "clj"
                 code
                 #js {:stdout #(.append oc %)
                      :stderr #(.append oc (str "Error: " %))})))

(-> (p/let [evaluation (evaluate "(println :foo) (+ 2 40)")]
      (.appendLine oc (str "=> " (.-result evaluation))))
    (p/catch (fn [e]
               (.appendLine oc (str "Evaluation error: " e)))))
const evaluate = (code) =>
  calvaApi.repl.evaluateCode("clj", code, {
    stdout: (s) => {
      console.log(s);
    },
    stderr: (s) => {
      console.error(s);
    },
  });

try {
  const evaluation = await evaluate("(println :foo) (+ 2 40)");
  console.log("=>", evaluation.result);
} catch (e) {
  console.error("Evaluation error:", e);
}

ranges

The ranges module contains functions for retreiving vscode.Ranges and text for pieces of interest in a Clojure document.

All functions in this module have the following TypeScript signature:

(editor = vscode.window.activeTextEditor, position = editor?.selection?.active) => [vscode.Range, string];

I.e. they expect a vscode.TextEditor – defaulting to the currently active editor – and a vscode.Position – defaulting to the current active position in the editor (or the first active position if multiple selections/positions exist, and will return a tuple with the range, and the text for the piece of interest requested.

Custom REPL Commands

The ranges function have corresponding REPL Snippets/Commands substitution variables. It is the same implementation functions used in both cases.

The functions available are:

ranges.currentForm()

Retrieves information about the current form, as determined from the editor and position.

Corresponding REPL Snippet variable: $current-form.

See also about Calva's Current Form on YouTube.

ranges.currentEnclosingForm()

The list/vector/etcetera form comtaining the current form.

Corresponding REPL Snippet variable: $enclosing-form.

ranges.currentTopLevelForm()

The current top level form. Outside (comment ...) (Rich comments) forms this is most often ((def ...), (defgn ...), etcetera. Inside Rich comments it will be the current immediate child to the (comment ...) form.

Corresponding REPL Snippet variable: $top-level-form.

ranges.currentFunction()

The current function, i.e. the form in ”call position” of the closest enclosing list.

Corresponding REPL Snippet variable: $current-fn.

ranges.currentTopLevelDef()

The symbol being defined by the current top level form. NB: Will stupidly assume it is the second form. I.e. it does not check that it is an actual definition, and will often return nonsense if used in Rich comments.

Corresponding REPL Snippet variable: $top-level-defined-symbol.

Example: ranges.currentTopLevelForm()

(let [[range text] (calva/ranges.currentTopLevelForm)]
  ...)
(let [[range text] ((get-in [:ranges :currentTopLevelForm]))]
  ...)
const [range, text] = ranges.currentTopLevelForm();

vscode

In the its vscode submodule, Calva exposes access to things from its own vscode module instance. It gets important in some situations.

vscode.registerDocumentSymbolProvider()

This is the [vscode.languages](https://code.visualstudio.com/api/references/vscode-api#languages).registerDocumentSymbolProvider() function from the Calva extension. Use it if you want to provide symbols for Clojure files together with the ones that Calva provides. (If you use the vscode.languages.registerDocumentSymbolProvider() function from your extension (or Joyride) you will provide a separate group.)

(-> (joyride/extension-context)
  .-subscriptions
  (.push (calva/vscode.registerDocumentSymbolProvider ...)))
(-> yourExtensionContext
  .-subscriptions
  (.push ((get-in calva [:vscode :registerDocumentSymbolProvider]) ...)))
yourExtensionContext.subscriptions.push(calva.vscode.registerDocumentSymbolProvider(...));

Feedback Welcome

Please let us know how you fare using this API. Either in the #calva or #joyride channels on Slack or via the issues/discussions sections on the repositories. (Whichever seems to apply best.)