Hyppää sisältöön

Kieliäly (diagnostiikka, osoitusvihjeet, allekirjoitusohje, symbolit)

Pieni, taustajärjestelmäneutraali sopimusperhe kieliälylle — sille datalle, jota toimittaja syöttää editorin koodiälypinnoille. Se noudattaa projektin sääntöä »liitännäiset tarjoavat dataa, ydin piirtää«: aaltoviivat, osoitusponnahdusikkuna ja kohdistinhypyt sijaitsevat editorinäkymässä eikä niitä koskaan paljasteta liitännäisille; liitännäinen toimittaa vain POD- ja std::string-dataa, jotta sama sopimus voidaan myöhemmin toteuttaa Python-taustajärjestelmällä.

Ensimmäinen kuluttaja on LSP-liitännäinen, mutta kanavat ovat yleisiä — staattinen analysaattori tai oikolukija voi julkaista diagnostiikkaa samalla tavalla.

Mitä täällä ei ole

Siirry määrittelyyn, etsi viittaukset, nimeä uudelleen ja muotoilu eivät tarvitse omaa API:ta — ne vain navigoivat tai muokkaavat tekstiä, jonka IEditorService jo ilmaisee (caret, openFile, setCaret, setText), joten ne pysyvät liitännäisen sisäisinä komentoina. Täydennys käyttää olemassa olevaa rajapintaa ICompletionRegistry. Uusi sopimus lisätään vain, kun ytimessä on piirto- tai valintapinta ja mahdollisesti useita toimittajia — mikä pätee diagnostiikkaan, osoitusvihjeisiin, allekirjoitusohjeeseen ja symboleihin.

Diagnostiikka — työntö

Toimittaja työntää diagnostiikkaa, kun sillä on sitä (se päättää, milloin tulokset ovat valmiit); editori piirtää ne aaltoviiva-alleviivauksina. Vastaanottaja tavoitetaan funktiolla IPluginContext::diagnostics(). Arvotyypit ovat tavallista POD-dataa (PluginApi/DiagnosticTypes.h):

enum class DiagnosticSeverity { Error, Warning, Information, Hint };

struct Diagnostic {
    MatchRange         range;   // UTF-8 byte range in the document
    DiagnosticSeverity severity = DiagnosticSeverity::Error;
    std::string        message;
    std::string        code;
};

struct IDiagnosticsSink {       // PluginApi/IDiagnosticsSink.h
    virtual void publishDiagnostics(DocumentId doc, std::string_view source,
                                    const std::vector<Diagnostic>& diags) = 0;
    virtual void clearDiagnostics(DocumentId doc) = 0;
};
  • source on korvausavain paria (asiakirja, lähde) kohti. Julkaiseminen lähdettä varten korvaa tuon lähteen aiemman diagnostiikan asiakirjassa, joten useat tuottajat (LSP, oikoluku, käännösloki) elävät rinnakkain sotkematta toistensa tuloksia.
  • Julkaise tyhjä taulukko lähteelle vetääksesi sen diagnostiikan takaisin.
  • Vakavuusasteet piirretään eri väreillä (virhe, varoitus, tieto, vihje); editori myös levittää hyvin lyhyitä alueita, jotta yhden merkin merkintä pysyy näkyvänä.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Isäntä tarjoaa aina kelvollisen vastaanottajan (tyhjän toteutuksen, kun editorilla ei ole diagnostiikkapintaa), joten sinun ei koskaan tarvitse tarkistaa, onko diagnostics() null.

Osoitusvihjeet — asynkroninen veto

Osoitusvihjeitä ohjaa editori (hiiri viipyy symbolin päällä). Liitännäinen rekisteröi toimittajan, joka vastaa asynkronisesti; rekisteri tavoitetaan funktiolla IPluginContext::hover() (PluginApi/IHoverRegistry.h):

struct HoverRequest { DocumentId doc; std::int64_t position; }; // byte offset
using  HoverReply  = std::function<void(std::string markup)>;   // empty = nothing

struct IHoverProvider {
    virtual void requestHover(const HoverRequest&, HoverReply reply) = 0;
};

struct IHoverRegistry {
    virtual HoverToken registerProvider(IHoverProvider& provider,
                                        std::int32_t    priority) = 0;
};

namespace hover_priority {
    inline constexpr std::int32_t kFallback      = 0;
    inline constexpr std::int32_t kLanguage      = 100;
    inline constexpr std::int32_t kActiveSession = 1000;
}
  • registerProvider palauttaa olion HoverToken (RAII); tuhoa se funktiossa shutdown() poistaaksesi rekisteröinnin.
  • Jokainen toimittaja rekisteröityy prioriteetilla. Valitse kaista sen mukaan, mitä vastaat, älä sen mukaan, miltä tärkeys tuntuu: kFallback sana- tai tekstitason tiedolle (sanakirjan määritelmä, URL:n kohde), kLanguage kielitason tiedolle symbolista (kielipalvelimen hover, linterin sääntö), kActiveSession tiedolle, jonka täytyy voittaa niin kauan kuin se on aktiivinen (osoittimen alla oleva arvo debuggerissa). Kaistojen väliset arvot (kLanguage + 10) ovat sallittuja, kun sinun on järjestettävä itsesi tunnetun naapurin suhteen.
  • Viipyessä isäntä kysyy kaikilta toimittajilta kerralla ja näyttää korkeimman prioriteetin toimittajan ei-tyhjän vastauksen; saman prioriteetin kesken voittaa ensin rekisteröity toimittaja. Ensimmäisenä saapuva matalamman prioriteetin vastaus pidätetään, kunnes jokainen korkeampi toimittaja on vastannut tyhjää. Kun kaikki vastaavat tyhjää, mitään ei näytetä.
  • Hidas toimittaja ei voi pidättää työkaluvihjettä ikuisesti: 300 ms pyynnön jälkeen näytetään paras siihen mennessä saatu vastaus, ja myöhemmät vastaukset tuohon pyyntöön hylätään. kActiveSession-toimittajan on siksi vastattava tyhjää heti, kun sillä ei ole sanottavaa (ei istuntoa) — muuten se viivästyttää jokaista editorin hoveria enintään aikakatkaisun verran.
  • Vastaukset, jotka saapuvat käyttäjän jo siirryttyä eteenpäin, hylätään.
  • Vastaus on tavallista merkkaustekstiä ja se piirretään Markdownina ponnahdusikkunassa, joten otsikot, lihavointi ja koodilohkot toimivat — pidä sopimus muodossa merkkijono sisään, merkkijono ulos.
class MyHover final : public IHoverProvider {
public:
    void requestHover(const HoverRequest& r, HoverReply reply) override {
        // ...look up info for r.doc at byte offset r.position, possibly async...
        reply("**foo** — a thing\n\n```cpp\nint foo();\n```");
    }
};
// in initialize():
//   m_token = ctx.hover().registerProvider(m_hover, hover_priority::kLanguage);

Allekirjoitusohje — asynkroninen veto

Kuten osoitusvihjeet, mutta sen ohjaa kutsun (-merkin tai ,-merkin kirjoittaminen. Toimittaja vastaa aktiivisella allekirjoituksella; isäntä piirtää sen kutsuvihjeenä aktiivinen parametri korostettuna. Rekisteri tavoitetaan funktiolla IPluginContext::signatureHelp() (PluginApi/ISignatureHelpRegistry.h):

struct SignatureHelpRequest { DocumentId doc; std::int64_t position; std::string languageId; };
struct SignatureHelp { std::string signature; std::int32_t activeStart, activeEnd; }; // byte range in `signature`
using  SignatureHelpReply = std::function<void(SignatureHelp)>;     // empty signature = nothing

struct ISignatureHelpProvider {
    virtual void requestSignatureHelp(const SignatureHelpRequest&, SignatureHelpReply) = 0;
};
struct ISignatureHelpRegistry { virtual SignatureHelpToken registerProvider(ISignatureHelpProvider&) = 0; };

Isäntä näyttää ensimmäisen ei-tyhjän vastauksen; [activeStart, activeEnd) on aktiivisen parametrin tavuväli merkkijonon signature sisällä, jota käytetään korostukseen.

Symbolit — asynkroninen veto (siirry symboliin)

Toimittaja luettelee asiakirjan symbolit (ja hakee työtilasta); isäntä piirtää valitsimen (komentopaletin tilat Ctrl+Shift+O / Ctrl+T), ja mikä tahansa liitännäinen voi vetää koostetut symbolit — niin tekee Funktioluettelo-telakka, mikä tekee tästä aidosti monikuluttajaisen sopimuksen. Se tavoitetaan funktiolla IPluginContext::symbols() (PluginApi/ISymbolRegistry.h):

struct SymbolLocation { std::string path; std::int32_t line, column; }; // path empty = request doc; column = byte offset in line
struct SymbolInfo { std::string name; std::int32_t kind; std::string containerName; SymbolLocation location; }; // kind = LSP SymbolKind (1..26)
using  SymbolReply = std::function<void(std::vector<SymbolInfo>)>;

struct ISymbolProvider {
    virtual void requestDocumentSymbols(const DocumentSymbolRequest&, SymbolReply) = 0;  // { DocumentId doc; std::string languageId; }
    virtual void requestWorkspaceSymbols(const WorkspaceSymbolRequest&, SymbolReply) = 0; // { std::string query; }
};
struct ISymbolRegistry {
    virtual SymbolToken registerProvider(ISymbolProvider&) = 0;                          // producer side
    // consumer side (pull) — used by the Command Palette and the Function List dock:
    virtual void requestDocumentSymbols(const DocumentSymbolRequest&, SymbolReply) const = 0;
    virtual void requestWorkspaceSymbols(const WorkspaceSymbolRequest&, SymbolReply) const = 0;
};
  • Sijainnit ilmaistaan niin kuin isäntä navigoi: tiedostojärjestelmän path (tyhjä tarkoittaa pyynnön asiakirjaa) sekä nollasta alkava line ja tavuperusteinen column sen sisällä — kutsupaikassa ei tarvita lisämuunnosta.
  • Isäntä toimittaa ensimmäisen ei-tyhjän symboliluettelon tai tyhjän luettelon, kun jokainen toimittaja on vastannut tyhjällä (jotta odottavalle valitsimelle kerrotaan aina jotain).
  • Kuluttajapuoli: rekisterin funktiot requestDocumentSymbols / requestWorkspaceSymbols ovat funktion registerProvider vetovastine — liitännäinen kutsuu niitä lukeakseen sen, mitä muut liitännäiset (esimerkiksi kielipalvelin) tarjoavat. Vastaus laukeaa kerran, käyttöliittymäsäikeessä, mahdollisesti myöhemmin (asynkronisesti). Funktioluettelo-telakka käyttää tätä tapaan kysy ja turvaudu varalle: se näyttää oman säännöllisiin lausekkeisiin perustuvan yleiskuvansa heti ja päivittää sen, jos ei-tyhjä vastaus saapuu; tyhjä vastaus ei koskaan tyhjennä perustasoa. Suojaa myöhäinen vastaus purkamiselta ja uudemmalta pyynnöltä (asiakirjan tunniste ja sukupolvi).

Kielet — luettele, vaihda, tarjoa

Syntaksivärjäys on ytimen ominaisuus: isäntä omistaa värjääjäluettelon (mukana tulevat colorizers/*.json-tiedostot sekä käyttäjän omat), tunnistaa asiakirjan kielen sen polusta ja ensimmäisestä rivistä ja värittää näkymän ilman liitännäistä. ctx.languages() (ILanguageRegistry) on se tapa, jolla liitännäinen osallistuu:

// Enumeration -- e.g. to fill a per-language settings dropdown.
std::vector<LanguageInfo> langs = ctx.languages().availableLanguages(); // {id, name}, by name

// Per-document state. languageFor() reports the same id as
// IEditorService::colorizerId(); setLanguageFor() takes exactly the path
// the Language menu takes ("" clears colouring; false = unknown id / doc).
std::string id = ctx.languages().languageFor(doc);
bool ok        = ctx.languages().setLanguageFor(doc, "json");
bool redone    = ctx.languages().autoDetectFor(doc);   // undo a manual override

// Contribution: a whole language definition, in the same shape as a
// shipped colorizer file -- backend lexer NAME, style NAMES -> category,
// keyword groups by the lexer's own slot description. Never engine ids.
LanguageDefinition def;
def.id = "mylang";  def.name = "My Language";  def.lexer = "cpp";
def.extensions = {"mylang"};
def.keywords   = {{"Primary keywords and identifiers", "if else while"}};
def.styles     = {{"SCE_C_COMMENT", SyntaxCategory::Comment},
                  {"SCE_C_STRING",  SyntaxCategory::String}};
m_lang = ctx.languages().registerLanguage(def);          // LanguageToken (RAII)

Tarjottu kieli käyttäytyy täsmälleen kuten mukana tuleva: isäntä ratkaisee nimet taustajärjestelmän leksikaalisen jäsentimen metatietoja vasten ja aktiivinen teema toimittaa värit. Jo olemassa olevan tunnisteen rekisteröinti korvaa sen niin kauan kuin poletti elää; kun poletti kuolee, syrjäytetty määritys palaa ja avoimet asiakirjat, jotka käyttävät tunnistetta, väritetään uudelleen — joten liitännäinen voi ohittaa mukana tulevan kielen vahingoittamatta luetteloa pysyvästi. Tarjoukset selviävät myös siitä luettelon uudelleenlatauksesta, jonka Asetukset ▸ Ota käyttöön tai teeman vaihto laukaisee. Isäntä ei voi saada uusia taustajärjestelmän leksikaalisia jäsentimiä ajon aikana — lexer-kentän täytyy nimetä sellainen, joka on olemassa. Aina kelvollinen: isäntä ilman värjäyskerrosta toimittaa tyhjän toteutuksen, joka ei luettele mitään ja hylkää jokaisen muutoksen.

Tila

Kaikki neljä sopimusta (diagnostiikka, osoitusvihjeet, allekirjoitusohje, symbolit) lisättiin ennen julkaisua nostamatta arvoa MTE_PLUGIN_API_VERSION; kielipinta (ILanguageRegistry) seurasi samalla tavalla. Niiden ensimmäinen kuluttaja on LSP-liitännäinen; symbolisopimus on tarkoituksella monikuluttajainen (komentopaletti tänään, Funktioluettelo-telakka seuraavaksi).