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.
| Hook | Wann er läuft | Rückgabe |
|---|---|---|
highlight | Keine Funktion: Keywords und Regex-Muster pro Token-Typ | |
highlightLine(line, index, lines) | Für jede sichtbare Zeile. Ersetzt highlight, wenn beides da ist | HighlightToken[] |
onKeyDown(key, ctx) | Bei jedem Tastendruck, bevor jano ihn verarbeitet | { handled, edit? } oder null |
onCursorAction(ctx) | Nach jeder Änderung, einmal pro Cursor | EditResult oder null |
onFormat(ctx) | Wenn F3 gedrückt wird | EditResult oder null |
onSave(ctx) | Bevor die Datei geschrieben wird. Die Änderungen werden mitgespeichert und sind rückgängig machbar | EditResult oder null |
onOpen(ctx) | Beim Öffnen einer Datei und nach dem Wiederherstellen eines Backups | nichts |
onValidate(lines) | Beim Tippen, verzögert | Diagnostic[] |
onComplete(ctx) | Bei Ctrl+Space und beim Tippen | CompletionItem[] 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
onKeyDownbekommt die vollständige Taste (name,ctrl,alt,shift) und kann jano's Standardverhalten mithandled: trueverhindern. Gut für Snippets und automatisch schließende Klammern.onCursorActionbekommt bei Backspace und Entf dendeletedText, so kann ein Plugin die schließende Klammer zusammen mit der öffnenden entfernen.replaceAllin einemEditResultersetzt das ganze Dokument, der einfachste Weg für einen Formatter.- Vorschläge können ein
kindhaben (keyword,function,variable,property,type,constant,snippet), jano zeigt dazu ein passendes Symbol. highlightLinebekommt 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.