Rozhraní zásuvných modulů — přehled¶
Všechny typy PluginApi bydlí v namespace MTE::plugin. Veřejné hlavičky nesmí vkládat žádnou
hlavičku Qt, Scintilly ani vnitřní hlavičku MTE — jen standardní knihovnu. Jedinou záměrnou
výjimkou je volitelná složka PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h,
ThemeFormat.h) pro moduly, které si nesou vlastní widgety Qt.
Tentýž kontrakt naplňují dva backendy: nativní zavaděč Qt (sdílené knihovny) a backend v
Pythonu běžící mimo proces (zásuvné moduly v Pythonu). Tato část
dokumentuje plochu v C++; každá zdejší služba, která je vůči backendu neutrální, má svůj
protějšek na ctx v Pythonu.
Životní cyklus zásuvného modulu¶
Nativní modul implementuje jednu třídu:
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átit stejnéidaapiVersionjakoplugin.json; hostitel to porovnává a při neshodě odmítne načíst.initialize()běží jednou při spuštění (poté, co jsou nalezeny všechny moduly). Registrujte zde příkazy, nabídky, poskytovatele i panely;falsevracejte jen při skutečném selhání (hostitel modul uvolní a zaznamená to).shutdown()běží při ukončení (nebo když se modul uvolňuje). Nesmí vyhazovat výjimky; zavolejtereset()na každém tokenu a vynulujte uložené ukazatele na služby.
Tokeny RAII¶
Každá registrace vrací token (CommandToken, MenuToken, DockToken, SettingsPageToken,
Subscription, …) — pouze přesouvatelný BasicToken, který registraci vlastní: jeho
zničením nebo zavoláním reset() se příkaz, položka nabídky, panel či odběr odvolá. Každý token
si držte jako člen a uvolněte jej ve shutdown(). Záleží na dvou přístupových metodách:
id()— nedestruktivní pohledTokenIdpro rozhraní, která položku adresují, aniž by si ji vzala (napříkladIMenuRegistry::setItemChecked/setItemEnabled).release()— odpojí a vrátí id; token už nic neodvolává. Zřídkakdy to, co chcete.
Lokátor služeb¶
initialize() dostává IPluginContext, lokátor služeb vlastněný hostitelem. Kontext si neukládejte
za hranici initialize(); uložte si jednotlivé odkazy na služby, které potřebujete (každá služba
přežije každý modul).
| Přístupová metoda | Rozhraní | Použití pro |
|---|---|---|
ctx.editor() |
IEditorService |
čtení a úpravy bufferu, výběr, kurzor, soubory, hledání, záložky, odsazení, skupiny rozděleného zobrazení |
ctx.commands() |
ICommandRegistry |
registrace a vyvolávání uživatelských akcí (se zkratkami) |
ctx.menus() |
IMenuRegistry |
položky nabídek, oddělovače, zaškrtávací přepínače, zašednutí položek |
ctx.docks() |
IDockRegistry |
ukotvitelné postranní panely |
ctx.documents() |
IDocumentTypeRegistry |
plnohodnotné typy karet hlavního okna (diff, zobrazení protokolu, …) |
ctx.compare() |
ICompareService |
otevření dvoupanelového zobrazení porovnání |
ctx.diagnostics() |
IDiagnosticsSink |
publikování vlnovek a značek na okraji |
ctx.events() |
IEventBus |
odběr a publikování událostí editoru |
ctx.completions() |
ICompletionRegistry |
přispívání kandidátů automatického dokončování |
ctx.contextMenu() |
IContextMenuRegistry |
dynamické položky v místní nabídce editoru |
ctx.snippetSessions() |
ISnippetSessionService |
poskytování úryvků rozvíjených Tabem (práci s poli řídí hostitel) |
ctx.hover() |
IHoverRegistry |
nápovědy při najetí |
ctx.signatureHelp() |
ISignatureHelpRegistry |
tipy k volání na ( a , |
ctx.symbols() |
ISymbolRegistry |
symboly dokumentu a pracovního prostoru (přechod na symbol) |
ctx.languages() |
ILanguageRegistry |
výpis jazyků, čtení a přepínání jazyka dokumentu, přispění definicí jazyka |
ctx.statusBar() |
IStatusBar |
pomíjivé zprávy a trvalé části |
ctx.settings() |
ISettings |
trvalá konfigurace klíč/hodnota pro daný modul |
ctx.appSettings() |
const ISettings& |
čtení nastavení celé aplikace (AppSettingsKeys.h) |
ctx.documentSettings() |
IDocumentSettings |
konfigurace dokumentu s vrstvami z .editorconfig |
ctx.settingsRegistry() |
ISettingsRegistry |
přidání stránky do okna Předvolby |
ctx.themePalette() |
IThemePalette |
pojmenované barvy motivu, propojení s Editorem motivů |
ctx.lineMarkers() |
ILineMarkerService |
symboly na okraji a podbarvení řádků, které sledují úpravy (zarážky, pruhy změn); nárok na kliknutí na okraj |
ctx.log() |
ILogger |
trace / debug / info / warning / error |
ctx.pluginDataDir() |
std::filesystem::path |
váš soukromý zapisovatelný adresář |
ctx.diagnosticsDir() |
std::filesystem::path |
sdílený adresář s artefakty pádů a protokoly (pro moduly jen ke čtení) |
Pozdější rozšíření rozhraní znamená přidat nové rozhraní služby a přístupovou metodu — což je podle pravidla verzování kompatibilní přírůstek: stávající moduly se dál načítají. Sada nástrojů, které se musí sestavení modulu držet (překladač, runtime, podverze Qt), je uvedena v části Psaní zásuvného modulu, »Požadavky na sestavení«.
IEditorService — model dokumentu¶
Editor tak, jak jej vidí zásuvné moduly — žádná Scintilla, žádné Qt. Dokumenty se odkazují
neprůhledným DocumentId (std::uint64_t, 0 = neplatné). Posuny jsou bajtové posuny v
textu UTF-8; MatchRange je polootevřený {start, end}, kde noMatch() znamená »nenalezeno«;
EditorPosition je {line, column} počítané od 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;
Zpřístupňuje také rozvržení skupin karet v rozděleném zobrazení v pojmech neutrálních vůči
backendu (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(),
setGroupWeights()), takže modul může uspořádání panelů zachytit a obnovit, aniž by viděl
rozdělovač z Qt. Úplný seznam najdete v PluginApi/IEditorService.h — závaznou referencí je
hlavička.
Dokument zobrazený ve dvou panelech je jeden dokument s jedním DocumentId, takže
groupOf() může pojmenovat jen jedno z jeho míst. Rozdělení popisují a znovu sestavují dvě
metody:
std::vector<int> groupsOf(DocumentId doc) const; // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group); // put it in one more
showDocumentInGroup() aktivuje zobrazení, které už v daném panelu je, a jinak tam vytvoří druhé
sdílené zobrazení — totéž, co dělá Zobrazit ▸ Rozdělit dokument vpravo, takže se
nevyvolává žádné DocumentOpened a obě zobrazení upravují jeden dokument. Panely jsou pojmem
obráceným k modulům; jednotlivá zobrazení nikoli, a proto volání pojmenovává panel, místo aby
vracelo zobrazení.
Přenesení uživatele na místo¶
openFileAt(path, position) je jediné volání za »přejít na chybu
sestavení«, »skočit na rámec zásobníku« nebo »otevřít výsledek hledání«, takže
všechny přistanou stejně. Otevře soubor přesně jako openFile() (karta už
otevřeného souboru se aktivuje, neotevírá se podruhé), učiní ho aktivním
dokumentem, umístí kurzor bez výběru, rozbalí sbalení, které řádek skrývá,
posune řádek do zobrazení — na střed, pokud byl mimo obrazovku, bez
posunu, pokud už byl vidět — a předá editoru fokus klávesnice. Pozice se
ořízne, nikdy neodmítne: řádek za koncem skončí na posledním řádku,
sloupec za koncem řádku na jeho konci, záporné hodnoty se počítají jako 0.
Vrací dokument, ve kterém byl kurzor umístěn, nebo 0, když soubor nešlo
otevřít — a pak se nic nepohnulo.
revealLine(doc, line) ukáže místo, aniž by tam uživatele přenesl: rozbalí a
posune panel, který zobrazuje doc, ale nepřesune kurzor ani výběr,
nepřepne kartu a nevezme fokus — používá ho panel, který zobrazuje náhled
rámce zásobníku nebo sleduje svůj výběr.
Kódování dokumentu¶
Znakové kódování dokumentu na disku je stavem jádra, nikoli zásuvného modulu. Hostitel je
rozpozná při čtení souboru a při každém uložení zapíše totéž kódování zpět, takže si soubor s
UTF-8 BOM svou značku ponechá a soubor UTF-16 zůstane UTF-16. DocumentEncoding
(PluginApi/DocumentEncoding.h) je výčet typu POD nezávislý na backendu, pokrývající pět hlavních
kódování Unicode a ANSI plus starší kódové stránky.
setDocumentEncoding() zaznamenává volbu: nemění žádné bajty a nic znovu nedekóduje — buffer už
je v Unicode a nové kódování se projeví při dalším uložení. Modul nabízející akci »převést na« by
proto měl zavolat i setModified(doc, true); modul nabízející »interpretovat jako« si text
nejprve znovu načte sám a teprve pak volbu zaznamená. Hostitel publikuje
DocumentEncodingChanged, kdykoli se hodnota skutečně změní. Projektový .editorconfig s
charset při uložení stále vyhrává.
Konce řádků dokumentu¶
Zakončení řádků dokumentu je rovněž stavem jádra. Hostitel je rozpozná při čtení souboru,
vtlačí je do zobrazení, aby Enter vkládal tuto posloupnost, a bajty bufferu zapisuje doslova —
takže unixový soubor upravovaný ve Windows zůstane LF a uložení bez úprav je bajt po bajtu
shodné. LineEnding (PluginApi/LineEnding.h) je výčet typu POD nezávislý na backendu —
Unknown / Lf / CrLf / Cr — pojmenovávající posloupnost bajtů, nikoli konstantu backendu.
Přináší dva inline pomocníky: lineEndingBytes() a platformLineEnding() (s čím nový buffer
začíná: CRLF ve Windows, LF jinde).
setLineEnding() zaznamená volbu, přeladí zobrazení dokumentu, aby další Enter vložil ji, a
rozhodne, co zapíše další uložení. Nepřevádí žádný existující text: modul přepisující celý
buffer (příkazy Převod konců řádků) nejprve nahradí text sám, takže převod zůstane jednou
vratnou úpravou. Hostitel publikuje DocumentLineEndingChanged, kdykoli se hodnota skutečně
změní. Projektový .editorconfig s end_of_line při uložení stále vyhrává a dokument s ním na
tuto konvenci převede.
Vlákna
Všechna volání kontextu i služeb probíhají v uživatelském vlákně hostitele. Nevolejte služby z vlákna na pozadí, které jste sami spustili; nejprve se vraťte zpět.
Verzování¶
Kontrakt nese dvoudílnou verzi, oba díly 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
Mezi nimi rozhoduje to, kdo rozhraní implementuje:
| Změna | Druh | Verze |
|---|---|---|
Metoda přidaná na konec rozhraní, které implementuje jen editor a moduly ho jen volají (IEditorService, IPluginContext, každý I…Registry, ILineMarkerService, …) |
kompatibilní | minor + 1 |
| Nové rozhraní, nová struktura události, nový enumerátor na konci výčtu | kompatibilní | minor + 1 |
Jakákoli změna struktury, která překračuje hranici (PluginInfo, ContextMenuRequest, DockPanelDescriptor, struktura události) |
nekompatibilní | major + 1 |
| Změněná signatura, odstraněná nebo přeřazená metoda, nové přetížení existujícího názvu | nekompatibilní | major + 1 |
Jakákoli změna rozhraní, které implementuje modul (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) |
nekompatibilní | major + 1 |
| Vydání sestavené sadou nástrojů, jejíž binární rozhraní se liší od předchozí — jiná rodina překladačů nebo standardní knihovna, nová hlavní verze Qt | nekompatibilní | major + 1 |
Novější sada nástrojů téže rodiny překladačů, jiná úroveň standardu C++ a novější minor verze Qt takovým zlomem nejsou a nemění ani jedno z čísel; co musí nativní modul dodržet, viz Požadavky na sestavení.
Editor načte modul jen tehdy, když
apiVersionvplugin.jsoniIPlugin::info().apiVersionse rovnají jeho vlastní major verzi, aapiMinorVersionvplugin.json(volitelné, výchozí0) iIPlugin::info().apiMinorVersionnepřesahují jeho vlastní minor verzi. Hodnota vinfo()je ve výchozím stavu minor verze hlaviček, proti kterým byl modul zkompilován; modul, který ví, že nepoužívá nic novějšího, smí vplugin.jsonuvést nižší číslo a i tak musí projít druhou kontrolou.
Odmítnutý modul zůstane uveden v Předvolby ▸ Zásuvné moduly s důvodem
(»Nekompatibilní (neshoda API)«, »Vyžaduje novější verzi aplikace OctoEdit«)
a dostane záznam v protokolu — nikdy pád. Modul v Pythonu nemá info()
zkompilované proti hlavičkám, takže jeho plugin.json je jeho jediné
prohlášení: uveďte minor verzi nejnovějšího volání, které používáte.
Politika před vydáním
Před prvním veřejným vydáním se kontrakt může ještě měnit bez zvýšení
kteréhokoli z čísel; do té doby obě zůstávají na 1 / 0. Od prvního
vydání se každá změna posuzuje podle tabulky výše.