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 dieselbeidundapiVersionzurückgeben wieplugin.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 Siefalsenur 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 Siereset()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-destruktiveTokenId-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
apiVersioninplugin.jsonundIPlugin::info().apiVersionseiner eigenen Major-Version entsprechen undapiMinorVersioninplugin.json(optional, Standard0) undIPlugin::info().apiMinorVersionnicht über seiner eigenen Minor-Version liegen. Der Wert ininfo()ist standardmäßig die Minor-Version der Header, gegen die das Plugin kompiliert wurde; ein Plugin, das weiß, dass es nichts Neueres verwendet, darf inplugin.jsoneine 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.