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éidaapiVersionakoplugin.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; hodnotufalsevráť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; zavolajtereset()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 typuTokenIdpre 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ď
apiVersionvplugin.jsonajIPlugin::info().apiVersionsa rovnajú jeho vlastnej major verzii, aapiMinorVersionvplugin.json(voliteľné, predvolene0) ajIPlugin::info().apiMinorVersionneprekračujú jeho vlastnú minor verziu. Hodnota vinfo()je predvolene minor verzia hlavičiek, voči ktorým bol modul skompilovaný; modul, ktorý vie, že nepoužíva nič novšie, smie vplugin.jsonuviesť 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.