Gå till innehållet

Insticksmoduls-API:t — översikt

Alla typer i PluginApi bor i namespace MTE::plugin. De publika headerna får inte inkludera någon header från Qt, Scintilla eller MTE:s inre — bara standardbiblioteket. Det enda medvetna undantaget är den valfria mappen PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) för insticksmoduler som levererar egna Qt-widgetar.

Samma kontrakt uppfylls av två backendar: den inhemska Qt-laddaren (delade bibliotek) och Python-backenden utanför processen (Python-insticksmoduler). Det här avsnittet dokumenterar C++-ytan; varje tjänst här som är backend-neutral har en Python-motsvarighet på ctx.

En insticksmoduls livscykel

En inhemsk insticksmodul implementerar en enda klass:

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åste returnera samma id och apiVersion som plugin.json; värden dubbelkollar och vägrar läsa in vid avvikelse.
  • initialize() körs en gång vid start (efter att varje insticksmodul upptäckts). Registrera kommandon, menyer, leverantörer och paneler här; returnera false bara vid ett verkligt misslyckande (värden läser ut modulen och noterar det).
  • shutdown() körs vid avslut (eller när modulen läses ut). Den får inte kasta undantag; reset() varje symbol och nollställ dina sparade tjänstepekare.

RAII-symboler

Varje registrering returnerar en symbol (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription, …) — en BasicToken som bara kan flyttas och som äger registreringen: att förstöra den eller köra reset() på den återkallar kommandot, menyposten, panelen eller prenumerationen. Håll varje symbol som medlem och frigör den i shutdown(). Två åtkomstfunktioner spelar roll:

  • id() — en icke-förstörande TokenId-vy, för API:er som adresserar posten utan att ta över den (till exempel IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — kopplar loss och returnerar id:t; symbolen återkallar inget längre. Sällan det du vill.

Tjänstesökaren

initialize() tar emot ett IPluginContext, en tjänstesökare som värden äger. Spara inte kontexten bortom initialize(); spara de enskilda tjänstereferenser du behöver (varje tjänst lever längre än varje insticksmodul).

Åtkomst Gränssnitt Används för
ctx.editor() IEditorService läsa/ändra buffert, markering, markör, filer, sökning, bokmärken, indrag, den delade vyns grupper
ctx.commands() ICommandRegistry registrera / anropa användaråtgärder (med kortkommandon)
ctx.menus() IMenuRegistry menyposter, avskiljare, kryssbara växlar, gråtona poster
ctx.docks() IDockRegistry dockningsbara sidopaneler
ctx.documents() IDocumentTypeRegistry fullvärdiga fliktyper i MainWindow (diff, loggvy, …)
ctx.compare() ICompareService öppna jämförelsevyn med två rutor
ctx.diagnostics() IDiagnosticsSink publicera våglinjer + marginaltecken
ctx.events() IEventBus prenumerera på / publicera redigerarhändelser
ctx.completions() ICompletionRegistry bidra med kandidater för automatisk komplettering
ctx.contextMenu() IContextMenuRegistry dynamiska poster i redigerarens högerklicksmeny
ctx.snippetSessions() ISnippetSessionService tillhandahålla kodfragment som utvidgas med Tab (värden kör fältsessionen)
ctx.hover() IHoverRegistry verktygstips vid pekning
ctx.signatureHelp() ISignatureHelpRegistry anropstips vid ( / ,
ctx.symbols() ISymbolRegistry symboler för dokument / arbetsyta (gå till symbol)
ctx.languages() ILanguageRegistry lista språk, läsa / byta ett dokuments språk, bidra med en språkdefinition
ctx.statusBar() IStatusBar flyktiga meddelanden + bestående segment
ctx.settings() ISettings bestående nyckel/värde-konfiguration per insticksmodul
ctx.appSettings() const ISettings& läsa programövergripande inställningar (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings konfiguration per dokument, skiktad med .editorconfig
ctx.settingsRegistry() ISettingsRegistry lägga till en sida i dialogrutan Inställningar
ctx.themePalette() IThemePalette namngivna temafärger, integration med Temaredigeraren
ctx.lineMarkers() ILineMarkerService marginalsymboler och radtoningar som följer redigeringar (brytpunkter, ändringsremsor); anspråket på marginalklicket
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path din egen skrivbara katalog
ctx.diagnosticsDir() std::filesystem::path delad katalog för kraschartefakter och logg (skrivskyddad för insticksmoduler)

Att utöka API:t senare innebär att lägga till ett nytt tjänstegränssnitt med tillhörande åtkomst — ett kompatibelt tillägg enligt versionsregeln: befintliga insticksmoduler fortsätter att läsas in. Den verktygskedja ett insticksmodulsbygge självt måste stämma med (kompilator, runtime, Qt-minor) listas under Skriva en insticksmodul, «Byggkrav».

IEditorService — dokumentmodellen

Redigeraren som insticksmoduler ser den — ingen Scintilla, ingen Qt. Dokument refereras med ett ogenomskinligt DocumentId (std::uint64_t, 0 = ogiltigt). Förskjutningar är förskjutningar i byte i UTF-8-texten; MatchRange är ett halvöppet {start, end} där noMatch() betyder «hittades inte»; EditorPosition är {line, column} räknat från 0.

// 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 exponerar också den delade vyns flikgruppslayout i backend-neutrala termer (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), så att en insticksmodul kan fånga och återställa ruppställningar utan att se Qt:s delare. Se PluginApi/IEditorService.h för hela listan — headern är den auktoritativa referensen.

Ett dokument som visas i två rutor är ett dokument med ett DocumentId, så groupOf() kan bara namnge en av dess platser. Två metoder beskriver och bygger om en delning:

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

showDocumentInGroup() aktiverar en vy som redan finns i den rutan, och skapar annars en andra delad vy där — samma sak som Visa ▸ Dela dokumentet höger gör, så ingen DocumentOpened väcks och båda vyerna redigerar ett dokument. Rutor är begreppet som vetter mot insticksmoduler; enskilda vyer är det inte, och därför namnger anropet en ruta i stället för att lämna tillbaka en vy.

Ta användaren någonstans

openFileAt(path, position) är det enda anropet bakom «gå till byggfelet», «hoppa till stackramen» eller «öppna sökträffen», så att alla landar på samma sätt. Det öppnar filen precis som openFile() (fliken för en redan öppen fil aktiveras, den öppnas inte en gång till), gör den till det aktiva dokumentet, placerar markören utan markering, fäller ut en vikning som döljer raden, rullar raden in i vyn — centrerad om den var utanför skärmen, utan rullning om den redan syntes — och ger redigeraren tangentbordsfokus. Positionen begränsas, avvisas aldrig: en rad bortom slutet hamnar på sista raden, en kolumn bortom radens slut hamnar i dess slut, negativa värden räknas som 0. Det returnerar dokumentet där markören placerades, eller 0 om filen inte kunde öppnas — och då har ingenting flyttats.

revealLine(doc, line) visar en plats utan att ta användaren dit: det fäller ut och rullar rutan som visar doc, men flyttar inte markören eller markeringen, byter inte flik och tar inte fokus — en panel som förhandsvisar en stackram eller följer sin markering använder det.

Dokumentets teckenkodning

Ett dokuments teckenkodning på disken är kärnans tillstånd, inte en insticksmoduls. Värden identifierar den när filen läses och skriver tillbaka samma kodning vid varje sparande, så att en UTF-8-fil med BOM behåller sitt märke och en UTF-16-fil förblir UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) är en backend-oberoende POD-uppräkning som täcker de fem huvudsakliga Unicode/ANSI-kodningarna plus de äldre kodsidorna.

setDocumentEncoding() noterar ett val: den ändrar ingen byte och avkodar inget om — bufferten är redan Unicode, och den nya kodningen får effekt vid nästa sparande. En insticksmodul som erbjuder en åtgärd «konvertera till» bör därför också anropa setModified(doc, true); en som erbjuder «tolka som» läser först om texten själv och noterar sedan valet. Värden publicerar DocumentEncodingChanged när värdet faktiskt ändras. Ett projekts charset i .editorconfig vinner ändå vid sparandet.

Dokumentets radslut

Ett dokuments radavslutare är kärnans tillstånd också. Värden identifierar det när filen läses, skjuter in det i vyn så att Enter infogar den sekvensen, och skriver tillbaka buffertens byte ordagrant — så att en Unix-fil som redigeras på Windows förblir LF och ett sparande utan ändringar är byte för byte identiskt. LineEnding (PluginApi/LineEnding.h) är en backend-oberoende POD-uppräkning — Unknown / Lf / CrLf / Cr — som namnger bytesekvensen snarare än en konstant i backenden. Den levererar två inline-hjälpare: lineEndingBytes() och platformLineEnding() (det en ny buffert börjar med: CRLF på Windows, LF annars).

setLineEnding() noterar valet, stämmer om dokumentets vy så att nästa Enter infogar det, och avgör vad nästa sparande skriver. Den konverterar ingen befintlig text: en insticksmodul som skriver om hela bufferten (kommandona för Konvertera radslut) ersätter texten själv först, så att konverteringen förblir en enda ångerbar ändring. Värden publicerar DocumentLineEndingChanged när värdet faktiskt ändras. Ett projekts end_of_line i .editorconfig vinner ändå vid sparandet, och flyttar dokumentet till den konventionen med sig.

Trådar

Alla anrop till kontexten och tjänsterna sker på värdens gränssnittstråd. Anropa inte tjänster från en bakgrundstråd du startat; återvänd först.

Versionshantering

Kontraktet har en version i två delar, båda 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 avgör mellan dem är vem som implementerar gränssnittet:

Ändring Typ Version
En metod tillagd i slutet av ett gränssnitt som bara redigeraren implementerar och som insticksmodulerna bara anropar (IEditorService, IPluginContext, varje I…Registry, ILineMarkerService, …) kompatibel minor + 1
Ett nytt gränssnitt, en ny händelsestruktur, en ny enumerator i slutet av en enum kompatibel minor + 1
Varje ändring av en struktur som korsar gränsen (PluginInfo, ContextMenuRequest, DockPanelDescriptor, en händelsestruktur) inkompatibel major + 1
En ändrad signatur, en borttagen eller omordnad metod, en ny överlagring av ett befintligt namn inkompatibel major + 1
Varje ändring av ett gränssnitt som en insticksmodul implementerar (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) inkompatibel major + 1
En utgåva byggd med en verktygskedja vars binära gränssnitt skiljer sig från den föregående — en annan kompilatorfamilj eller ett annat standardbibliotek, en ny huvudversion av Qt inkompatibel major + 1

En nyare verktygsuppsättning från samma kompilatorfamilj, en annan nivå av C++-standarden och en nyare minorversion av Qt är inget sådant brott och ändrar inget av talen; se Byggkrav för vad en inbyggd insticksmodul måste stämma med.

Redigeraren läser bara in en insticksmodul när

  • apiVersion i plugin.json och IPlugin::info().apiVersion är lika med dess egen major, och
  • apiMinorVersion i plugin.json (valfri, standard 0) och IPlugin::info().apiMinorVersion inte ligger över dess egen minor. Värdet i info() är som standard minorversionen för de headerfiler modulen kompilerades mot; en modul som vet att den inte använder något nyare får ange ett lägre tal i plugin.json, och måste ändå klara den andra kontrollen.

En avvisad insticksmodul står kvar i Inställningar ▸ Insticksmoduler med orsaken («Inkompatibel (API stämmer inte)», «Kräver en nyare version av OctoEdit») och får en loggpost — aldrig en krasch. En Python-modul har ingen info() kompilerad mot headerfilerna, så dess plugin.json är dess enda uppgift: ange minorversionen för det nyaste anropet du använder.

Policy före utgåvan

Före den första publika utgåvan kan kontraktet fortfarande ändras utan att något av talen höjs; fram till dess står båda på 1 / 0. Från den första utgåvan bedöms varje ändring enligt tabellen ovan.