jano

Plugins schreiben

Ein Plugin ist eine TypeScript-Datei, die ein Objekt exportiert. Jeder Hook ist optional, fang mit dem Highlighting an und nimm den Rest dazu, wenn du ihn brauchst.

Was ein Plugin ist

Ein Plugin ist ein Ordner mit zwei Dateien: eine plugin.json, die sagt, welche Dateien es behandelt, und eine gebündelte JavaScript-Datei mit dem Code. jano lädt es beim Start und ruft seine Hooks auf. Das Plugin sieht nie jano's Innenleben, nur Zeilen, Cursor und Änderungen.

Ein abstürzendes Plugin kann jano nicht mitreißen. Jeder Hook läuft durch eine Hülle, die Fehler abfängt, und im Debug-Modus landet der Stacktrace im Log.

Aufbau

my-plugin/
├── src/
│   └── index.ts      # das Plugin
├── plugin.json       # Manifest
├── package.json      # Build-Skript
└── README.md         # wird im Plugin Store angezeigt

Das 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 und entry sind Pflicht. api ist die Version der Plugin-API, für die dein Code geschrieben ist. Nimm 2, damit gehen asynchrone Hooks. Ist api neuer, als jano kennt, lehnt jano das Plugin ab und sagt dazu, welche Version nötig ist.

Ein minimales 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;

Mit esbuild zu einer Datei bündeln:

esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js

Zum Ausprobieren dist/index.js und plugin.json nach ~/.local/share/jano/plugins/my-language/ kopieren und eine passende Datei öffnen. jano-editor/plugin-python ist ein vollständiges Plugin zum Abschauen.

Hooks

Jeder Hook ist optional. ctx ist der Plugin-Kontext mit Dateiname, allen Zeilen, den Cursorn und den Einstellungen tabSize und insertSpaces.

HookWann er läuftRückgabe
highlightKeine Funktion: Keywords und Regex-Muster pro Token-Typ
highlightLine(line, index, lines)Für jede sichtbare Zeile. Ersetzt highlight, wenn beides da istHighlightToken[]
onKeyDown(key, ctx)Bei jedem Tastendruck, bevor jano ihn verarbeitet{ handled, edit? } oder null
onCursorAction(ctx)Nach jeder Änderung, einmal pro CursorEditResult oder null
onFormat(ctx)Wenn F3 gedrückt wirdEditResult oder null
onSave(ctx)Bevor die Datei geschrieben wird. Die Änderungen werden mitgespeichert und sind rückgängig machbarEditResult oder null
onOpen(ctx)Beim Öffnen einer Datei und nach dem Wiederherstellen eines Backupsnichts
onValidate(lines)Beim Tippen, verzögertDiagnostic[]
onComplete(ctx)Bei Ctrl+Space und beim TippenCompletionItem[] oder null

Asynchrone Hooks

onFormat, onSave, onOpen, onValidate und onComplete dürfen ein Promise zurückgeben. Das ist der Platz für alles Langsame, etwa einen externen Formatter. jano bleibt währenddessen bedienbar und verwirft ein spätes Ergebnis, wenn sich Dokument oder Cursor inzwischen geändert haben. Ein Hook, der zu lange braucht, läuft in einen Timeout.

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 und highlightLine bleiben synchron. Sie laufen bei jedem Tastendruck, ein Promise dort würde das Tippen verzögern.

Positionen

col sowie start / end von Highlight-Tokens sind String-Indizes wie bei String.prototype.slice, keine Bildschirmspalten. Ein Tab zählt als eins, ein Emoji mit seiner UTF-16-Länge. jano rechnet sie fürs Zeichnen um, Plugins müssen sich also nie um Tab-Breite oder breite Zeichen kümmern.

Tipps

  • onKeyDown bekommt die vollständige Taste (name, ctrl, alt, shift) und kann jano's Standardverhalten mit handled: true verhindern. Gut für Snippets und automatisch schließende Klammern.
  • onCursorAction bekommt bei Backspace und Entf den deletedText, so kann ein Plugin die schließende Klammer zusammen mit der öffnenden entfernen.
  • replaceAll in einem EditResult ersetzt das ganze Dokument, der einfachste Weg für einen Formatter.
  • Vorschläge können ein kind haben (keyword, function, variable, property, type, constant, snippet), jano zeigt dazu ein passendes Symbol.
  • highlightLine bekommt alle Zeilen und kann so Zustand über Zeilen hinweg verfolgen, für Blockkommentare oder mehrzeilige Strings. Zwischenspeichern lohnt sich, die Funktion läuft bei jedem Neuzeichnen.

Veröffentlichen

Das Plugin in ein öffentliches GitHub-Repository pushen, im Plugin Store mit GitHub anmelden und dort veröffentlichen. Die README.md wird zur Seite des Plugins im Store.