Gå til indholdet

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 samme id og apiVersion tilbage som plugin.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 kun false tilbage 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; kald reset() 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æggende TokenId-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

  • apiVersion i plugin.json og IPlugin::info().apiVersion er lig med dens egen major, og
  • apiMinorVersion i plugin.json (valgfri, standard 0) og IPlugin::info().apiMinorVersion ikke ligger over dens egen minor. Værdien i info() 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 i plugin.json og 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.