Przejdź do treści

API wtyczek — przegląd

Wszystkie typy PluginApi żyją w namespace MTE::plugin. Publiczne nagłówki nie mogą dołączać żadnego nagłówka Qt, Scintilli ani wewnętrznego nagłówka MTE — tylko bibliotekę standardową. Jedynym celowym wyjątkiem jest opcjonalny folder PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) dla wtyczek, które dostarczają własne widżety Qt.

Ten sam kontrakt realizują dwa backendy: natywny loader Qt (biblioteki współdzielone) oraz działający poza procesem backend Pythona (Wtyczki w Pythonie). Ta sekcja dokumentuje powierzchnię C++; każda opisana tu usługa niezależna od backendu ma swój odpowiednik w Pythonie na ctx.

Cykl życia wtyczki

Wtyczka natywna implementuje jedną klasę:

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() musi zwrócić te same id i apiVersion co plugin.json; host weryfikuje zgodność i przy rozbieżności odmawia załadowania.
  • initialize() uruchamiane jest raz przy starcie (po wykryciu wszystkich wtyczek). Tutaj rejestruj polecenia, menu, providery i panele; zwróć false tylko przy faktycznym błędzie (host wyładuje wtyczkę i to zaloguje).
  • shutdown() uruchamiane jest przy wyjściu (lub gdy wtyczka jest wyładowywana). Nie może rzucać wyjątków; wykonaj reset() na każdym tokenie i wyzeruj przechowywane wskaźniki do usług.

Tokeny RAII

Każda rejestracja zwraca token (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription, …) — przenoszalny (move-only) BasicToken, który jest właścicielem rejestracji: zniszczenie go lub wywołanie reset() cofa rejestrację polecenia / pozycji menu / panelu / subskrypcji. Trzymaj każdy token jako pole składowe i zwalniaj go w shutdown(). Istotne są dwa akcesory:

  • id() — nieniszczący widok TokenId, dla API, które adresują element bez przejmowania go (np. IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — odłącza token i zwraca id; token nie cofa już żadnej rejestracji. Rzadko tego chcesz.

Lokator usług

initialize() otrzymuje IPluginContext — lokator usług, którego właścicielem jest host. Nie przechowuj kontekstu po initialize(); przechowuj referencje do poszczególnych potrzebnych usług (każda usługa żyje dłużej niż każda wtyczka).

Akcesor Interfejs Zastosowanie
ctx.editor() IEditorService odczyt/edycja bufora, zaznaczenie, karetka, pliki, wyszukiwanie, zakładki, wcięcia, grupy widoku podzielonego
ctx.commands() ICommandRegistry rejestrowanie / wywoływanie akcji użytkownika (ze skrótami)
ctx.menus() IMenuRegistry pozycje menu, separatory, przełączniki z zaznaczeniem, wygaszanie pozycji
ctx.docks() IDockRegistry dokowane panele boczne
ctx.documents() IDocumentTypeRegistry pełnoprawne typy kart MainWindow (diff, widok logów, …)
ctx.compare() ICompareService otwieranie dwupanelowego widoku porównania
ctx.diagnostics() IDiagnosticsSink publikowanie podkreśleń + glifów na marginesie
ctx.events() IEventBus subskrypcja / publikacja zdarzeń edytora
ctx.completions() ICompletionRegistry dostarczanie kandydatów auto-uzupełniania
ctx.contextMenu() IContextMenuRegistry dynamiczne pozycje w menu prawego przycisku edytora
ctx.snippetSessions() ISnippetSessionService dostarczanie snippetów rozwijanych Tabem (host prowadzi sesję pól)
ctx.hover() IHoverRegistry dymki podpowiedzi (hover)
ctx.signatureHelp() ISignatureHelpRegistry podpowiedzi wywołań przy ( / ,
ctx.symbols() ISymbolRegistry symbole dokumentu / przestrzeni roboczej (go-to-symbol)
ctx.languages() ILanguageRegistry lista języków, odczyt / przełączenie języka dokumentu, wniesienie definicji języka
ctx.statusBar() IStatusBar komunikaty tymczasowe + trwałe segmenty
ctx.settings() ISettings trwała konfiguracja klucz/wartość per wtyczka
ctx.appSettings() const ISettings& odczyt ustawień globalnych aplikacji (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings warstwowa konfiguracja per dokument z .editorconfig
ctx.settingsRegistry() ISettingsRegistry dodanie strony do okna Preferencji
ctx.themePalette() IThemePalette nazwane kolory motywu, integracja z edytorem motywów
ctx.lineMarkers() ILineMarkerService glify na marginesie i zabarwienie wierszy podążające za edycjami (punkty przerwania, paski zmian); przejęcie kliknięcia marginesu
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path Twój prywatny katalog z prawem zapisu
ctx.diagnosticsDir() std::filesystem::path wspólny katalog artefaktów awarii + logów (dla wtyczek tylko do odczytu)

Późniejsze rozszerzanie API oznacza dodanie nowego interfejsu usługi i akcesora — to zgodne rozszerzenie według reguły wersjonowania: istniejące wtyczki nadal się ładują. Toolchain, z którym musi być zgodny sam build wtyczki (kompilator, runtime, wydanie Qt), opisuje strona Pisanie wtyczki, sekcja „Wymagania do kompilacji".

IEditorService — model dokumentu

Edytor widziany oczami wtyczek — bez Scintilli, bez Qt. Dokumenty adresowane są nieprzezroczystym DocumentId (std::uint64_t, 0 = nieprawidłowy). Offsety to offsety bajtowe w tekście UTF-8; MatchRange to przedział półotwarty {start, end}, gdzie noMatch() oznacza „nie znaleziono"; EditorPosition to liczony od 0 {line, column}.

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

Udostępnia też układ grup kart widoku podzielonego w terminach niezależnych od backendu (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), więc wtyczka może zapisać i odtworzyć rozmieszczenie paneli, nie widząc splittera Qt. Pełna lista w PluginApi/IEditorService.h — nagłówek jest referencją rozstrzygającą.

Dokument pokazany w dwóch panelach to jeden dokument z jednym DocumentId, więc groupOf() potrafi nazwać tylko jedno z jego miejsc. Podział opisują i odtwarzają dwie metody:

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

showDocumentInGroup() aktywuje widok już obecny w danym panelu, a w przeciwnym razie tworzy tam drugi współdzielony widok — dokładnie to, co robi Widok ▸ Podziel dokument w prawo — więc nie pada żadne DocumentOpened, a oba widoki edytują jeden dokument. Panele są pojęciem dostępnym dla wtyczek; pojedyncze widoki nie są, dlatego wywołanie wskazuje panel, zamiast zwracać uchwyt widoku.

Zabranie użytkownika w dane miejsce

openFileAt(path, position) to jedno wywołanie stojące za „przejdź do błędu kompilacji", „skocz do ramki stosu" czy „otwórz wynik wyszukiwania", więc wszystkie lądują tak samo. Otwiera plik dokładnie tak jak openFile() (karta otwartego pliku jest aktywowana, a nie otwierana drugi raz), czyni go aktywnym dokumentem, stawia kursor bez zaznaczenia, rozwija zwinięty fragment, który ukrywa wiersz, przewija wiersz do widoku — na środek, gdy był poza ekranem, bez przewijania, gdy już był widoczny — i daje edytorowi fokus klawiatury. Pozycja jest przycinana, nigdy odrzucana: wiersz za końcem trafia na ostatni wiersz, kolumna za końcem wiersza na jego koniec, wartości ujemne liczą się jako 0. Zwraca dokument, w którym postawiono kursor, albo 0, gdy pliku nie udało się otworzyć — i wtedy nic się nie przesunęło.

revealLine(doc, line) pokazuje miejsce, nie zabierając tam użytkownika: rozwija i przewija panel, który pokazuje doc, ale nie przesuwa kursora ani zaznaczenia, nie przełącza kart i nie zabiera fokusu — używa go panel, który podgląda ramkę stosu albo podąża za swoim zaznaczeniem.

Kodowanie dokumentu

Kodowanie znaków dokumentu na dysku to stan rdzenia, a nie wtyczki. Host wykrywa je przy odczycie pliku i zapisuje z powrotem to samo kodowanie przy każdym zapisie, dzięki czemu plik UTF-8-BOM zachowuje swój znacznik, a plik UTF-16 pozostaje w UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) to niezależny od backendu typ POD obejmujący pięć podstawowych kodowań Unicode/ANSI oraz strony kodowe starszego typu.

setDocumentEncoding() zapisuje wybór: nie zmienia żadnych bajtów i niczego nie dekoduje ponownie — bufor już jest w Unicode, a nowe kodowanie zadziała przy następnym zapisie. Wtyczka oferująca akcję „konwertuj na” powinna więc wywołać także setModified(doc, true); wtyczka oferująca „interpretuj jako” najpierw sama wczytuje tekst ponownie, a potem zapisuje wybór. Host publikuje DocumentEncodingChanged zawsze, gdy wartość faktycznie się zmieni. Ustawienie charset w projektowym .editorconfig nadal ma pierwszeństwo przy zapisie.

Zakończenia wierszy dokumentu

Zakończenie wiersza dokumentu też jest stanem rdzenia. Host rozpoznaje je przy odczycie pliku, przekazuje do widoku, aby Enter wstawiał właśnie tę sekwencję, i zapisuje bajty bufora dosłownie — dzięki czemu plik uniksowy edytowany na Windowsie pozostaje LF, a zapis bez zmian jest identyczny co do bajtu. LineEnding (PluginApi/LineEnding.h) to niezależny od backendu enum POD — Unknown / Lf / CrLf / Cr — nazywający sekwencję bajtów, a nie stałą backendu. Dostarcza dwa pomocniki inline: lineEndingBytes() oraz platformLineEnding() (od czego zaczyna nowy bufor: CRLF na Windowsie, LF gdzie indziej).

setLineEnding() zapisuje wybór, przestraja widok dokumentu tak, by kolejny Enter wstawił nową sekwencję, i decyduje, co zapisze następny zapis. Nie konwertuje istniejącego tekstu: wtyczka przepisująca cały bufor (polecenia Konwersji końca linii) sama najpierw podmienia tekst, dzięki czemu konwersja pozostaje jedną cofalną edycją. Host publikuje DocumentLineEndingChanged zawsze, gdy wartość naprawdę się zmieni. Projektowy end_of_line w .editorconfig nadal wygrywa przy zapisie i przestawia razem z sobą dokument na tę konwencję.

Wątki

Wszystkie wywołania kontekstu i usług odbywają się w wątku UI hosta. Nie wywołuj usług z uruchomionego przez siebie wątku w tle; najpierw wróć do wątku UI.

Wersjonowanie

Kontrakt ma dwuczęściową wersję, obie części w PluginApi/PluginInfo.h:

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

O tym, którą podbić, decyduje to, kto implementuje interfejs:

Zmiana Rodzaj Wersja
Metoda dopisana na końcu interfejsu, który implementuje tylko edytor, a wtyczki go tylko wywołują (IEditorService, IPluginContext, każdy I…Registry, ILineMarkerService, …) zgodna minor + 1
Nowy interfejs, nowa struktura zdarzenia, nowy enumerator na końcu enuma zgodna minor + 1
Dowolna zmiana struktury przechodzącej przez granicę (PluginInfo, ContextMenuRequest, DockPanelDescriptor, struktura zdarzenia) niezgodna major + 1
Zmieniona sygnatura, usunięta lub przestawiona metoda, nowe przeciążenie istniejącej nazwy niezgodna major + 1
Dowolna zmiana interfejsu, który implementuje wtyczka (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) niezgodna major + 1
Wydanie zbudowane narzędziami o innym interfejsie binarnym niż poprzednie — inna rodzina kompilatora lub biblioteka standardowa, nowa główna wersja Qt niezgodna major + 1

Nowszy zestaw narzędzi tej samej rodziny kompilatora, inny poziom standardu C++ i nowsza wersja minor Qt nie są taką zmianą i nie ruszają żadnej z liczb; zobacz Wymagania do kompilacji, żeby sprawdzić, do czego musi pasować wtyczka natywna.

Edytor ładuje wtyczkę tylko wtedy, gdy

  • apiVersion w plugin.json i IPlugin::info().apiVersion są równe jego własnej wersji major, oraz
  • apiMinorVersion w plugin.json (opcjonalne, domyślnie 0) i IPlugin::info().apiMinorVersion nie przekraczają jego własnej wersji minor. Wartość w info() domyślnie równa się minor nagłówków, z którymi wtyczkę skompilowano; wtyczka, która wie, że nie używa niczego nowszego, może podać w plugin.json niższą liczbę, a i tak musi przejść drugie sprawdzenie.

Odrzucona wtyczka pozostaje na liście w Preferencje ▸ Wtyczki z podanym powodem („Niezgodna (niezgodność API)", „Wymaga nowszej wersji OctoEdit") i dostaje wpis w logu — nigdy awarię. Wtyczka w Pythonie nie ma info() skompilowanego z nagłówkami, więc jej plugin.json jest jedyną deklaracją: podaj minor najnowszego wywołania, którego używasz.

Polityka przedpremierowa

Przed pierwszym publicznym wydaniem kontrakt może się jeszcze zmieniać bez podbijania którejkolwiek z liczb; do tego czasu obie pozostają na 1 / 0. Od pierwszego wydania każdą zmianę ocenia się według powyższej tabeli.