Gå til indholdet

Sprogintelligens (diagnostik, oplysninger ved pegning, signaturhjælp, symboler)

En lille, backend-neutral familie af kontrakter til sprogintelligens — de data, en leverandør sender ind i editorens flader for kodeintelligens. Den følger projektets regel om, at udvidelsesmoduler bidrager med data, mens kernen tegner: bølgelinjerne, boblen ved pegning og markørspringene hører hjemme i editorvisningen og blotlægges aldrig for udvidelsesmoduler; et modul leverer kun POD- og std::string-data, så den samme kontrakt senere kan opfyldes af en Python-backend.

Den første forbruger er LSP-udvidelsesmodulet, men kanalerne er almene — en statisk analysator eller en stavekontrol kan udgive diagnostik på samme måde.

Hvad der ikke er her

Gå til definition, find referencer, omdøb og formatér kræver ingen API for sig selv — de navigerer eller redigerer kun tekst, hvilket IEditorService allerede udtrykker (caret, openFile, setCaret, setText), så de bliver ved med at være kommandoer inde i modulet. Fuldførelse genbruger den eksisterende ICompletionRegistry. En ny kontrakt tilføjes kun, når der findes en flade i kernen, der tegner eller vælger, og potentielt flere leverandører — og det er tilfældet for diagnostik, oplysninger ved pegning, signaturhjælp og symboler.

Diagnostik — skub

En leverandør skubber diagnostik, når den har noget (den afgør selv, hvornår resultaterne er klar); editoren tegner den som bølgede understregninger. Modtageren nås via IPluginContext::diagnostics(). Værdityperne er almindelig 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 er en erstatningsnøgle pr. (dokument, kilde). At udgive for en kilde erstatter den kildes tidligere diagnostik på dokumentet, så flere producenter (LSP, stavekontrol, byggelog) kan sameksistere uden at slette hinandens.
  • Udgiv en tom tabel for en kilde for at trække dens diagnostik tilbage.
  • Alvorsgraderne tegnes med hver sin farve (fejl, advarsel, info, tip); editoren gør også meget korte områder bredere, så en markør på ét enkelt tegn bliver ved med at være synlig.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Værten stiller altid en gyldig modtager til rådighed (en tom udgave, når editoren ikke har nogen flade til diagnostik), så du aldrig behøver at kontrollere, om diagnostics() er null.

Oplysninger ved pegning — asynkront træk

Oplysninger ved pegning drives af editoren (musen hviler over et symbol). Et udvidelsesmodul registrerer en leverandør, der svarer asynkront; registret nås 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 giver et HoverToken tilbage (RAII); ødelæg det i shutdown() for at afregistrere.
  • Hver leverandør registrerer sig med en prioritet. Vælg det bånd, der beskriver, hvad du svarer, ikke hvor vigtigt det føles: kFallback til oplysninger på ord- eller tekstniveau (en ordbogsdefinition, målet for en URL), kLanguage til sprogoplysninger om symbolet (en sprogservers hover, en linters regel), kActiveSession til oplysninger, der skal vinde, så længe de er aktive (værdien under markøren i en debugger). Værdier mellem båndene (kLanguage + 10) er tilladt, når du skal ordne dig i forhold til en kendt nabo.
  • Når musen hviler, spørger værten alle leverandører på én gang og viser det ikke-tomme svar fra leverandøren med højest prioritet; ved lige prioritet vinder den leverandør, der blev registreret først. Et svar med lavere prioritet, der kommer først, holdes tilbage, indtil hver højere leverandør har svaret tomt. Svarer alle tomt, vises intet.
  • En langsom leverandør kan ikke holde værktøjstippet tilbage for evigt: 300 ms efter forespørgslen vises det bedste svar, der er modtaget indtil da, og senere svar på den forespørgsel kasseres. En kActiveSession-leverandør skal derfor svare tomt med det samme, når den intet har at sige (ingen session) — ellers forsinker den hver hover i editoren med op til tidsgrænsen.
  • Svar, der kommer, efter at brugeren er gået videre, kasseres.
  • Svaret er almindelig opmærkningstekst og gengives som Markdown i en boble, så overskrifter, fed skrift og kodeblokke virker — hold kontrakten til streng ind, streng ud.
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);

Signaturhjælp — asynkront træk

Som oplysninger ved pegning, men drevet af, at man skriver et kalds ( eller et ,. En leverandør svarer med den aktive signatur; værten tegner den som et kaldetip med den aktive parameter fremhævet. Registret nås 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; };

Værten viser det første ikke-tomme svar; [activeStart, activeEnd) er den aktive parameters byteområde inde i signature og bruges til fremhævningen.

Symboler — asynkront træk (gå til symbol)

En leverandør opregner et dokuments symboler (og søger i arbejdsområdet); værten tegner vælgeren (kommandopalettens tilstande Ctrl+Shift+O / Ctrl+T), og ethvert udvidelsesmodul kan trække de samlede symboler — det gør Funktionsliste-panelet, hvilket gør dette til en ægte kontrakt med flere forbrugere. Den nås 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;
};
  • Placeringer udtrykkes, som værten navigerer: en path i filsystemet (tom betyder forespørgslens dokument) plus en 0-baseret line og en column i byte inde i den — der skal ikke omregnes yderligere på kaldstedet.
  • Værten leverer den første ikke-tomme symbolliste eller en tom liste, når hver leverandør har svaret tomt (så en ventende vælger altid får noget at vide).
  • Forbrugersiden: requestDocumentSymbols / requestWorkspaceSymbols på registret er trækmodstykket til registerProvider — et udvidelsesmodul kalder dem for at læse det, andre moduler (f.eks. en sprogserver) bidrager med. Svaret udløses én gang, på brugerfladetråden, muligvis senere (asynkront). Funktionsliste-panelet bruger dette som spørg og fald tilbage: det viser sin egen regex-oversigt med det samme og opgraderer til svaret, hvis der kommer et ikke-tomt; et tomt svar rydder aldrig udgangspunktet. Gardér et sent svar mod nedrivning og mod en nyere forespørgsel (dokument-id plus generation).

Sprog — opregn, skift, bidrag

Syntaksfarvning er en kernefunktion: værten ejer et katalog af farvelæggere (de medleverede colorizers/*.json-filer plus brugerens egne), afgør et dokuments sprog ud fra dets sti og første linje og farver visningen uden noget udvidelsesmodul. ctx.languages() (ILanguageRegistry) er den måde, et modul er med på:

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

Et bidraget sprog opfører sig præcis som et medleveret: værten slår navnene op i backend-lexerens metadata, og det aktive tema leverer farverne. At registrere et id, der allerede findes, erstatter det, så længe symbolet lever; når symbolet dør, kommer den fortrængte definition tilbage, og åbne dokumenter, der bruger id'et, farves igen — så et udvidelsesmodul kan tilsidesætte et medleveret sprog uden at ødelægge kataloget varigt. Bidrag overlever også den genindlæsning af kataloget, som et Indstillinger ▸ Anvend eller et temaskift udløser. Værten kan ikke få nye backend-lexere under kørslen — lexer skal navngive en, der findes. Altid gyldigt: en vært uden et farvelægningslag leverer en tom udgave, der opregner ingenting og nægter enhver ændring.

Status

Alle fire kontrakter (diagnostik, oplysninger ved pegning, signaturhjælp, symboler) blev tilføjet inden udgivelsen, uden at MTE_PLUGIN_API_VERSION blev hævet; sprogfladen (ILanguageRegistry) fulgte på samme måde. Deres første forbruger er LSP-udvidelsesmodulet; symbolkontrakten har med vilje flere forbrugere (kommandopaletten i dag, Funktionsliste-panelet som det næste).