Přeskočit obsah

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é id a apiVersion jako plugin.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; false vracejte 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; zavolejte reset() 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í pohled TokenId pro rozhraní, která položku adresují, aniž by si ji vzala (například IMenuRegistry::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ž

  • apiVersion v plugin.json i IPlugin::info().apiVersion se rovnají jeho vlastní major verzi, a
  • apiMinorVersion v plugin.json (volitelné, výchozí 0) i IPlugin::info().apiMinorVersion nepřesahují jeho vlastní minor verzi. Hodnota v info() 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í v plugin.json uvé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.