Přeskočit obsah

Inteligence jazyka (diagnostika, nápovědy při najetí, nápověda k signaturám, symboly)

Malá rodina kontraktů neutrálních vůči backendu pro inteligenci jazyka — data, kterými poskytovatel krmí plochy inteligence kódu v editoru. Řídí se pravidlem projektu »zásuvné moduly přispívají daty, jádro vykresluje«: vlnovky, vyskakovací okno s nápovědou i skoky kurzoru bydlí v zobrazení editoru a modulům se nikdy nezpřístupňují; modul dodává pouze data typu POD a std::string, takže tentýž kontrakt může později naplnit backend v Pythonu.

Prvním konzumentem je zásuvný modul LSP, ale kanály jsou obecné — linter nebo kontrola pravopisu může diagnostiku publikovat stejně.

Co zde není

Přechod na definici, hledání odkazů, přejmenování a formátování nepotřebují žádné vlastní rozhraní — jen navigují v textu nebo jej upravují, což IEditorService už vyjadřuje (caret, openFile, setCaret, setText), takže zůstávají příkazy uvnitř modulu. Dokončování využívá stávající ICompletionRegistry. Nový kontrakt se přidává jen tehdy, existuje-li v jádře vykreslovací nebo výběrová plocha a potenciálně několik poskytovatelů — což je případ diagnostiky, nápovědy při najetí, nápovědy k signaturám a symbolů.

Diagnostika — vysílání

Poskytovatel diagnostiku vysílá, kdykoli ji má (sám rozhoduje, kdy jsou výsledky připravené); editor ji vykreslí jako vlnovkovitá podtržení. K příjemci se dostanete přes IPluginContext::diagnostics(). Hodnotové typy jsou obyčejné 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 klíč nahrazení pro dvojici (doc, source). Publikování pro daný zdroj nahradí jeho předchozí diagnostiku na dokumentu, takže několik producentů (LSP, kontrola pravopisu, protokol sestavení) koexistuje, aniž by se navzájem přepisovali.
  • Publikováním prázdného vektoru pro daný zdroj jeho diagnostiku stáhnete.
  • Závažnosti se vykreslují odlišnými barvami (chyba/varování/informace/nápověda); editor navíc velmi krátké rozsahy rozšíří, aby značka na jediný znak zůstala viditelná.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Hostitel vždy poskytuje platného příjemce (prázdnou implementaci, když editor žádnou plochu pro diagnostiku nemá), takže diagnostics() nikdy nemusíte kontrolovat na null.

Nápověda při najetí — asynchronní vyžádání

Nápovědu při najetí řídí editor (zastavení myši nad symbolem). Modul registruje poskytovatele, který odpovídá asynchronně; k registru se dostanete přes 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 vrací HoverToken (RAII); zničením ve shutdown() registraci zrušíte.
  • Každý poskytovatel se registruje s prioritou. Zvolte pásmo podle toho, co odpovídáte, ne podle toho, jak důležité se to zdá: kFallback pro informace na úrovni slova nebo textu (definice ze slovníku, cíl URL), kLanguage pro jazykové informace o symbolu (hover jazykového serveru, pravidlo linteru), kActiveSession pro informace, které musí vyhrát, dokud jsou aktivní (hodnota pod ukazatelem v ladicím programu). Hodnoty mezi pásmy (kLanguage + 10) jsou povolené, když se musíte zařadit vůči známému sousedovi.
  • Při zastavení myši se hostitel zeptá všech poskytovatelů najednou a zobrazí neprázdnou odpověď poskytovatele s nejvyšší prioritou; při shodné prioritě vyhrává poskytovatel zaregistrovaný dřív. Odpověď s nižší prioritou, která dorazí první, se podrží, dokud každý výše postavený poskytovatel neodpoví prázdně. Když všichni odpoví prázdně, nezobrazí se nic.
  • Pomalý poskytovatel nemůže zdržovat bublinovou nápovědu donekonečna: 300 ms po požadavku se zobrazí nejlepší dosud přijatá odpověď a pozdější odpovědi na tento požadavek se zahodí. Poskytovatel kActiveSession proto musí odpovědět prázdně okamžitě, když nemá co říct (žádná relace) — jinak zdrží každý hover v editoru až o tuto lhůtu.
  • Odpovědi, které dorazí poté, co uživatel pokračoval dál, se zahodí.
  • Odpověď je prostý text se značkováním a vykresluje se ve vyskakovacím okně jako Markdown, takže nadpisy, tučné písmo i bloky kódu fungují — kontrakt držte ve tvaru řetězec dovnitř / řetězec ven.
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);

Nápověda k signaturám — asynchronní vyžádání

Jako nápověda při najetí, ale řízená napsáním ( nebo , ve volání. Poskytovatel odpovídá aktivní signaturou; hostitel ji vykreslí jako tip k volání se zvýrazněným aktivním parametrem. K registru se dostanete přes 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; };

Hostitel zobrazí první neprázdnou odpověď; [activeStart, activeEnd) je bajtový úsek aktivního parametru uvnitř signature, použitý pro zvýraznění.

Symboly — asynchronní vyžádání (přechod na symbol)

Poskytovatel vyjmenovává symboly dokumentu (a prohledává pracovní prostor); hostitel vykresluje výběr (režimy Ctrl+Shift+O / Ctrl+T v Paletě příkazů) a kterýkoli modul si může souhrnné symboly vyžádat — což dok Seznam funkcí dělá, čímž je z toho skutečně vícekonzumentský kontrakt. Dostanete se k němu přes 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;
};
  • Umístění se vyjadřují tak, jak hostitel naviguje: cesta v souborovém systému path (prázdná znamená dokument požadavku) plus line počítaný od 0 a bajtový column v něm — v místě volání není potřeba žádný další převod.
  • Hostitel doručí první neprázdný seznam symbolů, nebo prázdný seznam, jakmile každý poskytovatel odpoví prázdně (takže čekajícímu výběru se vždy něco řekne).
  • Strana konzumenta: requestDocumentSymbols / requestWorkspaceSymbols na registru jsou protějškem registerProvider pro vyžádání — modul je volá, aby si přečetl, čím přispívají jiné moduly (například jazykový server). Odpověď se spustí jednou, v uživatelském vlákně, klidně později (asynchronně). Dok Seznam funkcí to používá ve stylu zeptej se a měj záložní řešení: okamžitě zobrazí svůj vlastní přehled z regulárních výrazů a dorazí-li neprázdná odpověď, povýší na ni; prázdná odpověď základ nikdy nesmaže. Pozdní odpověď pojistěte proti strhnutí a proti novějšímu požadavku (id dokumentu + generace).

Jazyky — vyjmenování, přepnutí, přispění

Obarvování syntaxe je funkcí jádra: hostitel vlastní katalog obarvovačů (dodávané soubory colorizers/*.json plus vlastní soubory uživatele), rozpozná jazyk dokumentu z jeho cesty a prvního řádku a zobrazení obarví bez jakéhokoli modulu. ctx.languages() (ILanguageRegistry) je způsob, jak se modul přidá:

// 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, kterým modul přispěl, se chová přesně jako dodávaný: hostitel názvy vyhodnotí proti metadatům lexeru backendu a barvy dodá aktivní motiv. Registrace id, které už existuje, jej nahradí na tak dlouho, dokud token žije; když token zanikne, vytlačená definice se vrátí a otevřené dokumenty používající ono id se přebarví — takže modul může dodávaný jazyk přebít, aniž by katalog trvale poškodil. Příspěvky přežijí i opětovné načtení katalogu, které vyvolá Předvolby ▸ Použít nebo přepnutí motivu. Hostitel nemůže za běhu získat nové lexery backendu — lexer musí pojmenovat některý z existujících. Vždy platné: hostitel bez obarvovací vrstvy dodá prázdnou implementaci, která nevyjmenuje nic a odmítne každou změnu.

Stav

Všechny čtyři kontrakty (diagnostika, nápověda při najetí, nápověda k signaturám, symboly) byly přidány před vydáním bez zvýšení MTE_PLUGIN_API_VERSION; jazyková plocha (ILanguageRegistry) následovala stejným způsobem. Jejich prvním konzumentem je zásuvný modul LSP; kontrakt symbolů je záměrně vícekonzumentský (dnes Paleta příkazů, dál dok Seznam funkcí).