Hyppää sisältöön

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 sama id ja apiVersion kuin plugin.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ä; palauta false vain 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; kutsu reset() 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() — tuhoamaton TokenId-näkymä API:ille, jotka osoittavat kohteeseen ottamatta sitä (esimerkiksi IMenuRegistry::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

  • apiVersion tiedostossa plugin.json ja IPlugin::info().apiVersion ovat yhtä suuria kuin sen oma major-versio, ja
  • apiMinorVersion tiedostossa plugin.json (valinnainen, oletus 0) ja IPlugin::info().apiMinorVersion eivä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 tiedostossa plugin.json pienemmä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.