Preskočiť na obsah

Jazyková inteligencia (diagnostika, informácie pri najazdení, pomoc so signatúrou, symboly)

Malá rodina kontraktov nezávislých od backendu pre jazykovú inteligenciu — údaje, ktoré poskytovateľ posiela plochám inteligencie kódu v editore. Riadi sa pravidlom projektu „zásuvné moduly prispievajú údajmi, jadro vykresľuje“: vlnovky, vyskakovacie okno pri najazdení aj skoky kurzora žijú v zobrazení editora a zásuvným modulom sa nikdy nesprístupňujú; modul dodáva iba údaje typu POD a std::string, takže ten istý kontrakt môže neskôr naplniť backend v Pythone.

Prvým konzumentom je zásuvný modul LSP, no kanály sú všeobecné — rovnako môže diagnostiku zverejňovať statický analyzátor alebo kontrola pravopisu.

Čo tu nie je

Prechod na definíciu, hľadanie odkazov, premenovanie a formátovanie nepotrebujú vlastné API — iba navigujú alebo upravujú text, čo IEditorService už vyjadruje (caret, openFile, setCaret, setText), takže ostávajú príkazmi vnútri zásuvného modulu. Dopĺňanie znova využíva existujúci ICompletionRegistry. Nový kontrakt sa pridáva iba vtedy, keď existuje plocha jadra na vykreslenie či výber a potenciálne viacero poskytovateľov — čo je prípad diagnostiky, informácií pri najazdení, pomoci so signatúrou a symbolov.

Diagnostika — push

Poskytovateľ diagnostiku posiela, kedykoľvek ju má (sám rozhoduje, kedy sú výsledky pripravené); editor ju vykreslí ako vlnovkové podčiarknutia. K cieľu sa dostanete cez IPluginContext::diagnostics(). Hodnotové typy sú obyčajné POD (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 je nahrádzací kľúč pre dvojicu (dokument, zdroj). Zverejnenie pre daný zdroj nahradí jeho predchádzajúcu diagnostiku v dokumente, takže viacerí producenti (LSP, kontrola pravopisu, log zostavenia) môžu existovať vedľa seba bez toho, aby si prekážali.
  • Zverejnením prázdneho vektora pre daný zdroj jeho diagnostiku stiahnete.
  • Závažnosti sa vykresľujú odlišnými farbami (chyba/upozornenie/informácia/rada); editor zároveň rozširuje veľmi krátke rozsahy, aby ostala jednoznaková značka viditeľná.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Hostiteľ vždy poskytuje platný cieľ (prázdnu implementáciu, keď editor nemá plochu pre diagnostiku), takže diagnostics() nikdy nemusíte kontrolovať na null.

Informácie pri najazdení — asynchrónny pull

Informácie pri najazdení riadi editor (zotrvanie myši nad symbolom). Zásuvný modul registruje poskytovateľa, ktorý odpovedá asynchrónne; k registru sa dostanete cez 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 vracia HoverToken (RAII); zničte ho v shutdown(), čím registráciu zrušíte.
  • Každý poskytovateľ sa registruje s prioritou. Zvoľte pásmo podľa toho, čo odpovedáte, nie podľa toho, aké dôležité sa to zdá: kFallback pre informácie na úrovni slova alebo textu (definícia zo slovníka, cieľ URL), kLanguage pre jazykové informácie o symbole (hover jazykového servera, pravidlo lintera), kActiveSession pre informácie, ktoré musia vyhrať, kým sú aktívne (hodnota pod ukazovateľom v ladiacom programe). Hodnoty medzi pásmami (kLanguage + 10) sú povolené, keď sa musíte zaradiť voči známemu susedovi.
  • Pri zotrvaní sa hostiteľ spýta všetkých poskytovateľov naraz a zobrazí neprázdnu odpoveď poskytovateľa s najvyššou prioritou; pri rovnakej priorite vyhráva poskytovateľ zaregistrovaný skôr. Odpoveď s nižšou prioritou, ktorá príde prvá, sa podrží, kým každý vyššie postavený poskytovateľ neodpovie prázdne. Keď všetci odpovedia prázdne, nezobrazí sa nič.
  • Pomalý poskytovateľ nemôže zdržiavať bublinový popis donekonečna: 300 ms po požiadavke sa zobrazí najlepšia dovtedy prijatá odpoveď a neskoršie odpovede na túto požiadavku sa zahodia. Poskytovateľ kActiveSession preto musí odpovedať prázdne okamžite, keď nemá čo povedať (žiadna relácia) — inak zdrží každý hover v editore až o túto lehotu.
  • Odpovede, ktoré prídu po tom, čo používateľ pokračoval ďalej, sa zahodia.
  • Odpoveďou je obyčajný text so značkami a vykresľuje sa vo vyskakovacom okne ako Markdown, takže nadpisy, tučné písmo aj bloky kódu fungujú — kontrakt držte v podobe reťazec dnu, reťazec von.
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);

Pomoc so signatúrou — asynchrónny pull

Ako pri najazdení, ale riadené napísaním ( alebo , vo volaní. Poskytovateľ odpovie aktívnou signatúrou; hostiteľ ju vykreslí ako bublinu volania so zvýrazneným aktívnym parametrom. K registru sa dostanete cez 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; };

Hostiteľ zobrazí prvú neprázdnu odpoveď; [activeStart, activeEnd) je bajtový rozsah aktívneho parametra v rámci signature, použitý na zvýraznenie.

Symboly — asynchrónny pull (prechod na symbol)

Poskytovateľ vypisuje symboly dokumentu (a prehľadáva pracovný priestor); hostiteľ vykresľuje výber (režimy Ctrl+Shift+O a Ctrl+T v Palete príkazov) a zhromaždené symboly si môže vyžiadať ktorýkoľvek zásuvný modul — robí to panel Zoznam funkcií, čím je tento kontrakt skutočne určený viacerým konzumentom. Dostanete sa k nemu cez 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;
};
  • Umiestnenia sa vyjadrujú tak, ako hostiteľ naviguje: cestou path v súborovom systéme (prázdna znamená dokument z požiadavky) plus line počítaným od nuly a bajtovým column v ňom — na mieste volania nie je potrebný žiadny ďalší prevod.
  • Hostiteľ doručí prvý neprázdny zoznam symbolov, prípadne prázdny zoznam, keď každý poskytovateľ odpovie prázdno (takže sa čakajúci výber vždy niečo dozvie).
  • Strana konzumenta: requestDocumentSymbols a requestWorkspaceSymbols na registri sú náprotivkom registerProvider typu pull — zásuvný modul ich volá, aby si prečítal, čím prispievajú iné moduly (napríklad jazykový server). Odpoveď sa vyvolá raz, vo vlákne rozhrania, prípadne aj neskôr (asynchrónne). Panel Zoznam funkcií to využíva spôsobom opýtaj sa a maj zálohu: okamžite zobrazí vlastnú osnovu podľa regulárnych výrazov a keď príde neprázdna odpoveď, povýši sa na ňu; prázdna odpoveď základ nikdy nezmaže. Oneskorenú odpoveď zabezpečte proti rušeniu a proti novšej požiadavke (id dokumentu a generácia).

Jazyky — výpis, prepnutie, príspevok

Farebné zvýrazňovanie syntaxe je funkciou jadra: hostiteľ vlastní katalóg zvýrazňovačov (dodávané súbory colorizers/*.json plus vlastné súbory používateľa), rozpozná jazyk dokumentu z jeho cesty a prvého riadka a zobrazenie zafarbí bez akéhokoľvek zásuvného modulu. ctx.languages() (ILanguageRegistry) je spôsob, ako sa modul pridá:

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

Jazyk pridaný príspevkom sa správa presne ako dodávaný: hostiteľ vyhodnotí názvy oproti metaúdajom lexera backendu a farby dodá aktívny motív. Zaregistrovaním id, ktoré už existuje, sa pôvodné id nahradí na tak dlho, ako žije token; keď token zanikne, vráti sa vytlačená definícia a otvorené dokumenty používajúce toto id sa prefarbia — takže zásuvný modul môže dodávaný jazyk prekryť bez toho, aby katalóg natrvalo poškodil. Príspevky prežijú aj opätovné načítanie katalógu, ktoré vyvolá Predvoľby ▸ Použiť alebo zmena motívu. Hostiteľ nemôže za behu získať nové lexery backendu — lexer musí pomenovať taký, ktorý existuje. Platné vždy: hostiteľ bez vrstvy zvýrazňovania poskytne prázdnu implementáciu, ktorá nevypíše nič a odmietne každú zmenu.

Stav

Všetky štyri kontrakty (diagnostika, informácie pri najazdení, pomoc so signatúrou, symboly) pribudli pred vydaním bez zvýšenia MTE_PLUGIN_API_VERSION; rovnako nasledovala jazyková plocha (ILanguageRegistry). Ich prvým konzumentom je zásuvný modul LSP; kontrakt pre symboly je zámerne určený viacerým konzumentom (dnes Palete príkazov, ďalej panelu Zoznam funkcií).