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 dezelfdeidenapiVersionopleveren alsplugin.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 alleenfalseop 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; voerreset()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-vernietigendeTokenId-kijk, voor API's die het item aanspreken zonder het over te nemen (bijvoorbeeldIMenuRegistry::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
apiVersioninplugin.jsonenIPlugin::info().apiVersiongelijk zijn aan zijn eigen major, enapiMinorVersioninplugin.json(optioneel, standaard0) enIPlugin::info().apiMinorVersionniet boven zijn eigen minor liggen. De waarde ininfo()is standaard de minor van de headers waartegen de plug-in is gecompileerd; een plug-in die weet dat hij niets nieuwers gebruikt, mag inplugin.jsoneen 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.