Preskočiť na obsah

API zásuvných modulov — prehľad

Všetky typy PluginApi sa nachádzajú v priestore namespace MTE::plugin. Verejné hlavičky nesmú zahŕňať žiadnu hlavičku z Qt, Scintilly ani internú hlavičku MTE — iba štandardnú knižnicu. Jedinou zámernou výnimkou je voliteľný priečinok PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) pre zásuvné moduly, ktoré dodávajú vlastné widgety Qt.

Ten istý kontrakt napĺňajú dva backendy: natívny zavádzač Qt (zdieľané knižnice) a backend Pythonu bežiaci mimo procesu (zásuvné moduly v Pythone). Táto sekcia dokumentuje plochu v C++; každá služba, ktorá je tu nezávislá od backendu, má na objekte ctx svoj náprotivok v Pythone.

Životný cyklus zásuvného modulu

Natívny zásuvný modul implementuje jednu triedu:

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() musí vrátiť to isté id a apiVersion ako plugin.json; hostiteľ to krížovo overí a pri nezhode odmietne načítanie.
  • initialize() beží raz pri spustení (po tom, čo sa nájdu všetky zásuvné moduly). Zaregistrujte tu príkazy, ponuky, poskytovateľov a panely; hodnotu false vráťte iba pri skutočnom zlyhaní (hostiteľ zásuvný modul odpojí a zaznamená to).
  • shutdown() beží pri ukončení (alebo keď sa zásuvný modul odpája). Nesmie vyvolať výnimku; zavolajte reset() na každom tokene a vaše uložené ukazovatele na služby nastavte na null.

Tokeny RAII

Každá registrácia vracia token (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription, …) — presúvateľný BasicToken, ktorý vlastní registráciu: jeho zničením alebo zavolaním reset() sa príkaz, položka ponuky, panel či odber odvolá. Každý token si držte ako člen a v shutdown() ho uvoľnite. Dôležité sú dva prístupové prvky:

  • id() — nedeštruktívny pohľad typu TokenId pre rozhrania, ktoré položku adresujú bez toho, aby si ju prevzali (napr. IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — odpojí sa a vráti id; token už nič neodvoláva. Málokedy je to to, čo chcete.

Lokátor služieb

initialize() dostáva IPluginContext, lokátor služieb vlastnený hostiteľom. Kontext si za hranice initialize() neuchovávajte; uložte si jednotlivé odkazy na služby, ktoré potrebujete (každá služba prežije každý zásuvný modul).

Prístup Rozhranie Používa sa na
ctx.editor() IEditorService čítanie a úpravu bufferu, výber, kurzor, súbory, hľadanie, záložky, odsadenie, skupiny rozdeleného zobrazenia
ctx.commands() ICommandRegistry registráciu a vyvolávanie používateľských akcií (aj so skratkami)
ctx.menus() IMenuRegistry položky ponuky, oddeľovače, začiarkavacie prepínače, zosivenie položiek
ctx.docks() IDockRegistry ukotviteľné bočné panely
ctx.documents() IDocumentTypeRegistry plnohodnotné typy kariet hlavného okna (diff, zobrazenie logu, …)
ctx.compare() ICompareService otvorenie dvojtablového zobrazenia porovnania
ctx.diagnostics() IDiagnosticsSink zverejňovanie vlnoviek a značiek na okraji
ctx.events() IEventBus odoberanie a zverejňovanie udalostí editora
ctx.completions() ICompletionRegistry prispievanie kandidátmi na automatické dopĺňanie
ctx.contextMenu() IContextMenuRegistry dynamické položky v kontextovej ponuke editora
ctx.snippetSessions() ISnippetSessionService poskytovanie útržkov rozvíjateľných klávesom Tab (prácu s poľami vedie hostiteľ)
ctx.hover() IHoverRegistry bubliny pri najazdení
ctx.signatureHelp() ISignatureHelpRegistry bubliny volania pri ( a ,
ctx.symbols() ISymbolRegistry symboly dokumentu a pracovného priestoru (prechod na symbol)
ctx.languages() ILanguageRegistry výpis jazykov, čítanie a prepínanie jazyka dokumentu, prispievanie definíciou jazyka
ctx.statusBar() IStatusBar dočasné správy a trvalé segmenty
ctx.settings() ISettings trvalú konfiguráciu kľúč/hodnota pre daný zásuvný modul
ctx.appSettings() const ISettings& čítanie nastavení celej aplikácie (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings konfiguráciu dokumentu vo vrstvách so .editorconfig
ctx.settingsRegistry() ISettingsRegistry pridanie stránky do dialógu Predvoľby
ctx.themePalette() IThemePalette pomenované farby motívu, integráciu s Editorom motívov
ctx.lineMarkers() ILineMarkerService symboly na okraji a podfarbenie riadkov, ktoré sledujú úpravy (zarážky, pásy zmien); nárok na kliknutie na okraj
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path váš súkromný zapisovateľný priečinok
ctx.diagnosticsDir() std::filesystem::path zdieľaný priečinok s artefaktmi pádov a logmi (pre zásuvné moduly iba na čítanie)

Neskoršie rozšírenie API znamená pridanie nového rozhrania služby a prístupového prvku — čo je podľa pravidla verzií kompatibilný prírastok: existujúce zásuvné moduly sa naďalej načítavajú. Nástrojový reťazec, ktorému musí zodpovedať samotné zostavenie zásuvného modulu (prekladač, runtime, menšia verzia Qt), je uvedený v kapitole Písanie zásuvného modulu, časť „Požiadavky na zostavenie“.

IEditorService — model dokumentu

Editor tak, ako ho vidia zásuvné moduly — bez Scintilly, bez Qt. Dokumenty sa adresujú nepriehľadným DocumentId (std::uint64_t, 0 = neplatné). Posuny sú bajtové posuny v texte UTF-8; MatchRange je polootvorený rozsah {start, end}, pričom noMatch() znamená „nenájdené“; EditorPosition je {line, column} počítané od nuly.

// 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;

Sprístupňuje aj rozloženie skupín kariet v rozdelenom zobrazení v pojmoch nezávislých od backendu (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), takže zásuvný modul dokáže zachytiť a obnoviť usporiadanie tabiel bez toho, aby videl rozdeľovač z Qt. Úplný zoznam nájdete v PluginApi/IEditorService.h — hlavička je záväznou referenciou.

Dokument zobrazený v dvoch tablách je jeden dokument s jediným DocumentId, takže groupOf() dokáže pomenovať iba jedno z jeho miest. Rozdelenie opisujú a obnovujú dve metódy:

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

showDocumentInGroup() aktivuje zobrazenie, ktoré v danej table už je, a inak tam vytvorí druhé zdieľané zobrazenie — to isté, čo robí Zobraziť ▸ Rozdeliť dokument vpravo, takže sa nevyvolá DocumentOpened a obidve zobrazenia upravujú jeden dokument. Pojmom smerom k zásuvným modulom sú tably; jednotlivé zobrazenia nie, a preto volanie pomenúva tablu namiesto toho, aby vracalo zobrazenie.

Prenesenie používateľa na miesto

openFileAt(path, position) je jediné volanie za „prejsť na chybu zostavenia“, „skočiť na rámec zásobníka“ alebo „otvoriť výsledok hľadania“, takže všetky pristanú rovnako. Otvorí súbor presne ako openFile() (karta už otvoreného súboru sa aktivuje, neotvára sa druhýkrát), urobí z neho aktívny dokument, umiestni kurzor bez výberu, rozbalí zbalenie, ktoré riadok skrýva, posunie riadok do zobrazenia — na stred, ak bol mimo obrazovky, bez posunu, ak už bol viditeľný — a odovzdá editoru fokus klávesnice. Pozícia sa oreže, nikdy neodmietne: riadok za koncom skončí na poslednom riadku, stĺpec za koncom riadka na jeho konci, záporné hodnoty sa počítajú ako 0. Vracia dokument, v ktorom bol kurzor umiestnený, alebo 0, keď sa súbor nepodarilo otvoriť — a vtedy sa nič nepohlo.

revealLine(doc, line) ukáže miesto bez toho, aby tam používateľa preniesol: rozbalí a posunie tablo, ktoré zobrazuje doc, ale nepresunie kurzor ani výber, neprepne kartu a nezoberie fokus — používa ho panel, ktorý zobrazuje náhľad rámca zásobníka alebo sleduje svoj výber.

Kódovanie dokumentu

Kódovanie znakov dokumentu na disku je stavom jadra, nie zásuvného modulu. Hostiteľ ho rozpozná pri čítaní súboru a pri každom uložení zapíše späť to isté kódovanie, takže si súbor v UTF-8 so značkou BOM svoju značku ponechá a súbor v UTF-16 ostane v UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) je výpočtový typ POD nezávislý od backendu, ktorý pokrýva päť hlavných kódovaní Unicode a ANSI plus staršie kódové stránky.

setDocumentEncoding() zaznamená voľbu: nemení žiadne bajty a nič nedekóduje znova — buffer je už v Unicode a nové kódovanie sa prejaví pri ďalšom uložení. Zásuvný modul ponúkajúci akciu „previesť na“ by preto mal zavolať aj setModified(doc, true); modul ponúkajúci „interpretovať ako“ si najprv sám znova načíta text a potom voľbu zaznamená. Hostiteľ zverejní DocumentEncodingChanged vždy, keď sa hodnota naozaj zmení. Kľúč charset zo súboru .editorconfig projektu pri ukladaní naďalej vyhráva.

Konce riadkov dokumentu

Aj ukončovač riadkov dokumentu je stavom jadra. Hostiteľ ho rozpozná pri čítaní súboru, vloží ho do zobrazenia, aby Enter vkladal práve túto sekvenciu, a bajty bufferu zapíše späť doslovne — takže unixový súbor upravovaný vo Windows ostane s LF a uloženie bez úprav bude bajt po bajte rovnaké. LineEnding (PluginApi/LineEnding.h) je výpočtový typ POD nezávislý od backendu — Unknown / Lf / CrLf / Cr — pomenúvajúci sekvenciu bajtov, a nie konštantu backendu. Dodáva sa s dvoma vnorenými pomocníkmi: lineEndingBytes() a platformLineEnding() (to, s čím sa začína nový buffer: CRLF vo Windows, LF inde).

setLineEnding() zaznamená voľbu, preladí zobrazenie dokumentu tak, aby ju nasledujúci Enter vložil, a rozhodne o tom, čo zapíše ďalšie uloženie. Neprevádza žiadny existujúci text: zásuvný modul prepisujúci celý buffer (príkazy prevodu koncov riadkov) najprv sám nahradí text, takže prevod ostane jednou vrátiteľnou úpravou. Hostiteľ zverejní DocumentLineEndingChanged vždy, keď sa hodnota naozaj zmení. Kľúč end_of_line zo súboru .editorconfig projektu pri ukladaní naďalej vyhráva a dokument spolu s tým prepne na túto konvenciu.

Vlákna

Všetky volania kontextu a služieb prebiehajú vo vlákne rozhrania hostiteľa. Nevolajte služby z vlákna na pozadí, ktoré ste spustili; najprv sa vráťte späť.

Verzie

Kontrakt nesie dvojdielnu verziu, oba diely v PluginApi/PluginInfo.h:

constexpr std::uint32_t MTE_PLUGIN_API_VERSION       = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor

Medzi nimi rozhoduje to, kto rozhranie implementuje:

Zmena Druh Verzia
Metóda pridaná na koniec rozhrania, ktoré implementuje iba editor a moduly ho iba volajú (IEditorService, IPluginContext, každý I…Registry, ILineMarkerService, …) kompatibilná minor + 1
Nové rozhranie, nová štruktúra udalosti, nový enumerátor na konci výčtu kompatibilná minor + 1
Akákoľvek zmena štruktúry, ktorá prekračuje hranicu (PluginInfo, ContextMenuRequest, DockPanelDescriptor, štruktúra udalosti) nekompatibilná major + 1
Zmenená signatúra, odstránená alebo preradená metóda, nové preťaženie existujúceho názvu nekompatibilná major + 1
Akákoľvek zmena rozhrania, ktoré implementuje modul (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) nekompatibilná major + 1
Vydanie zostavené nástrojovým reťazcom, ktorého binárne rozhranie sa líši od predchádzajúceho — iná rodina prekladačov alebo štandardná knižnica, nová hlavná verzia Qt nekompatibilná major + 1

Novšia sada nástrojov tej istej rodiny prekladačov, iná úroveň štandardu C++ a novšia minor verzia Qt takým zlomom nie sú a nemenia ani jedno z čísel; čo musí natívny modul dodržať, pozri Požiadavky na zostavenie.

Editor načíta modul iba vtedy, keď

  • apiVersion v plugin.json aj IPlugin::info().apiVersion sa rovnajú jeho vlastnej major verzii, a
  • apiMinorVersion v plugin.json (voliteľné, predvolene 0) aj IPlugin::info().apiMinorVersion neprekračujú jeho vlastnú minor verziu. Hodnota v info() je predvolene minor verzia hlavičiek, voči ktorým bol modul skompilovaný; modul, ktorý vie, že nepoužíva nič novšie, smie v plugin.json uviesť nižšie číslo a aj tak musí prejsť druhou kontrolou.

Odmietnutý modul zostane uvedený v Predvoľby ▸ Zásuvné moduly s dôvodom („Nekompatibilné (nezhoda API)“, „Vyžaduje novšiu verziu aplikácie OctoEdit“) a dostane záznam v logu — nikdy pád. Modul v Pythone nemá info() skompilované voči hlavičkám, takže jeho plugin.json je jeho jediné vyhlásenie: uveďte minor verziu najnovšieho volania, ktoré používate.

Politika pred vydaním

Pred prvým verejným vydaním sa kontrakt môže ešte meniť bez zvýšenia ktoréhokoľvek z čísel; dovtedy obe zostávajú na 1 / 0. Od prvého vydania sa každá zmena posudzuje podľa tabuľky vyššie.