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 sameidiapiVersioncoplugin.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óćfalsetylko 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; wykonajreset()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 widokTokenId, 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
apiVersionwplugin.jsoniIPlugin::info().apiVersionsą równe jego własnej wersji major, orazapiMinorVersionwplugin.json(opcjonalne, domyślnie0) iIPlugin::info().apiMinorVersionnie przekraczają jego własnej wersji minor. Wartość winfo()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ć wplugin.jsonniż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.