Ga naar inhoud

Plug-in-API — overzicht

Alle typen van PluginApi wonen in namespace MTE::plugin. De publieke headers mogen geen enkele header van Qt, Scintilla of intern van MTE invoegen — alleen de standaardbibliotheek. De ene bewuste uitzondering is de optionele map PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) voor plug-ins die hun eigen Qt-widgets meeleveren.

Hetzelfde contract wordt door twee backends vervuld: de inheemse Qt-lader (gedeelde bibliotheken) en de Python-backend buiten het proces (Python-plug-ins). Dit onderdeel documenteert het C++-vlak; elke dienst hier die backend-neutraal is heeft een Python-tegenhanger op ctx.

Levenscyclus van een plug-in

Een inheemse plug-in implementeert één 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() moet dezelfde id en apiVersion opleveren als plugin.json; de host vergelijkt ze en weigert bij een verschil te laden.
  • initialize() draait één keer bij het starten (nadat elke plug-in gevonden is). Registreer hier opdrachten, menu's, leveranciers en panelen; lever alleen false op bij een echte mislukking (de host ontlaadt de plug-in en noteert het).
  • shutdown() draait bij het afsluiten (of wanneer de plug-in ontladen wordt). Hij mag geen uitzondering werpen; voer reset() uit op elk token en zet uw bewaarde dienstwijzers op nul.

RAII-tokens

Elke registratie levert een token op (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription, …) — een BasicToken die alleen verplaatst kan worden en die de registratie bezit: hem vernietigen of er reset() op uitvoeren trekt de opdracht, het menu-item, het paneel of de inschrijving in. Houd elk token als lid vast en geef het in shutdown() vrij. Twee toegangen doen ertoe:

  • id() — een niet-vernietigende TokenId-kijk, voor API's die het item aanspreken zonder het over te nemen (bijvoorbeeld IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — koppelt los en levert de id op; het token trekt niets meer in. Zelden wat u wilt.

De dienstenzoeker

initialize() ontvangt een IPluginContext, een dienstenzoeker die de host bezit. Bewaar de context niet voorbij initialize(); bewaar de afzonderlijke dienstverwijzingen die u nodig hebt (elke dienst leeft langer dan elke plug-in).

Toegang Interface Waarvoor
ctx.editor() IEditorService buffer, selectie, cursor, bestanden, zoeken, bladwijzers, inspringing en groepen van de gesplitste weergave lezen/bewerken
ctx.commands() ICommandRegistry gebruikersacties registreren / aanroepen (met sneltoetsen)
ctx.menus() IMenuRegistry menu-items, scheidingen, aanvinkbare schakelaars, items grijs maken
ctx.docks() IDockRegistry dokbare zijpanelen
ctx.documents() IDocumentTypeRegistry volwaardige tabbladsoorten van de MainWindow (diff, logweergave, …)
ctx.compare() ICompareService de vergelijkingsweergave met twee deelvensters openen
ctx.diagnostics() IDiagnosticsSink golflijnen + margetekens publiceren
ctx.events() IEventBus inschrijven op / publiceren van editorgebeurtenissen
ctx.completions() ICompletionRegistry kandidaten voor automatisch aanvullen bijdragen
ctx.contextMenu() IContextMenuRegistry dynamische items in het rechtsklikmenu van de editor
ctx.snippetSessions() ISnippetSessionService met Tab uitvouwbare fragmenten leveren (de host draait de veldsessie)
ctx.hover() IHoverRegistry tekstballonnen bij aanwijzen
ctx.signatureHelp() ISignatureHelpRegistry aanroephulp bij ( / ,
ctx.symbols() ISymbolRegistry symbolen van document / werkruimte (ga naar symbool)
ctx.languages() ILanguageRegistry talen opsommen, de taal van een document lezen / wisselen, een taaldefinitie bijdragen
ctx.statusBar() IStatusBar vluchtige berichten + blijvende segmenten
ctx.settings() ISettings blijvende sleutel/waarde-configuratie per plug-in
ctx.appSettings() const ISettings& de instellingen van de hele toepassing lezen (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings configuratie per document, gelaagd met .editorconfig
ctx.settingsRegistry() ISettingsRegistry een pagina aan het venster Voorkeuren toevoegen
ctx.themePalette() IThemePalette benoemde themakleuren, integratie met de Thema-editor
ctx.lineMarkers() ILineMarkerService margesymbolen en regelkleuringen die bewerkingen volgen (onderbrekingspunten, wijzigingsstroken); de claim op de margeklik
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path uw eigen beschrijfbare map
ctx.diagnosticsDir() std::filesystem::path gedeelde map met crashartefacten en logboek (voor plug-ins alleen-lezen)

De API later uitbreiden betekent een nieuwe dienstinterface met bijbehorende toegang toevoegen — een compatibele toevoeging volgens de versieregel: bestaande plug-ins blijven laden. De toolchain waaraan de bouw van een plug-in zelf moet voldoen (compiler, runtime, Qt-minor) staat onder Een plug-in schrijven, «Bouwvereisten».

IEditorService — het documentmodel

De editor zoals plug-ins hem zien — geen Scintilla, geen Qt. Documenten worden aangeduid met een ondoorzichtige DocumentId (std::uint64_t, 0 = ongeldig). Verschuivingen zijn verschuivingen in bytes in de UTF-8-tekst; MatchRange is een halfopen {start, end} waarbij noMatch() «niet gevonden» betekent; EditorPosition is {line, column} vanaf 0 geteld.

// 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;

Hij stelt ook de indeling van de tabbladgroepen van de gesplitste weergave in backend-neutrale termen beschikbaar (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), zodat een plug-in schikkingen van deelvensters kan vastleggen en herstellen zonder de splitser van Qt te zien. Zie PluginApi/IEditorService.h voor de volledige lijst — de header is de gezaghebbende naslag.

Een document dat in twee deelvensters getoond wordt is één document met één DocumentId, dus groupOf() kan maar een van zijn plekken noemen. Twee methoden beschrijven en herbouwen een splitsing:

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

showDocumentInGroup() activeert een weergave die al in dat deelvenster staat, en maakt er anders een tweede gedeelde weergave — hetzelfde wat Beeld ▸ Document rechts splitsen doet, dus er wordt geen DocumentOpened opgeworpen en beide weergaven bewerken één document. Deelvensters zijn het begrip dat naar plug-ins gekeerd is; afzonderlijke weergaven niet, en daarom noemt de aanroep een deelvenster in plaats van een weergave terug te geven.

De gebruiker ergens heen brengen

openFileAt(path, position) is de ene aanroep achter «ga naar de bouwfout», «spring naar het stackframe» of «open het zoekresultaat», zodat ze allemaal op dezelfde manier landen. Hij opent het bestand precies zoals openFile() (het tabblad van een al geopend bestand wordt geactiveerd, niet een tweede keer geopend), maakt het tot het actieve document, zet de cursor zonder selectie, klapt een vouw open die de regel verbergt, scrolt de regel in beeld — gecentreerd als hij buiten beeld stond, zonder te scrollen als hij al zichtbaar was — en geeft de editor de toetsenbordfocus. De positie wordt begrensd, nooit geweigerd: een regel voorbij het einde komt op de laatste regel terecht, een kolom voorbij het einde van de regel aan het einde ervan, negatieve waarden tellen als 0. Hij geeft het document terug waarin de cursor is gezet, of 0 als het bestand niet geopend kon worden — en dan is er niets verschoven.

revealLine(doc, line) toont een plek zonder de gebruiker erheen te brengen: hij klapt open en scrolt het deelvenster dat doc toont, maar verplaatst de cursor en de selectie niet, wisselt niet van tabblad en neemt de focus niet — een paneel dat een stackframe voorvertoont of zijn selectie volgt, gebruikt hem.

Tekencodering van het document

De tekencodering van een document op de schijf is staat van de kern, niet van een plug-in. De host herkent haar wanneer het bestand gelezen wordt en schrijft bij elke opslag dezelfde codering terug, zodat een UTF-8-bestand met BOM zijn markering houdt en een UTF-16-bestand UTF-16 blijft. DocumentEncoding (PluginApi/DocumentEncoding.h) is een backend-onafhankelijke POD-enum die de vijf voornaamste Unicode/ANSI-coderingen plus de oude codepagina's dekt.

setDocumentEncoding() legt een keuze vast: het verandert geen byte en decodeert niets opnieuw — de buffer is al Unicode, en de nieuwe codering wordt bij de volgende opslag van kracht. Een plug-in die een actie «omzetten naar» aanbiedt zou daarom ook setModified(doc, true) moeten aanroepen; een die «interpreteren als» aanbiedt herlaadt eerst zelf de tekst en legt daarna de keuze vast. De host publiceert DocumentEncodingChanged telkens wanneer de waarde werkelijk verandert. Een charset in een .editorconfig van een project wint nog steeds bij het opslaan.

Regeleinden van het document

Ook het regeleindeteken van een document is staat van de kern. De host herkent het wanneer het bestand gelezen wordt, duwt het in de weergave zodat Enter die reeks invoegt, en schrijft de bytes van de buffer letterlijk terug — zodat een Unix-bestand dat op Windows bewerkt wordt LF blijft en een opslag zonder bewerkingen byte voor byte identiek is. LineEnding (PluginApi/LineEnding.h) is een backend-onafhankelijke POD-enum — Unknown / Lf / CrLf / Cr — die de bytereeks noemt in plaats van een constante van de backend. Er komen twee inline hulpjes mee: lineEndingBytes() en platformLineEnding() (waarmee een nieuwe buffer begint: CRLF op Windows, LF elders).

setLineEnding() legt de keuze vast, stemt de weergave van het document zo af dat de volgende Enter haar invoegt, en bepaalt wat de volgende opslag schrijft. Het zet geen bestaande tekst om: een plug-in die de hele buffer herschrijft (de opdrachten voor Regeleinde omzetten) vervangt eerst zelf de tekst, zodat de omzetting één ongedaan te maken bewerking blijft. De host publiceert DocumentLineEndingChanged telkens wanneer de waarde werkelijk verandert. Een end_of_line in een .editorconfig van een project wint nog steeds bij het opslaan, en brengt het document daarmee op die afspraak.

Threads

Alle aanroepen naar de context en de diensten gebeuren op de interfacethread van de host. Roep geen diensten aan vanuit een achtergrondthread die u zelf gestart hebt; keer eerst terug.

Versiebeheer

Het contract heeft een versie in twee delen, allebei 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

Wat tussen de twee beslist, is wie de interface implementeert:

Wijziging Soort Versie
Een methode achteraan toegevoegd aan een interface die alleen de editor implementeert en die plug-ins alleen aanroepen (IEditorService, IPluginContext, elke I…Registry, ILineMarkerService, …) compatibel minor + 1
Een nieuwe interface, een nieuwe gebeurtenisstructuur, een nieuwe enumerator achteraan een enum compatibel minor + 1
Elke wijziging aan een structuur die de grens oversteekt (PluginInfo, ContextMenuRequest, DockPanelDescriptor, een gebeurtenisstructuur) niet compatibel major + 1
Een gewijzigde signatuur, een verwijderde of anders geordende methode, een nieuwe overload van een bestaande naam niet compatibel major + 1
Elke wijziging aan een interface die een plug-in implementeert (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) niet compatibel major + 1
Een uitgave gebouwd met een toolchain waarvan de binaire interface verschilt van de vorige — een andere compilerfamilie of standaardbibliotheek, een nieuwe hoofdversie van Qt niet compatibel major + 1

Een nieuwere toolset van dezelfde compilerfamilie, een ander niveau van de C++-standaard en een nieuwere minorversie van Qt zijn geen zulke breuk en veranderen geen van beide getallen; zie Bouwvereisten voor wat een native plug-in moet volgen.

De editor laadt een plug-in alleen als

  • apiVersion in plugin.json en IPlugin::info().apiVersion gelijk zijn aan zijn eigen major, en
  • apiMinorVersion in plugin.json (optioneel, standaard 0) en IPlugin::info().apiMinorVersion niet boven zijn eigen minor liggen. De waarde in info() is standaard de minor van de headers waartegen de plug-in is gecompileerd; een plug-in die weet dat hij niets nieuwers gebruikt, mag in plugin.json een lager getal opgeven en moet toch door de tweede controle komen.

Een geweigerde plug-in blijft onder Voorkeuren ▸ Plug-ins staan met de reden («Niet compatibel (API komt niet overeen)», «Vereist een nieuwere versie van OctoEdit») en krijgt een logregel — nooit een crash. Een Python-plug-in heeft geen tegen de headers gecompileerde info(), dus zijn plugin.json is zijn enige verklaring: geef de minor op van de nieuwste aanroep die je gebruikt.

Beleid vóór de uitgave

Vóór de eerste publieke uitgave kan het contract nog veranderen zonder dat een van beide getallen omhooggaat; tot dan blijven ze op 1 / 0. Vanaf de eerste uitgave wordt elke wijziging aan de hand van de tabel hierboven beoordeeld.