Gå til innhold

Språkintelligens (diagnostikk, opplysninger ved peking, signaturhjelp, symboler)

En liten, backend-nøytral familie av kontrakter for språkintelligens — de dataene en leverandør mater inn i redigererens flater for kodeintelligens. Den følger prosjektets regel om at programtillegg bidrar med data, og kjernen tegner: bølgelinjene, boblen ved peking og markørhoppene hører hjemme i redigererens visning og blottlegges aldri for programtillegg; et programtillegg leverer bare POD- og std::string-data, så den samme kontrakten senere kan oppfylles av en Python-backend.

Den første forbrukeren er LSP-programtillegget, men kanalene er allmenne — en statisk analysator eller en stavekontroll kan publisere diagnostikk på samme måte.

Hva som ikke er her

Gå til definisjonen, søk etter referanser, gi nytt navn og formater krever ingen API for seg selv — de bare navigerer eller redigerer tekst, noe IEditorService alt uttrykker (caret, openFile, setCaret, setText), så de blir værende kommandoer inne i programtillegget. Fullføring gjenbruker den eksisterende ICompletionRegistry. En ny kontrakt legges bare til når det finnes en flate i kjernen som tegner eller velger, og kanskje flere leverandører — og det er tilfellet for diagnostikk, opplysninger ved peking, signaturhjelp og symboler.

Diagnostikk — skyv

En leverandør skyver diagnostikk når den har noe (den avgjør selv når resultatene er klare); redigereren tegner den som bølgede understrekinger. Mottakeren nås gjennom IPluginContext::diagnostics(). Verditypene er alminnelig 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økkel per (dokument, kilde). Å publisere for en kilde erstatter den kildens tidligere diagnostikk på dokumentet, så flere produsenter (LSP, stavekontroll, byggelogg) kan leve side om side uten å slette hverandres.
  • Publiser en tom tabell for en kilde for å trekke diagnostikken dens tilbake.
  • Alvorsgradene tegnes med hver sin farge (feil, advarsel, informasjon, tips); redigereren gjør også svært korte områder bredere, så en markør på ett enkelt tegn blir værende synlig.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Verten stiller alltid en gyldig mottaker til rådighet (en tom utgave når redigereren ikke har noen flate for diagnostikk), så du aldri trenger å kontrollere om diagnostics() er null.

Opplysninger ved peking — asynkront trekk

Opplysninger ved peking drives av redigereren (musen hviler over et symbol). Et programtillegg registrerer en leverandør som svarer asynkront; registeret nås gjennom 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 gir et HoverToken tilbake (RAII); ødelegg det i shutdown() for å avregistrere.
  • Hver leverandør registrerer seg med en prioritet. Velg båndet som beskriver hva du svarer, ikke hvor viktig det føles: kFallback for informasjon på ord- eller tekstnivå (en ordboksdefinisjon, målet for en URL), kLanguage for språkinformasjon om symbolet (hoveren til en språkserver, regelen til en linter), kActiveSession for informasjon som må vinne så lenge den er aktiv (verdien under pekeren i en feilsøker). Verdier mellom båndene (kLanguage + 10) er tillatt når du må ordne deg i forhold til en kjent nabo.
  • Når musen hviler, spør verten alle leverandørene samtidig og viser det ikke-tomme svaret fra leverandøren med høyest prioritet; ved lik prioritet vinner leverandøren som ble registrert først. Et svar med lavere prioritet som kommer først, holdes tilbake til hver høyere leverandør har svart tomt. Når alle svarer tomt, vises ingenting.
  • En treg leverandør kan ikke holde verktøytipset tilbake for alltid: 300 ms etter forespørselen vises det beste svaret som er mottatt så langt, og senere svar på den forespørselen kastes. En kActiveSession-leverandør må derfor svare tomt med en gang når den ikke har noe å si (ingen økt) — ellers forsinker den hver hover i redigeringsprogrammet med inntil tidsgrensen.
  • Svar som kommer etter at brukeren har gått videre, kastes.
  • Svaret er alminnelig oppmerkingstekst og gjengis som Markdown i en boble, så overskrifter, halvfet skrift og kodeblokker virker — hold kontrakten til streng inn, streng ut.
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);

Signaturhjelp — asynkront trekk

Som opplysninger ved peking, men drevet av at man skriver et kalls ( eller et ,. En leverandør svarer med den aktive signaturen; verten tegner den som et kalltips med den aktive parameteren uthevet. Registeret nås gjennom 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; };

Verten viser det første ikke-tomme svaret; [activeStart, activeEnd) er den aktive parameterens byteområde inne i signature, og brukes til uthevingen.

Symboler — asynkront trekk (gå til symbol)

En leverandør lister et dokuments symboler (og søker i arbeidsområdet); verten tegner velgeren (kommandopalettens modi Ctrl+Shift+O / Ctrl+T), og ethvert programtillegg kan trekke de samlede symbolene — det gjør Funksjonsliste-panelet, noe som gjør dette til en ekte kontrakt med flere forbrukere. Den nås gjennom 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;
};
  • Plasseringer uttrykkes slik verten navigerer: en path i filsystemet (tom betyr forespørselens dokument) pluss en 0-basert line og en column i byte inne i den — det trengs ingen videre omregning på kallstedet.
  • Verten leverer den første ikke-tomme symbollisten, eller en tom liste når hver leverandør har svart tomt (så en ventende velger alltid får vite noe).
  • Forbrukersiden: requestDocumentSymbols / requestWorkspaceSymbols på registeret er trekkmotstykket til registerProvider — et programtillegg kaller dem for å lese det andre programtillegg (for eksempel en språkserver) bidrar med. Svaret utløses én gang, på grensesnittråden, kanskje senere (asynkront). Funksjonsliste-panelet bruker dette som spør og fall tilbake: det viser sin egen regex-oversikt med det samme og oppgraderer til svaret hvis det kommer et ikke-tomt; et tomt svar tømmer aldri utgangspunktet. Vern et sent svar mot nedriving og mot en nyere forespørsel (dokument-id pluss generasjon).

Språk — list, bytt, bidra

Syntaksfarging er en kjernefunksjon: verten eier en katalog av fargeleggere (de medfølgende colorizers/*.json-filene pluss brukerens egne), finner ut et dokuments språk ut fra stien og første linje, og farger visningen uten noe programtillegg. ctx.languages() (ILanguageRegistry) er måten et programtillegg 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 språk som er bidratt med, oppfører seg nøyaktig som et medfølgende: verten slår navnene opp i backend-leksikografens metadata, og det aktive temaet leverer fargene. Å registrere en id som alt finnes, erstatter den så lenge symbolet lever; når symbolet dør, kommer den fortrengte definisjonen tilbake, og åpne dokumenter som bruker id-en, farges på nytt — så et programtillegg kan overstyre et medfølgende språk uten å skade katalogen varig. Bidrag overlever også den innlastingen av katalogen som et Innstillinger ▸ Bruk eller et temabytte utløser. Verten kan ikke få nye backend-leksikografer under kjøringen — lexer må navngi en som finnes. Alltid gyldig: en vert uten et fargeleggingslag leverer en tom utgave som lister ingenting og nekter enhver endring.

Status

Alle fire kontraktene (diagnostikk, opplysninger ved peking, signaturhjelp, symboler) ble lagt til før utgivelsen, uten at MTE_PLUGIN_API_VERSION ble hevet; språkflaten (ILanguageRegistry) fulgte på samme måte. Den første forbrukeren deres er LSP-programtillegget; symbolkontrakten har med vilje flere forbrukere (kommandopaletten i dag, Funksjonsliste-panelet som det neste).