Ga naar inhoud

Taalintelligentie (diagnostiek, aanwijshulp, handtekeninghulp, symbolen)

Een kleine, backend-neutrale familie contracten voor taalintelligentie — de gegevens die een leverancier aan de code-intelligentievlakken van de editor voert. Zij volgt de regel van het project «plug-ins dragen gegevens bij, de kern tekent»: de golflijnen, de pop-up bij aanwijzen en de cursorsprongen wonen in de editorweergave en worden nooit aan plug-ins blootgesteld; een plug-in levert alleen POD- en std::string-gegevens, zodat hetzelfde contract later door een Python-backend vervuld kan worden.

De eerste afnemer is de LSP-plug-in, maar de kanalen zijn algemeen: een statische analysator of een spellingcontrole kan diagnostiek op dezelfde manier publiceren.

Wat hier niet staat

Ga naar definitie, verwijzingen zoeken, naam wijzigen en opmaken hebben geen eigen API nodig — ze navigeren of bewerken alleen tekst, wat IEditorService al uitdrukt (caret, openFile, setCaret, setText), dus ze blijven opdrachten binnen de plug-in. Aanvulling hergebruikt de bestaande ICompletionRegistry. Er wordt pas een nieuw contract toegevoegd wanneer er een teken- of kiesvlak in de kern is en er mogelijk meerdere leveranciers zijn — wat het geval is voor diagnostiek, aanwijshulp, handtekeninghulp en symbolen.

Diagnostiek — duwen

Een leverancier duwt diagnostiek zodra hij die heeft (hij beslist wanneer de resultaten klaar zijn); de editor tekent ze als golvende onderstrepingen. Bereik de opvang via IPluginContext::diagnostics(). De waardetypen zijn eenvoudige 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 is een vervangsleutel per (doc, source). Publiceren voor een bron vervangt de vorige diagnostiek van die bron op het document, zodat meerdere producenten (LSP, spellingcontrole, bouwlogboek) naast elkaar bestaan zonder elkaar te overschrijven.
  • Publiceer een lege vector voor een bron om haar diagnostiek in te trekken.
  • Ernstgraden worden met verschillende kleuren getekend (fout/waarschuwing/informatie/hint); de editor verbreedt bovendien heel korte bereiken, zodat een markering van één teken zichtbaar blijft.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

De host levert altijd een geldige opvang (een niet-bewerking wanneer de editor geen diagnostiekvlak heeft), dus u hoeft nooit te controleren of diagnostics() niet nul is.

Aanwijzen — asynchroon trekken

Aanwijshulp wordt door de editor aangestuurd (de muis die op een symbool blijft rusten). Een plug-in registreert een leverancier die asynchroon antwoordt; bereik het register via 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 levert een HoverToken (RAII) op; vernietig hem in shutdown() om de registratie op te heffen.
  • Elke leverancier registreert zich met een prioriteit. Kies de band die beschrijft wat je beantwoordt, niet hoe belangrijk het aanvoelt: kFallback voor informatie op woord- of tekstniveau (een woordenboekdefinitie, het doel van een URL), kLanguage voor taalkundige informatie over het symbool (de hover van een taalserver, de regel van een linter), kActiveSession voor informatie die moet winnen zolang ze actief is (de waarde onder de aanwijzer in een debugger). Waarden tussen de banden (kLanguage + 10) zijn toegestaan wanneer je je ten opzichte van een bekende buur moet ordenen.
  • Blijft de aanwijzer rusten, dan vraagt de host alle leveranciers tegelijk en toont hij het niet-lege antwoord van de leverancier met de hoogste prioriteit; bij gelijke prioriteit wint de leverancier die het eerst is geregistreerd. Een antwoord met lagere prioriteit dat eerst binnenkomt, wordt vastgehouden tot elke hogere leverancier leeg heeft geantwoord. Antwoorden ze allemaal leeg, dan wordt er niets getoond.
  • Een trage leverancier kan de tooltip niet eindeloos ophouden: 300 ms na het verzoek wordt het beste tot dan ontvangen antwoord getoond en worden latere antwoorden op dat verzoek verworpen. Een kActiveSession-leverancier moet daarom meteen leeg antwoorden als hij niets te zeggen heeft (geen sessie) — anders vertraagt hij elke hover in de editor met maximaal die tijdslimiet.
  • Antwoorden die binnenkomen nadat de gebruiker verder is gegaan, worden verworpen.
  • Het antwoord is gewone opmaaktekst en wordt als Markdown in een pop-up weergegeven, dus koppen, vet en codeblokken werken — houd het contract op tekenreeks in / tekenreeks uit.
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);

Handtekeninghulp — asynchroon trekken

Als aanwijshulp, maar aangestuurd door het typen van de ( of een , van een aanroep. Een leverancier antwoordt met de actieve handtekening; de host tekent die als een aanroephulp met de actieve parameter gemarkeerd. Bereik het register via 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; };

De host toont het eerste niet-lege antwoord; [activeStart, activeEnd) is het bytebereik van de actieve parameter binnen signature, gebruikt voor de markering.

Symbolen — asynchroon trekken (ga naar symbool)

Een leverancier somt de symbolen van een document op (en doorzoekt de werkruimte); de host tekent de kiezer (de symboolmodi Ctrl+Shift+O / Ctrl+T van het Opdrachtenpalet) en elke plug-in kan de verzamelde symbolen trekken — het dok Functielijst doet dat, wat hier een werkelijk contract met meerdere afnemers van maakt. Bereik het via 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;
};
  • Locaties worden uitgedrukt zoals de host navigeert: een path in het bestandssysteem (leeg betekent het document van het verzoek) plus een vanaf 0 getelde line en een column in bytes daarin — er is op de plek van de aanroep geen verdere omzetting nodig.
  • De host levert de eerste niet-lege symbolenlijst, of een lege lijst zodra elke leverancier leeg geantwoord heeft (zodat een wachtende kiezer altijd iets te horen krijgt).
  • Afnemerskant: requestDocumentSymbols / requestWorkspaceSymbols op het register zijn de trek-tegenhanger van registerProvider — een plug-in roept ze aan om te lezen wat andere plug-ins (bijvoorbeeld een taalserver) bijdragen. Het antwoord komt één keer, op de interfacethread, mogelijk later (asynchroon). Het dok Functielijst gebruikt dit patroon van vragen en terugvallen: het toont meteen zijn eigen schets op basis van reguliere expressies en schakelt over zodra er een niet-leeg antwoord binnenkomt; een leeg antwoord wist de basis nooit. Bescherm een laat antwoord tegen het afbreken en tegen een nieuwer verzoek (document-id + generatie).

Talen — opsommen, wisselen, bijdragen

Syntaxiskleuring is een functie van de kern: de host bezit een catalogus van colorizers (de meegeleverde bestanden colorizers/*.json plus die van de gebruiker), herkent de taal van een document aan zijn pad en eerste regel, en kleurt de weergave zonder enige plug-in. ctx.languages() (ILanguageRegistry) is hoe een plug-in meedoet:

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

Een bijgedragen taal gedraagt zich precies als een meegeleverde: de host lost de namen op tegen de metagegevens van de lexer van de backend en het actieve thema levert de kleuren. Een id registreren die al bestaat vervangt die zolang het token leeft; sterft het token, dan komt de verdrongen definitie terug en worden geopende documenten die de id gebruiken opnieuw gekleurd — zodat een plug-in een meegeleverde taal kan overschrijven zonder de catalogus blijvend te beschadigen. Bijdragen overleven ook de herlading van de catalogus die een Voorkeuren ▸ Toepassen of een themawissel in gang zet. De host kan tijdens het draaien geen nieuwe lexers van de backend krijgen — lexer moet er een noemen die bestaat. Altijd geldig: een host zonder kleurlaag levert een niet-bewerking die niets opsomt en elke wijziging weigert.

Status

Alle vier de contracten (diagnostiek, aanwijshulp, handtekeninghulp, symbolen) zijn vóór de uitgave toegevoegd zonder MTE_PLUGIN_API_VERSION te verhogen; het taalvlak (ILanguageRegistry) is op dezelfde manier gevolgd. Hun eerste afnemer is de LSP-plug-in; het symboolcontract is met opzet voor meerdere afnemers (vandaag het Opdrachtenpalet, daarna het dok Functielijst).