jano

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.

HookWhen it runsReturns
highlightNot a function: keywords and regex patterns per token type
highlightLine(line, index, lines)For every visible line. Replaces highlight when both existHighlightToken[]
onKeyDown(key, ctx)On every key press, before jano handles it{ handled, edit? } or null
onCursorAction(ctx)After every edit, once per cursorEditResult or null
onFormat(ctx)When the user presses F3EditResult or null
onSave(ctx)Before the file is written. The edits are saved and can be undoneEditResult or null
onOpen(ctx)When a file is opened, and after a backup is restorednothing
onValidate(lines)While typing, debouncedDiagnostic[]
onComplete(ctx)On Ctrl+Space and while typingCompletionItem[] 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

  • onKeyDown gets the full key (name, ctrl, alt, shift) and can stop jano's default with handled: true. Good for snippets and auto-closing brackets.
  • onCursorAction gets deletedText on backspace and delete, so a plugin can remove a closing bracket together with the opening one.
  • replaceAll in an EditResult replaces 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.
  • highlightLine gets 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.