Liitännäis-API — yleiskuva¶
Kaikki PluginApi-tyypit sijaitsevat nimiavaruudessa MTE::plugin. Julkiset
otsikkotiedostot eivät saa sisällyttää mitään Qt-, Scintilla- tai MTE:n sisäistä
otsikkotiedostoa — vain standardikirjaston. Yksi tarkoituksellinen poikkeus on valinnainen
PluginApi/Qt/-kansio (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h)
liitännäisille, jotka toimittavat omia Qt-ohjausobjektejaan.
Saman sopimuksen toteuttaa kaksi taustajärjestelmää: natiivi Qt-lataaja (jaetut
kirjastot) ja prosessin ulkopuolinen Python-taustajärjestelmä
(Python-liitännäiset). Tämä osio dokumentoi C++-pinnan;
jokaisella täällä olevalla taustajärjestelmäneutraalilla palvelulla on Python-vastineensa
kohteessa ctx.
Liitännäisen elinkaari¶
Natiivi liitännäinen toteuttaa yhden luokan:
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()täytyy palauttaa samaidjaapiVersionkuinplugin.json; isäntä ristiintarkistaa ne ja kieltäytyy lataamasta ristiriidan sattuessa.initialize()ajetaan kerran käynnistyksessä (kun jokainen liitännäinen on löydetty). Rekisteröi komennot, valikot, toimittajat ja paneelit täällä; palautafalsevain todellisen virheen kohdalla (isäntä poistaa silloin liitännäisen ja kirjaa sen).shutdown()ajetaan poistuttaessa (tai kun liitännäistä ollaan poistamassa). Se ei saa heittää poikkeusta; kutsureset()jokaiselle poletille ja nollaa tallennetut palveluosoittimesi.
RAII-poletit¶
Kukin rekisteröinti palauttaa poletin (CommandToken, MenuToken, DockToken,
SettingsPageToken, Subscription …) — vain siirrettävän BasicToken-olion, joka
omistaa rekisteröinnin: sen tuhoaminen tai funktion reset() kutsuminen peruu komennon,
valikkokohdan, paneelin tai tilauksen. Pidä kutakin polettia jäsenenä ja vapauta se
funktiossa shutdown(). Kaksi käyttäjää on tärkeitä:
id()— tuhoamatonTokenId-näkymä API:ille, jotka osoittavat kohteeseen ottamatta sitä (esimerkiksiIMenuRegistry::setItemChecked/setItemEnabled).release()— irrottaa ja palauttaa tunnisteen; poletti ei enää peru mitään. Harvoin se, mitä haluat.
Palveluhakemisto¶
initialize() vastaanottaa IPluginContext-olion, isännän omistaman palveluhakemiston.
Älä säilytä kontekstia funktion initialize() jälkeen; säilytä ne yksittäiset
palveluviittaukset, joita tarvitset (kukin palvelu elää jokaisen liitännäisen yli).
| Käyttäjä | Rajapinta | Käytä |
|---|---|---|
ctx.editor() |
IEditorService |
puskurin, valinnan, kohdistimen, tiedostojen, haun, kirjanmerkkien, sisennyksen ja jaetun näkymän ryhmien lukemiseen ja muokkaamiseen |
ctx.commands() |
ICommandRegistry |
käyttäjätoimintojen rekisteröintiin ja kutsumiseen (pikanäppäimineen) |
ctx.menus() |
IMenuRegistry |
valikkokohtiin, erottimiin, valittaviin kytkimiin ja kohtien harmaannuttamiseen |
ctx.docks() |
IDockRegistry |
telakoitaviin sivupaneeleihin |
ctx.documents() |
IDocumentTypeRegistry |
pääikkunan täysivaltaisiin välilehtityyppeihin (vertailu, lokinäkymä …) |
ctx.compare() |
ICompareService |
kahden ruudun vertailunäkymän avaamiseen |
ctx.diagnostics() |
IDiagnosticsSink |
aaltoviivojen ja marginaalimerkkien julkaisemiseen |
ctx.events() |
IEventBus |
editoritapahtumien tilaamiseen ja julkaisemiseen |
ctx.completions() |
ICompletionRegistry |
automaattisen täydennyksen ehdokkaiden tarjoamiseen |
ctx.contextMenu() |
IContextMenuRegistry |
dynaamisiin kohtiin editorin hiiren oikean napin valikossa |
ctx.snippetSessions() |
ISnippetSessionService |
Tabilla laajennettavien koodinpätkien tarjoamiseen (isäntä ajaa kenttäistunnon) |
ctx.hover() |
IHoverRegistry |
osoitusvihjeisiin |
ctx.signatureHelp() |
ISignatureHelpRegistry |
kutsuvihjeisiin merkkien ( / , kohdalla |
ctx.symbols() |
ISymbolRegistry |
asiakirjan ja työtilan symboleihin (siirry symboliin) |
ctx.languages() |
ILanguageRegistry |
kielten luetteloimiseen, asiakirjan kielen lukemiseen ja vaihtamiseen sekä kielimäärityksen tarjoamiseen |
ctx.statusBar() |
IStatusBar |
ohimeneviin viesteihin ja pysyviin osioihin |
ctx.settings() |
ISettings |
liitännäiskohtaiseen pysyvään avain–arvo-kokoonpanoon |
ctx.appSettings() |
const ISettings& |
sovelluksenlaajuisten asetusten lukemiseen (AppSettingsKeys.h) |
ctx.documentSettings() |
IDocumentSettings |
asiakirjakohtaiseen kokoonpanoon .editorconfig-kerroksineen |
ctx.settingsRegistry() |
ISettingsRegistry |
sivun lisäämiseen asetusikkunaan |
ctx.themePalette() |
IThemePalette |
nimettyihin teemaväreihin ja Teemaeditorin integraatioon |
ctx.lineMarkers() |
ILineMarkerService |
muokkauksia seuraaviin marginaalisymboleihin ja rivien sävytyksiin (keskeytyskohdat, muutospalkit); marginaalin napsautuksen varaamiseen |
ctx.log() |
ILogger |
trace / debug / info / warning / error |
ctx.pluginDataDir() |
std::filesystem::path |
omaan yksityiseen kirjoitettavaan hakemistoosi |
ctx.diagnosticsDir() |
std::filesystem::path |
jaettuun kaatumis- ja lokihakemistoon (liitännäisille vain luettava) |
API:n laajentaminen myöhemmin tarkoittaa uuden palvelurajapinnan ja käyttäjän lisäämistä — yhteensopiva lisäys versiointisäännön mukaan: olemassa olevat liitännäiset latautuvat edelleen. Se työkaluketju, jota liitännäiskäännöksen itsensä täytyy vastata (kääntäjä, ajonaikainen kirjasto, Qt:n alaversio), on lueteltu kohdassa Liitännäisen kirjoittaminen, »Käännösvaatimukset«.
IEditorService — asiakirjamalli¶
Editori sellaisena kuin liitännäiset sen näkevät — ei Scintillaa, ei Qt:tä. Asiakirjoihin
viitataan läpinäkymättömällä tunnisteella DocumentId (std::uint64_t, 0 = virheellinen).
Siirtymät ovat tavusiirtymiä UTF-8-tekstissä; MatchRange on puoliavoin
{start, end}, jossa noMatch() tarkoittaa »ei löytynyt«; EditorPosition on nollasta
alkava {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;
Se paljastaa myös jaetun näkymän välilehtiryhmien asettelun taustajärjestelmäneutraalisti
(groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(),
setGroupWeights()), jotta liitännäinen voi tallentaa ja palauttaa ruutujen järjestelyn
näkemättä Qt:n jakajaa. Katso koko luettelo tiedostosta PluginApi/IEditorService.h —
otsikkotiedosto on ratkaiseva viite.
Kahdessa ruudussa näkyvä asiakirja on yksi asiakirja, jolla on yksi DocumentId, joten
groupOf() voi nimetä vain toisen sen paikoista. Kaksi metodia kuvaa ja rakentaa jaon
uudelleen:
std::vector<int> groupsOf(DocumentId doc) const; // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group); // put it in one more
showDocumentInGroup() aktivoi näkymän, joka on jo tuossa ruudussa, ja luo muutoin sinne
toisen jaetun näkymän — saman kuin Näytä ▸ Jaa asiakirja oikealle tekee, joten
DocumentOpened-tapahtumaa ei nosteta ja molemmat näkymät muokkaavat yhtä asiakirjaa.
Ruudut ovat se käsite, jonka liitännäiset näkevät; yksittäiset näkymät eivät ole, ja siksi
kutsu nimeää ruudun sen sijaan että antaisi näkymän takaisin.
Käyttäjän vieminen jonnekin¶
openFileAt(path, position) on se yksi kutsu, jonka takana ovat »siirry
käännösvirheeseen«, »hyppää pinokehykseen« ja »avaa hakutulos«, joten ne
kaikki päätyvät perille samalla tavalla. Se avaa tiedoston täsmälleen kuten
openFile() (jo avoimen tiedoston välilehti aktivoidaan, sitä ei avata
toista kertaa), tekee siitä aktiivisen asiakirjan, sijoittaa kohdistimen
ilman valintaa, avaa rivin piilottavan laskostuksen, vierittää rivin näkyviin
— keskitettynä, jos se oli näytön ulkopuolella, vierittämättä, jos se oli
jo näkyvissä — ja antaa editorille näppäimistön kohdistuksen. Sijainti
rajataan, sitä ei koskaan hylätä: lopun yli menevä rivi päätyy
viimeiselle riville, rivin lopun yli menevä sarake rivin loppuun, ja
negatiiviset arvot lasketaan arvoksi 0. Se palauttaa asiakirjan, johon
kohdistin sijoitettiin, tai 0, jos tiedostoa ei voitu avata — ja silloin
mikään ei liikkunut.
revealLine(doc, line) näyttää kohdan viemättä käyttäjää sinne: se avaa
laskostuksen ja vierittää ruutua, joka näyttää asiakirjan doc, mutta ei
siirrä kohdistinta tai valintaa, ei vaihda välilehteä eikä ota kohdistusta —
sitä käyttää paneeli, joka esikatselee pinokehystä tai seuraa omaa
valintaansa.
Asiakirjan merkistökoodaus¶
Asiakirjan levyllä oleva merkistökoodaus on ytimen tilaa eikä liitännäisen. Isäntä
tunnistaa sen, kun tiedosto luetaan, ja kirjoittaa saman koodauksen takaisin joka
tallennuksella, joten UTF-8-BOM-tiedosto säilyttää merkkinsä ja UTF-16-tiedosto pysyy
UTF-16:na. DocumentEncoding (PluginApi/DocumentEncoding.h) on
taustajärjestelmästä riippumaton POD-luettelotyyppi, joka kattaa viisi ensisijaista
Unicode- ja ANSI-koodausta sekä vanhemmat koodisivut.
setDocumentEncoding() kirjaa valinnan: se ei muuta tavuja eikä pura mitään uudelleen —
puskuri on jo Unicodea, ja uusi koodaus tulee voimaan seuraavalla tallennuksella.
Liitännäisen, joka tarjoaa »muunna muotoon« -toiminnon, tulisi siksi kutsua myös
setModified(doc, true); sellainen, joka tarjoaa »tulkitse muodossa«, lataa tekstin itse
ensin uudelleen ja kirjaa sitten valinnan. Isäntä julkaisee tapahtuman
DocumentEncodingChanged aina, kun arvo todella muuttuu. Projektin .editorconfig, jossa
on charset, voittaa silti tallennushetkellä.
Asiakirjan rivinvaihdot¶
Asiakirjan rivinpäätin on myös ytimen tilaa. Isäntä tunnistaa sen, kun tiedosto luetaan,
työntää sen näkymään, jotta Enter lisää tuota sarjaa, ja kirjoittaa puskurin tavut takaisin
sanatarkasti — joten Windowsissa muokattu Unix-tiedosto pysyy LF-muotoisena ja tallennus
ilman muutoksia on tavulleen sama. LineEnding (PluginApi/LineEnding.h) on
taustajärjestelmästä riippumaton POD-luettelotyyppi — Unknown / Lf / CrLf / Cr —
joka nimeää tavusarjan eikä taustajärjestelmän vakiota. Se toimittaa kaksi inline-apuria:
lineEndingBytes() ja platformLineEnding() (se, millä uusi puskuri alkaa: CRLF
Windowsissa, LF muualla).
setLineEnding() kirjaa valinnan, virittää asiakirjan näkymän niin, että seuraava Enter
lisää sen, ja päättää, mitä seuraava tallennus kirjoittaa. Se ei muunna olemassa olevaa
tekstiä: liitännäinen, joka kirjoittaa koko puskurin uudelleen (rivinvaihtojen
muunnoskomennot), korvaa tekstin itse ensin, jotta muunnos pysyy yhtenä kumottavana
muokkauksena. Isäntä julkaisee tapahtuman DocumentLineEndingChanged aina, kun arvo todella
muuttuu. Projektin .editorconfig, jossa on end_of_line, voittaa silti tallennushetkellä
ja siirtää asiakirjan tuohon käytäntöön mukanaan.
Säikeet
Kaikki konteksti- ja palvelukutsut tapahtuvat isännän käyttöliittymäsäikeessä. Älä kutsu palveluja käynnistämästäsi taustasäikeestä; siirry ensin takaisin.
Versiointi¶
Sopimuksella on kaksiosainen versio, molemmat osat tiedostossa
PluginApi/PluginInfo.h:
constexpr std::uint32_t MTE_PLUGIN_API_VERSION = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor
Niiden välillä ratkaisee se, kuka rajapinnan toteuttaa:
| Muutos | Laji | Versio |
|---|---|---|
Metodi lisättynä sellaisen rajapinnan loppuun, jonka vain editori toteuttaa ja jota liitännäiset vain kutsuvat (IEditorService, IPluginContext, jokainen I…Registry, ILineMarkerService, …) |
yhteensopiva | minor + 1 |
| Uusi rajapinta, uusi tapahtumarakenne, uusi luettelotyypin arvo luettelon loppuun | yhteensopiva | minor + 1 |
Mikä tahansa muutos rakenteeseen, joka ylittää rajan (PluginInfo, ContextMenuRequest, DockPanelDescriptor, tapahtumarakenne) |
yhteensopimaton | major + 1 |
| Muutettu allekirjoitus, poistettu tai uudelleen järjestetty metodi, olemassa olevan nimen uusi ylikuormitus | yhteensopimaton | major + 1 |
Mikä tahansa muutos rajapintaan, jonka liitännäinen toteuttaa (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) |
yhteensopimaton | major + 1 |
| Julkaisu, joka on käännetty työkaluketjulla, jonka binäärirajapinta eroaa edellisestä — toinen kääntäjäperhe tai vakiokirjasto, Qt:n uusi pääversio | yhteensopimaton | major + 1 |
Saman kääntäjäperheen uudempi työkalusarja, C++-standardin eri taso ja Qt:n uudempi minor-versio eivät ole tällainen katkos eivätkä muuta kumpaakaan lukua; katso Käännösvaatimukset siitä, mitä natiivin liitännäisen on vastattava.
Editori lataa liitännäisen vain, kun
apiVersiontiedostossaplugin.jsonjaIPlugin::info().apiVersionovat yhtä suuria kuin sen oma major-versio, jaapiMinorVersiontiedostossaplugin.json(valinnainen, oletus0) jaIPlugin::info().apiMinorVersioneivät ylitä sen omaa minor-versiota.info()-funktion arvo on oletuksena niiden otsakkeiden minor-versio, joita vasten liitännäinen käännettiin; liitännäinen, joka tietää, ettei se käytä mitään uudempaa, voi ilmoittaa tiedostossaplugin.jsonpienemmän luvun, ja sen on silti läpäistävä toinen tarkistus.
Hylätty liitännäinen pysyy luettelossa kohdassa Asetukset ▸ Liitännäiset
syyn kanssa (»Yhteensopimaton (API-ristiriita)«, »Vaatii uudemman
OctoEdit-version«) ja saa lokimerkinnän — ei koskaan kaatumista.
Python-liitännäisellä ei ole otsakkeita vasten käännettyä info()-funktiota,
joten sen plugin.json on sen ainoa ilmoitus: ilmoita uusimman käyttämäsi
kutsun minor-versio.
Julkaisua edeltävä käytäntö
Ennen ensimmäistä julkista julkaisua sopimus voi vielä muuttua nostamatta
kumpaakaan lukua; siihen asti molemmat pysyvät arvoissa 1 / 0.
Ensimmäisestä julkaisusta alkaen jokainen muutos arvioidaan yllä olevan
taulukon mukaan.