Zum Inhalt

Plugin-API — Überblick

Alle PluginApi-Typen liegen in namespace MTE::plugin. Die öffentlichen Header dürfen keinen Qt-, Scintilla- oder MTE-internen Header einbinden — nur die Standardbibliothek. Die eine bewusste Ausnahme ist der Opt-in-Ordner PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) für Plugins, die eigene Qt-Widgets mitbringen.

Derselbe Vertrag wird von zwei Backends erfüllt: dem nativen Qt-Loader (Shared Libraries) und dem Out-of-Process-Python-Backend (Python-Plugins). Dieser Abschnitt dokumentiert die C++-Oberfläche; jeder hier aufgeführte Backend-neutrale Dienst hat ein Python-Gegenstück auf ctx.

Plugin-Lebenszyklus

Ein natives Plugin implementiert eine Klasse:

class IPlugin {
public:
    virtual PluginInfo info() const = 0;                    // id, name, version, apiVersion
    virtual bool initialize(IPluginContext& ctx) = 0;       // register everything here
    virtual void shutdown() noexcept = 0;                   // release every token
};
  • info() muss dieselbe id und apiVersion zurückgeben wie plugin.json; der Host gleicht beide ab und verweigert bei Abweichung das Laden.
  • initialize() läuft einmal beim Start (nachdem alle Plugins erkannt wurden). Registrieren Sie hier Commands, Menüs, Provider und Panels; geben Sie false nur bei einem echten Fehler zurück (der Host entlädt das Plugin und protokolliert es).
  • shutdown() läuft beim Beenden (oder wenn das Plugin entladen wird). Es darf nicht werfen; rufen Sie reset() auf jedem Token auf und setzen Sie Ihre gespeicherten Dienstzeiger auf null.

RAII-Tokens

Jede Registrierung liefert ein Token zurück (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription, …) — ein Move-only BasicToken, das die Registrierung besitzt: Wird es zerstört oder mit reset() zurückgesetzt, wird das Command / der Menüeintrag / das Panel / das Abonnement widerrufen. Halten Sie jedes Token als Member und geben Sie es in shutdown() frei. Zwei Accessoren sind wichtig:

  • id() — nicht-destruktive TokenId-Sicht, für APIs, die das Element adressieren, ohne es zu übernehmen (z. B. IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — löst die Bindung und liefert die Id zurück; das Token widerruft danach nichts mehr. Selten das, was Sie wollen.

Der Service-Locator

initialize() erhält einen IPluginContext, einen Service-Locator im Besitz des Hosts. Speichern Sie den Kontext nicht über initialize() hinaus; speichern Sie die einzelnen Dienstreferenzen, die Sie benötigen (jeder Dienst überlebt jedes Plugin).

Accessor Schnittstelle Verwendung für
ctx.editor() IEditorService Puffer lesen/bearbeiten, Auswahl, Caret, Dateien, Suche, Lesezeichen, Einrückung, Split-View-Gruppen
ctx.commands() ICommandRegistry Benutzeraktionen registrieren / aufrufen (mit Shortcuts)
ctx.menus() IMenuRegistry Menüeinträge, Trennlinien, ankreuzbare Umschalter, Einträge ausgrauen
ctx.docks() IDockRegistry andockbare Seitenpanels
ctx.documents() IDocumentTypeRegistry vollwertige MainWindow-Tab-Typen (Diff, Log-Ansicht, …)
ctx.compare() ICompareService die zweigeteilte Vergleichsansicht öffnen
ctx.diagnostics() IDiagnosticsSink Unterschlängelungen + Randsymbole veröffentlichen
ctx.events() IEventBus Editor-Events abonnieren / veröffentlichen
ctx.completions() ICompletionRegistry Autovervollständigungs-Kandidaten beisteuern
ctx.contextMenu() IContextMenuRegistry dynamische Einträge im Rechtsklick-Menü des Editors
ctx.snippetSessions() ISnippetSessionService per Tab expandierbare Snippets bereitstellen (der Host führt die Feldsession aus)
ctx.hover() IHoverRegistry Hover-Tooltips
ctx.signatureHelp() ISignatureHelpRegistry Call-Tips bei ( / ,
ctx.symbols() ISymbolRegistry Dokument- / Workspace-Symbole (Go-to-Symbol)
ctx.languages() ILanguageRegistry Sprachen auflisten, die Sprache eines Dokuments lesen / umschalten, eine Sprachdefinition beisteuern
ctx.statusBar() IStatusBar flüchtige Meldungen + persistente Segmente
ctx.settings() ISettings persistente Schlüssel/Wert-Konfiguration pro Plugin
ctx.appSettings() const ISettings& app-weite Einstellungen lesen (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings .editorconfig-geschichtete Konfiguration pro Dokument
ctx.settingsRegistry() ISettingsRegistry eine Seite zum Einstellungen-Dialog hinzufügen
ctx.themePalette() IThemePalette benannte Theme-Farben, Theme-Editor-Integration
ctx.lineMarkers() ILineMarkerService Randsymbole und Zeilentönungen, die Bearbeitungen folgen (Haltepunkte, Änderungsstreifen); der Anspruch auf den Randklick
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path Ihr privates beschreibbares Verzeichnis
ctx.diagnosticsDir() std::filesystem::path gemeinsames Verzeichnis für Crash-Artefakte + Logs (für Plugins nur lesend)

Die API später zu erweitern bedeutet, eine neue Dienstschnittstelle samt Accessor hinzuzufügen — eine kompatible Ergänzung nach der Versionierungsregel: Bestehende Plugins werden weiterhin geladen. Welche Toolchain der Plugin-Build selbst treffen muss (Compiler, Laufzeit, Qt-Minor-Version), steht unter Ein Plugin schreiben, Abschnitt „Build-Voraussetzungen".

IEditorService — das Dokumentmodell

Der Editor aus Sicht der Plugins — kein Scintilla, kein Qt. Dokumente werden über eine opake DocumentId referenziert (std::uint64_t, 0 = ungültig). Offsets sind Byte-Offsets in UTF-8-Text; MatchRange ist halboffen {start, end}, wobei noMatch() „nicht gefunden“ bedeutet; EditorPosition ist 0-basiert {line, column}.

// Documents
DocumentId               activeDocument() const;
std::vector<DocumentId>  openDocuments() const;
void                     setActiveDocument(DocumentId);
std::optional<std::filesystem::path> filePath(DocumentId) const;
DocumentId               openFile(std::filesystem::path);
DocumentId               openFileAt(std::filesystem::path, EditorPosition); // jump there
void                     revealLine(DocumentId, std::int64_t line);        // show, don't move
bool                     saveDocument(DocumentId);
bool                     closeDocument(DocumentId);  // prompts if modified
bool                     isModified(DocumentId) const;
DocumentEncoding         documentEncoding(DocumentId) const;
void                     setDocumentEncoding(DocumentId, DocumentEncoding);
LineEnding               lineEnding(DocumentId) const;
void                     setLineEnding(DocumentId, LineEnding);

// Text, selection, caret
std::string   text(DocumentId) const;              // whole buffer
std::string   textRange(DocumentId, start, end) const;
void          setText(DocumentId, std::string_view);
std::int64_t  length(DocumentId) const;
std::string   selectedText(DocumentId) const;
void          replaceSelection(DocumentId, std::string_view);
MatchRange    selectionRange(DocumentId) const;
void          setSelection(DocumentId, MatchRange);
EditorPosition caret(DocumentId) const;
void          setCaret(DocumentId, EditorPosition);

// Search & replace (SearchOptions: case, whole-word, regex, wrap)
MatchRange    findInRange(DocumentId, needle, range, SearchOptions) const;
void          replaceTarget(DocumentId, MatchRange, std::string_view);

// Bookmarks, marks, URL highlighting
void toggleBookmark(DocumentId, line);   std::vector<std::int64_t> bookmarkedLines(DocumentId) const;
void markRange(DocumentId, MatchRange);  void clearMarks(DocumentId);

// Language & indentation
std::string      colorizerId(DocumentId) const;      // "cpp", "" if none
IndentationStyle indentation(DocumentId) const;      // {useTabs, width}
void             setIndentation(DocumentId, bool useTabs, int width);
void             setWrapIndent(DocumentId, WrapIndentMode); // where wrapped
WrapIndentMode   wrapIndent(DocumentId) const;             // continuation rows
                 // start: LeftMargin / SameAsLine / Indented

// Semantic classification (from the active colorizer's style rules;
// cheap -- no re-lex). CategorySpan = {start, length, SyntaxCategory}.
// The spell checker keeps to Comment/Documentation/String spans this way.
std::vector<CategorySpan> textCategories(DocumentId, start, end) const;

// Clipboard
std::string clipboardText() const;

Zusätzlich stellt es das Tab-Gruppen-Layout der geteilten Ansicht in Backend-neutralen Begriffen bereit (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), sodass ein Plugin Fensterbereichs-Anordnungen erfassen und wiederherstellen kann, ohne Qt-Splitter zu sehen. Die vollständige Liste finden Sie in PluginApi/IEditorService.h — der Header ist die maßgebliche Referenz.

Ein in zwei Bereichen gezeigtes Dokument ist ein Dokument mit einer DocumentId, sodass groupOf() nur einen seiner Plätze nennen kann. Zwei Methoden beschreiben und rekonstruieren eine Teilung:

std::vector<int> groupsOf(DocumentId doc) const;      // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group);  // put it in one more

showDocumentInGroup() aktiviert eine bereits in diesem Bereich vorhandene Ansicht und legt andernfalls dort eine zweite, geteilte Ansicht an — genau das, was Ansicht ▸ Dokument rechts teilen tut, sodass kein DocumentOpened ausgelöst wird und beide Ansichten ein Dokument bearbeiten. Bereiche sind das plugin-seitige Konzept, einzelne Ansichten nicht; darum benennt der Aufruf einen Bereich, statt ein Ansichts-Handle zurückzugeben.

Den Benutzer an eine Stelle bringen

openFileAt(path, position) ist der eine Aufruf hinter „zum Build-Fehler gehen“, „zum Stack-Frame springen“ oder „Suchtreffer öffnen“, sodass alle gleich landen. Er öffnet die Datei genau wie openFile() (der Tab einer bereits offenen Datei wird aktiviert, nicht ein zweites Mal geöffnet), macht sie zum aktiven Dokument, setzt die Einfügemarke ohne Auswahl, klappt eine Faltung auf, die die Zeile verbirgt, scrollt die Zeile in den sichtbaren Bereich — zentriert, wenn sie außerhalb des Bildschirms lag, ohne zu scrollen, wenn sie schon sichtbar war — und gibt dem Editor den Tastaturfokus. Die Position wird begrenzt, nie abgelehnt: Eine Zeile hinter dem Ende landet auf der letzten Zeile, eine Spalte hinter dem Zeilenende an deren Ende, negative Werte zählen als 0. Er liefert das Dokument, in das die Einfügemarke gesetzt wurde, oder 0, wenn die Datei nicht geöffnet werden konnte — und dann hat sich nichts bewegt.

revealLine(doc, line) zeigt eine Stelle, ohne den Benutzer dorthin zu bringen: Es klappt auf und scrollt den Bereich, der doc zeigt, bewegt aber weder Einfügemarke noch Auswahl, wechselt keine Tabs und nimmt keinen Fokus — ein Panel, das einen Stack-Frame in der Vorschau zeigt oder seiner Auswahl folgt, verwendet es.

Dokumentkodierung

Die Zeichenkodierung eines Dokuments auf der Festplatte ist Kern-Zustand, kein Plugin-Zustand. Der Host erkennt sie beim Lesen der Datei und schreibt bei jedem Speichern dieselbe Kodierung zurück, sodass eine UTF-8-BOM-Datei ihre Markierung behält und eine UTF-16-Datei UTF-16 bleibt. DocumentEncoding (PluginApi/DocumentEncoding.h) ist ein backend-neutraler POD-Enum, der die fünf Unicode/ANSI-Grundkodierungen sowie die älteren Codepages abdeckt.

setDocumentEncoding() hält eine Entscheidung fest: Es ändert keine Bytes und dekodiert nichts neu — der Puffer ist bereits Unicode, und die neue Kodierung wirkt beim nächsten Speichern. Ein Plugin mit einer „Konvertieren in“-Aktion sollte daher zusätzlich setModified(doc, true) aufrufen; eines mit „Interpretieren als“ lädt den Text zuerst selbst neu und hält dann die Wahl fest. Der Host veröffentlicht DocumentEncodingChanged, sobald sich der Wert tatsächlich ändert. Ein charset in der .editorconfig des Projekts hat beim Speichern weiterhin Vorrang.

Zeilenenden eines Dokuments

Auch das Zeilenende eines Dokuments ist Kernzustand. Der Host erkennt es beim Lesen der Datei, reicht es an die Ansicht weiter, damit Enter genau diese Sequenz einfügt, und schreibt die Bytes des Puffers wörtlich zurück — eine unter Windows bearbeitete Unix-Datei bleibt also LF, und ein Speichern ohne Änderungen ist byte-identisch. LineEnding (PluginApi/LineEnding.h) ist ein backend-neutrales POD-Enum — Unknown / Lf / CrLf / Cr —, das die Bytefolge benennt statt einer Backend-Konstante. Dazu kommen zwei Inline-Helfer: lineEndingBytes() und platformLineEnding() (womit ein neuer Puffer beginnt: CRLF unter Windows, LF sonst).

setLineEnding() hinterlegt die Wahl, stellt die Ansicht des Dokuments so um, dass das nächste Enter sie einfügt, und bestimmt, was das nächste Speichern schreibt. Es konvertiert keinen vorhandenen Text: ein Plugin, das den ganzen Puffer umschreibt (die Zeilenende-Konvertierungsbefehle), ersetzt den Text zuerst selbst, damit die Konvertierung eine einzige rückgängig machbare Bearbeitung bleibt. Der Host veröffentlicht DocumentLineEndingChanged, sobald sich der Wert wirklich ändert. Ein end_of_line aus der Projekt-.editorconfig gewinnt beim Speichern weiterhin und stellt das Dokument gleich mit auf diese Konvention um.

Threading

Alle Kontext- und Dienstaufrufe erfolgen auf dem UI-Thread des Hosts. Rufen Sie Dienste nicht aus einem selbst gestarteten Hintergrund-Thread auf; wechseln Sie zuerst zurück auf den UI-Thread.

Versionierung

Der Vertrag trägt eine zweiteilige Version, beide Teile in PluginApi/PluginInfo.h:

constexpr std::uint32_t MTE_PLUGIN_API_VERSION       = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor

Was zwischen ihnen entscheidet, ist, wer die Schnittstelle implementiert:

Änderung Art Version
Eine Methode, angehängt am Ende einer Schnittstelle, die nur der Editor implementiert und die Plugins nur aufrufen (IEditorService, IPluginContext, jede I…Registry, ILineMarkerService, …) kompatibel Minor + 1
Eine neue Schnittstelle, eine neue Event-Struktur, ein neuer Enumerator am Ende eines Enums kompatibel Minor + 1
Jede Änderung an einer Struktur, die die Grenze überquert (PluginInfo, ContextMenuRequest, DockPanelDescriptor, eine Event-Struktur) inkompatibel Major + 1
Eine geänderte Signatur, eine entfernte oder umsortierte Methode, eine neue Überladung eines vorhandenen Namens inkompatibel Major + 1
Jede Änderung an einer Schnittstelle, die ein Plugin implementiert (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) inkompatibel Major + 1
Ein Release, gebaut mit einer Toolchain, deren Binärschnittstelle sich von der vorherigen unterscheidet — eine andere Compiler-Familie oder Standardbibliothek, eine neue Qt-Hauptversion inkompatibel Major + 1

Ein neueres Toolset derselben Compiler-Familie, ein anderer C++-Standard und eine neuere Qt-Minor-Version sind kein solcher Bruch und ändern keine der beiden Zahlen; was ein natives Plugin treffen muss, steht unter Build-Voraussetzungen.

Der Editor lädt ein Plugin nur, wenn

  • apiVersion in plugin.json und IPlugin::info().apiVersion seiner eigenen Major-Version entsprechen und
  • apiMinorVersion in plugin.json (optional, Standard 0) und IPlugin::info().apiMinorVersion nicht über seiner eigenen Minor-Version liegen. Der Wert in info() ist standardmäßig die Minor-Version der Header, gegen die das Plugin kompiliert wurde; ein Plugin, das weiß, dass es nichts Neueres verwendet, darf in plugin.json eine niedrigere Zahl angeben und muss trotzdem die zweite Prüfung bestehen.

Ein abgelehntes Plugin bleibt unter Einstellungen ▸ Erweiterungen mit dem Grund aufgeführt („Inkompatibel (API-Unterschied)“, „Erfordert eine neuere Version von OctoEdit“) und bekommt einen Log-Eintrag — nie einen Absturz. Ein Python-Plugin hat kein gegen die Header kompiliertes info(), daher ist seine plugin.json seine einzige Angabe: Geben Sie die Minor-Version des neuesten Aufrufs an, den Sie verwenden.

Pre-Release-Richtlinie

Vor der ersten öffentlichen Veröffentlichung kann sich der Vertrag noch ändern, ohne dass eine der beiden Zahlen erhöht wird; bis dahin bleiben beide bei 1 / 0. Ab der ersten Veröffentlichung wird jede Änderung an der obigen Tabelle gemessen.