Writing plugins
A plugin is one TypeScript file that exports an object. Every hook is optional, start with highlighting and add the rest when you need it.
What a plugin is
A plugin is a folder with two files: a plugin.json that says which files it handles, and a bundled JavaScript file with the code. jano loads it on start and calls its hooks. The plugin never sees jano's internals, only lines, cursors and edits.
A crashing plugin can't take jano down. Every hook runs through a wrapper that catches errors, and in debug mode the stack trace ends up in the log.
Project layout
my-plugin/
├── src/
│ └── index.ts # the plugin
├── plugin.json # manifest
├── package.json # build script
└── README.md # shown in the plugin store
The manifest
{
"name": "my-language",
"version": "1.0.0",
"api": 2,
"description": "My language support for jano",
"extensions": [".ext"],
"entry": "index.js",
"author": "your-name",
"license": "MIT"
}
name, version, description, extensions and entry are required. api is the plugin API version your code is written for. Use 2, which supports async hooks. jano refuses plugins with a newer api than it knows, with a message that says which version is needed.
A minimal plugin
import type { LanguagePlugin } from "@jano-editor/plugin-types";
const plugin: LanguagePlugin = {
name: "My Language",
extensions: [".ext"],
highlight: {
keywords: ["if", "else", "return", "function"],
patterns: {
comment: /\/\/.*$/gm,
string: /"(?:[^"\\]|\\.)*"|'[^']*'/g,
number: /\b\d+\.?\d*\b/g,
},
},
// keep the indent of the previous line on Enter
onCursorAction(ctx) {
if (ctx.action?.type !== "newline") return null;
const line = ctx.action.cursor.position.line;
const indent = ctx.lines[line - 1]?.match(/^\s*/)?.[0] ?? "";
if (!indent) return null;
return {
edits: [{ range: { start: { line, col: 0 }, end: { line, col: 0 } }, text: indent }],
cursors: [{ position: { line, col: indent.length }, anchor: null }],
};
},
};
export default plugin;
Bundle it into one file with esbuild:
esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js
To try it, copy dist/index.js and plugin.json into ~/.local/share/jano/plugins/my-language/ and open a matching file. jano-editor/plugin-python is a complete plugin to copy from.
Hooks
Every hook is optional. ctx is the plugin context with the file name, all lines, the cursors and the user's tabSize and insertSpaces settings.
| Hook | When it runs | Returns |
|---|---|---|
highlight | Not a function: keywords and regex patterns per token type | |
highlightLine(line, index, lines) | For every visible line. Replaces highlight when both exist | HighlightToken[] |
onKeyDown(key, ctx) | On every key press, before jano handles it | { handled, edit? } or null |
onCursorAction(ctx) | After every edit, once per cursor | EditResult or null |
onFormat(ctx) | When the user presses F3 | EditResult or null |
onSave(ctx) | Before the file is written. The edits are saved and can be undone | EditResult or null |
onOpen(ctx) | When a file is opened, and after a backup is restored | nothing |
onValidate(lines) | While typing, debounced | Diagnostic[] |
onComplete(ctx) | On Ctrl+Space and while typing | CompletionItem[] or null |
Async hooks
onFormat, onSave, onOpen, onValidate and onComplete may return a Promise. That is the place for anything slow, like running an external formatter. jano keeps going while you wait, and throws a late result away if the document or the cursor changed in the meantime. A hook that takes too long times out.
async onValidate(lines) {
const problems = await check(lines.join("\n"));
return problems.map((p) => ({ line: p.line, col: p.col, severity: "error", message: p.text }));
},
onKeyDown, onCursorAction and highlightLine stay synchronous. They run on every key press, a Promise there would make typing lag.
Positions
col, and start / end of highlight tokens, are string indices, like String.prototype.slice, not screen columns. A tab counts as one, an emoji as its UTF-16 length. jano converts them for drawing, so plugins never have to care about tab width or wide characters.
Tips
onKeyDowngets the full key (name,ctrl,alt,shift) and can stop jano's default withhandled: true. Good for snippets and auto-closing brackets.onCursorActiongetsdeletedTexton backspace and delete, so a plugin can remove a closing bracket together with the opening one.replaceAllin anEditResultreplaces the whole document, the easy way to implement a formatter.- Completion items can have a
kind(keyword,function,variable,property,type,constant,snippet), jano shows a matching icon. highlightLinegets all lines, so it can track state across lines, for block comments or multi-line strings. Cache what you compute, it runs on every render.
Publish
Push the plugin to a public GitHub repository, sign in on the plugin store with GitHub and publish it from there. The README.md becomes the plugin's page in the store.