Gå til innhold

Programtillegg-API-et — oversikt

Alle PluginApi-typer ligger i namespace MTE::plugin. De offentlige hodefilene må ikke inkludere noen Qt-, Scintilla- eller MTE-intern hodefil — bare standardbiblioteket. Det ene bevisste unntaket er den valgfrie PluginApi/Qt/-mappen (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) for programtillegg som leverer sine egne Qt-kontroller.

Den samme kontrakten oppfylles av to backender: den innebygde Qt-innlasteren (delte biblioteker) og Python-backenden som kjører i sin egen prosess (Python-programtillegg). Dette avsnittet beskriver C++-flaten; hver tjeneste her som er backend-nøytral, har et motstykke på ctx i Python.

Programtilleggets livssyklus

Et innebygd programtillegg 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() må gi samme id og apiVersion tilbake som plugin.json; verten kryssjekker og nekter å laste inn ved uoverensstemmelse.
  • initialize() kjører én gang ved oppstart (etter at hvert programtillegg er oppdaget). Registrer kommandoer, menyer, leverandører og paneler her; gi bare false tilbake ved en virkelig feil (verten laster da programtillegget ut og noterer det).
  • shutdown() kjører ved avslutning (eller når programtillegget holder på å bli lastet ut). Den må ikke kaste unntak; kall reset() på hvert symbol, og sett de lagrede tjenestepekerne dine til null.

RAII-symboler

Hver registrering gir et symbol tilbake (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription …) — et BasicToken som bare kan flyttes, og som eier registreringen: ødelegger man det eller kaller reset() på det, tilbakekalles kommandoen, menyposten, panelet eller abonnementet. Behold hvert symbol som et medlem, og frigi det i shutdown(). To tilganger betyr noe:

  • id() — en ikke-ødeleggende TokenId-visning, for API-er som adresserer elementet uten å overta det (for eksempel IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — løsner og gir id-en tilbake; symbolet tilbakekaller ikke lenger noe. Sjelden det du vil ha.

Tjenestekatalogen

initialize() mottar en IPluginContext, et tjenestekatalog som verten eier. Lagre ikke konteksten ut over initialize(); lagre de enkelte tjenestereferansene du trenger (hver tjeneste overlever hvert programtillegg).

Tilgang Grensesnitt Brukes til
ctx.editor() IEditorService å lese og redigere bufferen, merkingen, markøren, filer, søk, bokmerker, innrykk og grupper i delt visning
ctx.commands() ICommandRegistry å registrere og kalle brukerhandlinger (med hurtigtaster)
ctx.menus() IMenuRegistry menyposter, skillelinjer, avkryssbare brytere og å nedtone poster
ctx.docks() IDockRegistry dokkbare sidepaneler
ctx.documents() IDocumentTypeRegistry fullverdige fanetyper i hovedvinduet (sammenligning, loggvisning …)
ctx.compare() ICompareService å åpne sammenligningsvisningen med to ruter
ctx.diagnostics() IDiagnosticsSink å publisere bølgelinjer og margtegn
ctx.events() IEventBus å abonnere på og publisere redigererhendelser
ctx.completions() ICompletionRegistry å bidra med kandidater til automatisk fullføring
ctx.contextMenu() IContextMenuRegistry dynamiske poster i redigererens høyreklikkmeny
ctx.snippetSessions() ISnippetSessionService å levere kodebiter som foldes ut med Tab (verten kjører feltøkten)
ctx.hover() IHoverRegistry bobler med opplysninger ved peking
ctx.signatureHelp() ISignatureHelpRegistry kalltips ved ( / ,
ctx.symbols() ISymbolRegistry symboler i dokumentet og arbeidsområdet (gå til symbol)
ctx.languages() ILanguageRegistry å liste språk, lese og bytte et dokuments språk og bidra med en språkdefinisjon
ctx.statusBar() IStatusBar forbigående meldinger og varige avsnitt
ctx.settings() ISettings varig nøkkel/verdi-oppsett per programtillegg
ctx.appSettings() const ISettings& å lese innstillinger for hele programmet (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings oppsett per dokument i lag med .editorconfig
ctx.settingsRegistry() ISettingsRegistry å legge til en side i innstillingsvinduet
ctx.themePalette() IThemePalette navngitte temafarger og integrasjon med Temaredigereren
ctx.lineMarkers() ILineMarkerService margsymboler og linjefarging som følger redigeringer (stoppunkter, endringsstriper); kravet på margklikket
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path din egen private skrivbare mappe
ctx.diagnosticsDir() std::filesystem::path den felles mappen for krasjspor og logger (skrivebeskyttet for programtillegg)

Å utvide API-et senere betyr å legge til et nytt tjenestegrensesnitt og en ny tilgang — et forenlig tillegg etter versjonsregelen: programtillegg som alt finnes, lastes fortsatt inn. Den verktøykjeden et programtilleggsbygg selv må passe til (kompilator, kjøretidsbibliotek, Qt-underversjon), er listet under Å skrive et programtillegg, «Byggekrav».

IEditorService — dokumentmodellen

Redigereren slik programtillegg ser den — ingen Scintilla, ingen Qt. Dokumenter vises det til med en ugjennomsiktig DocumentId (std::uint64_t, 0 = ugyldig). Posisjoner er byteposisjoner i UTF-8-tekst; MatchRange er et halvåpent {start, end} der noMatch() betyr «ikke funnet»; EditorPosition er et 0-basert {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 blottlegger også fanegruppenes delte oppsett i backend-nøytrale vendinger (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), så et programtillegg kan fange og gjenopprette ruteoppstillinger uten å se Qts deler. Se PluginApi/IEditorService.h for hele listen — hodefilen er den avgjørende referansen.

Et dokument som vises i to ruter, er ett dokument med én DocumentId, så groupOf() kan bare navngi den ene av plassene sine. To metoder beskriver og bygger opp igjen en deling:

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

showDocumentInGroup() gjør en visning som alt er i den ruten, aktiv, og lager ellers enda en delt visning der — det samme som Vis ▸ Del dokumentet til høyre gjør, så det reises ingen DocumentOpened, og begge visningene redigerer ett dokument. Ruter er det begrepet programtillegg ser; enkelte visninger er ikke det, og det er derfor kallet navngir en rute i stedet for å gi en visning tilbake.

Ta brukeren et sted

openFileAt(path, position) er det ene kallet bak «gå til byggefeilen», «hopp til stakkrammen» eller «åpne søketreffet», så alle lander på samme måte. Det åpner filen akkurat som openFile() (fanen til en fil som allerede er åpen, aktiveres, den åpnes ikke en gang til), gjør den til det aktive dokumentet, plasserer markøren uten merking, folder ut en bretting som skjuler linjen, ruller linjen inn i visningen — sentrert når den var utenfor skjermen, uten rulling når den allerede var synlig — og gir redigereren tastaturfokus. Posisjonen avgrenses, avvises aldri: en linje forbi slutten havner på siste linje, en kolonne forbi slutten av linjen havner på slutten av den, negative verdier teller som 0. Det returnerer dokumentet markøren ble plassert i, eller 0 når filen ikke kunne åpnes — og da har ingenting flyttet seg.

revealLine(doc, line) viser et sted uten å ta brukeren dit: det folder ut og ruller ruten som viser doc, men flytter ikke markøren eller merkingen, bytter ikke fane og tar ikke fokus — et panel som forhåndsviser en stakkramme eller følger sin egen merking, bruker det.

Dokumentets tegnkoding

Et dokuments tegnkoding på disken er kjernetilstand og ikke et programtilleggs. Verten finner den når filen leses, og skriver den samme tegnkodingen tilbake ved hver lagring, så en UTF-8-BOM-fil beholder merket sitt, og en UTF-16-fil blir værende UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) er en backend-uavhengig POD-enumerasjon som dekker de fem viktigste Unicode- og ANSI-kodingene pluss de eldre tegntabellene.

setDocumentEncoding() noterer et valg: den endrer ingen byte og avkoder ingenting på nytt — bufferen er alt Unicode, og den nye tegnkodingen får virkning ved neste lagring. Et programtillegg som tilbyr en «konverter til»-handling, bør derfor også kalle setModified(doc, true); et som tilbyr «tolk som», laster selv teksten på nytt først og noterer så valget. Verten publiserer DocumentEncodingChanged hver gang verdien faktisk endrer seg. Et prosjekts .editorconfig med charset vinner fortsatt ved lagring.

Dokumentets linjeskift

Et dokuments linjeavslutning er også kjernetilstand. Verten finner den når filen leses, skyver den inn i visningen så Enter setter inn den sekvensen, og skriver bufferens byte tilbake ordrett — så en Unix-fil som redigeres på Windows, blir værende LF, og en lagring uten endringer er byte for byte den samme. LineEnding (PluginApi/LineEnding.h) er en backend-uavhengig POD-enumerasjon — Unknown / Lf / CrLf / Cr — som navngir bytesekvensen og ikke en backend-konstant. Den leverer to inline-hjelpere: lineEndingBytes() og platformLineEnding() (det en ny buffer begynner med: CRLF på Windows, LF alle andre steder).

setLineEnding() noterer valget, stiller inn dokumentets visning så neste Enter setter det inn, og avgjør hva neste lagring skriver. Den konverterer ingen eksisterende tekst: et programtillegg som skriver om hele bufferen (kommandoene for konvertering av linjeslutt), erstatter selv teksten først, så konverteringen blir værende én endring som kan angres. Verten publiserer DocumentLineEndingChanged hver gang verdien faktisk endrer seg. Et prosjekts .editorconfig med end_of_line vinner fortsatt ved lagring, og flytter dokumentet over på den konvensjonen samtidig.

Tråder

Alle kall til konteksten og tjenestene skjer på vertens grensesnittråd. Kall ikke tjenester fra en bakgrunnstråd du selv har startet; kom tilbake først.

Versjonering

Kontrakten har en versjon i to deler, 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 som avgjør mellom dem, er hvem som implementerer grensesnittet:

Endring Type Versjon
En metode lagt til på slutten av et grensesnitt som bare redigereren implementerer, og som programtilleggene bare kaller (IEditorService, IPluginContext, hvert I…Registry, ILineMarkerService, …) forenlig minor + 1
Et nytt grensesnitt, en ny hendelsesstruktur, en ny enumerator på slutten av en enum forenlig minor + 1
Enhver endring av en struktur som krysser grensen (PluginInfo, ContextMenuRequest, DockPanelDescriptor, en hendelsesstruktur) uforenlig major + 1
En endret signatur, en fjernet eller omordnet metode, en ny overlasting av et eksisterende navn uforenlig major + 1
Enhver endring av et grensesnitt som et programtillegg implementerer (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) uforenlig major + 1
En utgivelse bygget med en verktøykjede med et annet binært grensesnitt enn den forrige — en annen kompilatorfamilie eller et annet standardbibliotek, en ny hovedversjon av Qt uforenlig major + 1

Et nyere verktøysett fra den samme kompilatorfamilien, et annet nivå av C++-standarden og en nyere minor-versjon av Qt er ikke et slikt brudd og endrer ingen av tallene; se Byggekrav for hva et innebygd programtillegg må passe til.

Redigereren laster bare inn et programtillegg når

  • apiVersion i plugin.json og IPlugin::info().apiVersion er lik dens egen major, og
  • apiMinorVersion i plugin.json (valgfri, standard 0) og IPlugin::info().apiMinorVersion ikke er høyere enn dens egen minor. Verdien i info() er som standard minor-versjonen til headerfilene programtillegget ble kompilert mot; et programtillegg som vet at det ikke bruker noe nyere, kan oppgi et lavere tall i plugin.json, og må likevel bestå den andre kontrollen.

Et avvist programtillegg står fortsatt oppført under Innstillinger ▸ Programtillegg med årsaken («Ikke kompatibel (API-en stemmer ikke)», «Krever en nyere versjon av OctoEdit») og får en loggoppføring — aldri et krasj. Et Python-programtillegg har ingen info() kompilert mot headerfilene, så plugin.json er dets eneste erklæring: oppgi minor-versjonen til det nyeste kallet du bruker.

Retningslinjer før utgivelsen

Før den første offentlige utgivelsen kan kontrakten fortsatt endre seg uten at noen av tallene heves; fram til da står begge på 1 / 0. Fra den første utgivelsen vurderes hver endring etter tabellen over.