Udvidelsesmodul-API'en — oversigt¶
Alle PluginApi-typer ligger i namespace MTE::plugin. De offentlige headerfiler må
ikke inkludere nogen Qt-, Scintilla- eller MTE-intern headerfil — kun
standardbiblioteket. Den ene bevidste undtagelse er den valgfri PluginApi/Qt/-mappe
(PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) til udvidelsesmoduler, der
leverer deres egne Qt-kontroller.
Den samme kontrakt opfyldes af to backends: den indbyggede Qt-indlæser (delte
biblioteker) og Python-backenden, der kører i sin egen proces
(Python-udvidelsesmoduler). Dette afsnit beskriver
C++-fladen; hver tjeneste her, der er backend-neutral, har et modstykke på ctx i
Python.
Udvidelsesmodulets livscyklus¶
Et indbygget udvidelsesmodul implementerer é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()skal give det sammeidogapiVersiontilbage somplugin.json; værten krydskontrollerer og nægter at indlæse ved uoverensstemmelse.initialize()kører én gang ved opstart (efter at hvert udvidelsesmodul er opdaget). Registrér kommandoer, menuer, leverandører og paneler her; giv kunfalsetilbage ved en virkelig fejl (værten udlæser så modulet og noterer det).shutdown()kører ved afslutning (eller når udvidelsesmodulet er ved at blive udlæst). Den må ikke kaste undtagelser; kaldreset()på hvert symbol, og sæt dine gemte tjenestepegere til null.
RAII-symboler¶
Hver registrering giver et symbol tilbage (CommandToken, MenuToken, DockToken,
SettingsPageToken, Subscription …) — et BasicToken, der kun kan flyttes, og som
ejer registreringen: ødelægger man det eller kalder reset() på det, tilbagekaldes
kommandoen, menupunktet, panelet eller abonnementet. Behold hvert symbol som et medlem,
og frigiv det i shutdown(). To tilgange betyder noget:
id()— en ikke-ødelæggendeTokenId-visning til API'er, der adresserer elementet uden at overtage det (f.eks.IMenuRegistry::setItemChecked/setItemEnabled).release()— frigør og giver id'et tilbage; symbolet tilbagekalder ikke længere noget. Sjældent det, du vil have.
Tjenestekataloget¶
initialize() modtager en IPluginContext, et tjenestekatalog, som værten ejer. Gem
ikke konteksten ud over initialize(); gem de enkelte tjenestereferencer, du har
brug for (hver tjeneste overlever hvert udvidelsesmodul).
| Tilgang | Grænseflade | Bruges til |
|---|---|---|
ctx.editor() |
IEditorService |
at læse og redigere bufferen, markeringen, markøren, filer, søgning, bogmærker, indrykning og grupper i opdelt visning |
ctx.commands() |
ICommandRegistry |
at registrere og kalde brugerhandlinger (med genvejstaster) |
ctx.menus() |
IMenuRegistry |
menupunkter, skillelinjer, afkrydselige kontakter og at nedtone punkter |
ctx.docks() |
IDockRegistry |
sidepaneler, der kan dokkes |
ctx.documents() |
IDocumentTypeRegistry |
fuldgyldige fanebladstyper i hovedvinduet (forskel, logvisning …) |
ctx.compare() |
ICompareService |
at åbne sammenligningsvisningen med to ruder |
ctx.diagnostics() |
IDiagnosticsSink |
at udgive bølgelinjer og margentegn |
ctx.events() |
IEventBus |
at abonnere på og udgive editorhændelser |
ctx.completions() |
ICompletionRegistry |
at bidrage med kandidater til automatisk fuldførelse |
ctx.contextMenu() |
IContextMenuRegistry |
dynamiske poster i editorens højrekliksmenu |
ctx.snippetSessions() |
ISnippetSessionService |
at levere kodestumper, der kan foldes ud med Tab (værten kører feltsessionen) |
ctx.hover() |
IHoverRegistry |
bobler med oplysninger ved pegning |
ctx.signatureHelp() |
ISignatureHelpRegistry |
kaldetip ved ( / , |
ctx.symbols() |
ISymbolRegistry |
symboler i dokumentet og arbejdsområdet (gå til symbol) |
ctx.languages() |
ILanguageRegistry |
at opregne sprog, læse og skifte et dokuments sprog og bidrage med en sprogdefinition |
ctx.statusBar() |
IStatusBar |
forbigående beskeder og vedvarende afsnit |
ctx.settings() |
ISettings |
vedvarende nøgle/værdi-opsætning pr. udvidelsesmodul |
ctx.appSettings() |
const ISettings& |
at læse indstillinger for hele programmet (AppSettingsKeys.h) |
ctx.documentSettings() |
IDocumentSettings |
opsætning pr. dokument i lag med .editorconfig |
ctx.settingsRegistry() |
ISettingsRegistry |
at tilføje en side til indstillingsvinduet |
ctx.themePalette() |
IThemePalette |
navngivne temafarver og integration med Temaeditoren |
ctx.lineMarkers() |
ILineMarkerService |
margensymboler og linjefarvning, der følger redigeringer (stoppunkter, ændringsstriber); kravet på margenklikket |
ctx.log() |
ILogger |
trace / debug / info / warning / error |
ctx.pluginDataDir() |
std::filesystem::path |
din egen private skrivbare mappe |
ctx.diagnosticsDir() |
std::filesystem::path |
den fælles mappe til nedbrudsspor og logfiler (skrivebeskyttet for udvidelsesmoduler) |
At udvide API'en senere betyder at tilføje en ny tjenestegrænseflade og en ny tilgang — en forenelig tilføjelse efter versionsreglen: eksisterende udvidelsesmoduler indlæses fortsat. Den værktøjskæde, en modulbygning selv skal passe til (compiler, kørselsbibliotek, Qt-underversion), er opregnet under At skrive et udvidelsesmodul, »Byggekrav«.
IEditorService — dokumentmodellen¶
Editoren, som udvidelsesmoduler ser den — ingen Scintilla, ingen Qt. Dokumenter
henvises der til med et uigennemsigtigt DocumentId (std::uint64_t, 0 = ugyldigt).
Positioner er bytepositioner i UTF-8-tekst; MatchRange er et halvåbent
{start, end}, hvor noMatch() betyder »ikke fundet«; EditorPosition er et
0-baseret {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;
Den blotlægger også fanebladsgruppernes opdelte layout i backend-neutrale vendinger
(groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(),
setGroupWeights()), så et udvidelsesmodul kan optage og gendanne rudeopstillinger
uden at se Qts opdeler. Se PluginApi/IEditorService.h for hele listen —
headerfilen er den afgørende reference.
Et dokument, der vises i to ruder, er ét dokument med ét DocumentId, så
groupOf() kan kun navngive den ene af dets pladser. To metoder beskriver og bygger
en opdeling op igen:
std::vector<int> groupsOf(DocumentId doc) const; // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group); // put it in one more
showDocumentInGroup() aktiverer en visning, der allerede er i den rude, og opretter
ellers endnu en delt visning der — det samme, som Vis ▸ Opdel dokumentet til
højre gør, så der rejses ingen DocumentOpened, og begge visninger redigerer ét
dokument. Ruder er det begreb, udvidelsesmoduler ser; enkelte visninger er ikke, og
det er derfor, kaldet navngiver en rude i stedet for at give en visning tilbage.
At tage brugeren et sted hen¶
openFileAt(path, position) er det ene kald bag »gå til byggefejlen«, »spring
til stakrammen« eller »åbn søgeresultatet«, så de alle lander på samme måde.
Det åbner filen præcis som openFile() (fanebladet for en fil, der allerede er
åben, aktiveres; den åbnes ikke en gang til), gør den til det aktive dokument,
placerer markøren uden markering, folder en foldning ud, der skjuler linjen,
ruller linjen ind i visningen — centreret, når den var uden for skærmen,
uden rulning, når den allerede var synlig — og giver editoren
tastaturfokus. Positionen afgrænses, afvises aldrig: en linje efter
slutningen lander på den sidste linje, en kolonne efter linjens slutning
lander ved dens slutning, negative værdier tæller som 0. Det returnerer det
dokument, markøren blev placeret i, eller 0, når filen ikke kunne åbnes — og
så har intet flyttet sig.
revealLine(doc, line) viser et sted uden at tage brugeren derhen: det folder
ud og ruller den rude, der viser doc, men flytter ikke markøren eller
markeringen, skifter ikke faneblad og tager ikke fokus — et panel, der viser
en stakramme som forhåndsvisning eller følger sin egen markering, bruger det.
Dokumentets tegnkodning¶
Et dokuments tegnkodning på disken er kernetilstand og ikke et udvidelsesmoduls.
Værten registrerer den, når filen læses, og skriver den samme tegnkodning tilbage ved
hver gemning, så en UTF-8-BOM-fil beholder sit mærke, og en UTF-16-fil bliver ved med
at være UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) er en
backend-uafhængig POD-enumeration, der dækker de fem primære Unicode- og
ANSI-kodninger plus de ældre tegntabeller.
setDocumentEncoding() noterer et valg: den ændrer ingen byte og afkoder ingenting
igen — bufferen er allerede Unicode, og den nye tegnkodning får virkning ved næste
gemning. Et udvidelsesmodul, der tilbyder en »konvertér til«-handling, bør derfor også
kalde setModified(doc, true); et, der tilbyder »fortolk som«, indlæser selv teksten
igen først og noterer så valget. Værten udgiver DocumentEncodingChanged, hver gang
værdien faktisk ændrer sig. Et projekts .editorconfig med charset vinder stadig
ved gemning.
Dokumentets linjeskift¶
Et dokuments linjeafslutning er også kernetilstand. Værten registrerer den, når
filen læses, skubber den ind i visningen, så Enter indsætter den sekvens, og skriver
bufferens byte tilbage ordret — så en Unix-fil, der redigeres på Windows, bliver ved
med at være LF, og en gemning uden ændringer er byte for byte den samme. LineEnding
(PluginApi/LineEnding.h) er en backend-uafhængig POD-enumeration — Unknown / Lf /
CrLf / Cr — der navngiver bytesekvensen og ikke en backend-konstant. Den leverer to
inline-hjælpere: lineEndingBytes() og platformLineEnding() (det, en ny buffer
begynder med: CRLF på Windows, LF alle andre steder).
setLineEnding() noterer valget, indstiller dokumentets visning, så næste Enter
indsætter det, og afgør, hvad næste gemning skriver. Den konverterer ingen
eksisterende tekst: et udvidelsesmodul, der skriver hele bufferen om (kommandoerne til
konvertering af linjeskift), erstatter selv teksten først, så konverteringen bliver ved
med at være én ændring, der kan fortrydes. Værten udgiver
DocumentLineEndingChanged, hver gang værdien faktisk ændrer sig. Et projekts
.editorconfig med end_of_line vinder stadig ved gemning og flytter dokumentet over
på den konvention samtidig.
Tråde
Alle kald til konteksten og tjenesterne sker på værtens brugerfladetråd. Kald ikke tjenester fra en baggrundstråd, du selv har startet; vend tilbage først.
Versionering¶
Kontrakten har en version i to dele, begge i PluginApi/PluginInfo.h:
constexpr std::uint32_t MTE_PLUGIN_API_VERSION = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor
Det, der afgør mellem dem, er hvem der implementerer grænsefladen:
| Ændring | Art | Version |
|---|---|---|
En metode tilføjet sidst i en grænseflade, som kun editoren implementerer, og som modulerne kun kalder (IEditorService, IPluginContext, hvert I…Registry, ILineMarkerService, …) |
forenelig | minor + 1 |
| En ny grænseflade, en ny hændelsesstruktur, en ny enumerator sidst i en enum | forenelig | minor + 1 |
Enhver ændring af en struktur, der krydser grænsen (PluginInfo, ContextMenuRequest, DockPanelDescriptor, en hændelsesstruktur) |
uforenelig | major + 1 |
| En ændret signatur, en fjernet eller omordnet metode, en ny overload af et eksisterende navn | uforenelig | major + 1 |
Enhver ændring af en grænseflade, som et modul implementerer (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) |
uforenelig | major + 1 |
| En udgivelse bygget med en værktøjskæde, hvis binære grænseflade adskiller sig fra den forrige — en anden compilerfamilie eller et andet standardbibliotek, en ny hovedversion af Qt | uforenelig | major + 1 |
Et nyere værktøjssæt fra den samme compilerfamilie, et andet niveau af C++-standarden og en nyere minor-version af Qt er ikke et sådant brud og ændrer ingen af tallene; se Byggekrav for, hvad et indbygget udvidelsesmodul skal passe til.
Editoren indlæser kun et udvidelsesmodul, når
apiVersioniplugin.jsonogIPlugin::info().apiVersioner lig med dens egen major, ogapiMinorVersioniplugin.json(valgfri, standard0) ogIPlugin::info().apiMinorVersionikke ligger over dens egen minor. Værdien iinfo()er som standard minor-versionen af de headerfiler, modulet blev kompileret mod; et modul, der ved, at det ikke bruger noget nyere, må angive et lavere tal iplugin.jsonog skal stadig bestå den anden kontrol.
Et afvist udvidelsesmodul står fortsat i Indstillinger ▸ Udvidelsesmoduler
med årsagen (»Ikke kompatibel (API'en passer ikke)«, »Kræver en nyere version
af OctoEdit«) og får en logpost — aldrig et nedbrud. Et Python-modul har ingen
info() kompileret mod headerfilerne, så dets plugin.json er dets eneste
erklæring: angiv minor-versionen for det nyeste kald, du bruger.
Politik inden udgivelse
Inden den første offentlige udgivelse kan kontrakten stadig ændre sig, uden at
nogen af tallene hæves; indtil da står begge på 1 / 0. Fra den første
udgivelse vurderes hver ændring efter tabellen ovenfor.