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 v1 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$v1" :as calva]))
;; OR
(require '["ext://betterthantomorrow.calva$v1" :as calva])
(calva/repl.currentSessionKey) => "cljs" ; or "clj", depending
(def calvaExt (vscode/extensions.getExtension "betterthantomorrow.calva"))
(def calva (-> calvaExt
.-exports
.-v1
(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.v1;
const sessionKey = calva.repl.currentSessionKey()
repl#
The repl module 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.listSessions()#
Use repl.listSessions() to inspect every registered Calva REPL session, including secondary or custom session roles. It returns a collection/array of metadata objects with the following shape:
replSessionKey(string, required): The session key you can pass to other Calva APIs such asevaluateCode.projectRoot(string, optional): A URI string describing the project/workspace that owns the session.lastActivity(number, optional): Milliseconds since Unix epoch for the latest known activity on the session.globs(string[], optional): The set of file globs that the session declared it can handle. Calva iterates sessions in connection order and picks the first one whose globs match the active file.
(def sessions (calva/repl.listSessions))
(println "Session keys:" (map :replSessionKey sessions))
(def list-sessions (get-in [:repl :listSessions] calvaApi))
(def session-keys (map :replSessionKey (list-sessions)))
const sessions = calva.repl.listSessions();
const secondary = sessions.find((s) => s.replSessionKey === 'cljs');
repl.evaluate()#
The primary evaluation function. When multiple agents or tools evaluate code through the API, evaluate() lets each identify itself so that REPL output shows who triggered each evaluation. It also tracks which other callers have evaluated since each caller's last evaluation.
export async function evaluate(
code: string,
options?: {
sessionKey?: 'clj' | 'cljs' | 'cljc' | string;
ns?: string;
output?: {
stdout: (m: string) => void;
stderr: (m: string) => void;
};
nReplOptions?: Record<string, unknown>;
who?: string;
description?: string;
}
): Promise<Result>;
Where Result is:
type Result = {
result: string;
ns: string;
output: string;
errorOutput: string;
sessionKey: string; // Actual session key used
who?: string; // Resolved who identifier
otherWhosSinceLast?: string[]; // Other who values that evaluated since this who's last evaluation
error?: string; // Error message, if any
stacktrace?: any; // Raw nrepl stacktrace object, if error
};
Options#
sessionKey— Which REPL session to use. Same as the first argument toevaluateCode(). Defaults to the current routed session.ns— The namespace to evaluate in. Defaults to"user".output— Optional stdout/stderr handlers, same asevaluateCode().nReplOptions— Additional nREPL evaluation options.who— A freeform string identifying who is evaluating. Defaults to"api". This appears as a badge in Calva's REPL output, helping users distinguish between different agents or tools.description— An optional description that is output before the evaluated code, providing context about why the evaluation is happening.
Reserved who values
The values "ui" and "api" are reserved for Calva's internal use. Passing either explicitly will throw an Error. Omitting who (which defaults to "api") is fine — only explicit use is rejected.
Who Attribution#
The who field identifies which API caller triggered an evaluation. It is included in:
- The
Resultobject returned by the promise (always the resolved value) OutputMessageobjects delivered toonOutputLogged()subscribers- A badge in the REPL output, shown before the session type and namespace
; my-agent clj user
(+ 1 2)
3
When who is omitted or falsy, it defaults to "api". UI-triggered evaluations (from the editor) use "ui" internally, and the badge is ommitted.
otherWhosSinceLast Tracking#
The otherWhosSinceLast field on the Result tells you which other who values have evaluated on the same session since this caller's previous evaluation. This is useful for detecting interleaved evaluations:
const result = await calva.repl.evaluate("(+ 1 2)", { who: "my-agent" });
console.log(result.otherWhosSinceLast); // e.g. ["ui", "other-agent"]
Tracking is per-session and resets when the REPL disconnects.
Examples#
(-> (p/let [result (calva/repl.evaluate "(+ 2 40)"
#js {:who "my-script"
:ns "user"})]
(println (.-result result))
(println (.-otherWhosSinceLast result)))
(p/catch (fn [e]
(println "Evaluation error:" e))))
try {
const result = await calva.repl.evaluate("(+ 2 40)", {
who: "my-agent",
sessionKey: "clj",
description: "Testing addition",
});
console.log(result.result);
console.log(result.who); // "my-agent"
console.log(result.otherWhosSinceLast); // [] or ["ui", ...]
} catch (e) {
console.error("Evaluation error:", e);
}
Handling Output#
The output member on the Result object will have any output produced during evaluation. 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().
An example:
(def oc (joyride.core/output-channel)) ;; Assuming Joyride is used
(def evaluate (fn [code]
(calva/repl.evaluateCode
"clj"
code
"user"
#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
"user"
#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, "user", {
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);
}
repl.onOutputLogged()#
Subscribe to Calva REPL output messages. Returns a vscode.Disposable that you should dispose when you no longer need updates. (For fire-and-forget convenience, push it onto your extension’s context.subscriptions).
The signature in TypeScript:
export function onOutputLogged(
callback: (msg: OutputMessage) => void
): vscode.Disposable;
export type OutputCategory =
| 'evaluationResults'
| 'evaluatedCode'
| 'evaluationOutput'
| 'evaluationErrorOutput'
| 'otherOutput'
| 'otherErrorOutput';
export interface OutputMessage {
category: OutputCategory;
text: string;
who?: string; // Present when the output was triggered by an identified who
ns?: string; // The namespace the output is associated with, when applicable
replSessionKey?: string; // The REPL session key (e.g. "clj", "cljs"), when applicable
}
repl.evaluateCode() (deprecated)#
Deprecated
evaluateCode() is deprecated. Use evaluate() instead.
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();
editor#
The editor module has facilites (well, a facility, so far) for editing Clojure documents.
editor.replace()#
With editor.replace() you can replace a range in a Clojure editor with new text. The arguments are:
editor, avscode.TextEditorrange, avscode.RangenewText, a string
(-> (p/let [top-level-form-range (first (calva/ranges.currentTopLevelForm))
_ (calva/editor.replace vscode/window.activeTextEditor top-level-form-range "Some new text")]
(println "Text replaced!"))
(p/catch (fn [e]
(println "Error replacing text:" e))))
const topLevelRange = calvaApi.ranges.currentTopLevelForm();
calva.editor.replace(topLevelRange, "Some new text")
.then((_) => console.log("Text replaced!"))
.catch((e) => console.log("Error replacing text:", e));
document#
The document module provides access to the Clojure/Calva aspects of VS Code TextDocuments.
document.getNamespace(document?: vscode.TextDocument): string#
document.getNamespace() returns the namespace of a document.
document, avscode.TextDocument(defaults to the current active document)
Example usage. To evaluate some code in the namespace of the current document:
(calva/repl.evaluateCode "clj" "(+ 1 2 39)" (calva/document.getNamespace))
calva.repl.evaluateCode("clj", "(+ 1 2 39)", calva.document.getNamespace());
document.getNamespaceAndNsForm(document?: vscode.TextDocument): [ns: string, nsForm: string]#
document.getNamespaceAndNsForm() returns the namespace and the ns form of a document as a tuple.
document, avscode.TextDocument(defaults to the current active document)
Example usage. To evaluate the ns form of the current document:
(calva/repl.evaluateCode "clj" (second (calva/document.getNamespaceAndNsForm)))
calva.repl.evaluateCode("clj", calva.document.getNamespaceAndNsForm()[1]);
pprint#
The pprint module lets you pretty print Clojure code/data using Calva's pretty printing engine (which in turn uses zprint).
pprint.prettyPrint()#
Use pprint.prettyPrint() to pretty print some Clojure data using your Calva pretty printing options. It accepts these arguments:
text, astringwith the text to pretty printoptions, a JavaScript object with pretty printing options, this is optional and will default to the current settings.
The function is synchronous and returns the prettified text.
(println (calva/pprint.prettyPrint "Some text")))
console.log(calvaApi.pprint.prettyPrint();
pprint.prettyPrintingOptions()#
Use to get the current pretty printint options:
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(...));
Deprecation candidate
VS Code is still creating a separate group, just with the same name as Calva's, so this API is not good for anything, and we will probably remove it.
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.)