Gå till innehållet

Språkintelligens (diagnostik, pekhjälp, signaturhjälp, symboler)

En liten, backend-neutral kontraktsfamilj för språkintelligens — de data en leverantör matar in till redigerarens ytor för kodintelligens. Den följer projektets regel «insticksmoduler bidrar med data, kärnan ritar»: våglinjerna, pekhjälpsfönstret och markörhoppen bor i redigerarvyn och exponeras aldrig för insticksmoduler; en insticksmodul levererar bara POD- och std::string-data, så att samma kontrakt senare kan uppfyllas av en Python-backend.

Den första förbrukaren är LSP-insticksmodulen, men kanalerna är allmänna — en statisk analysator eller en stavningskontroll kan publicera diagnostik på samma sätt.

Vad som inte finns här

Gå till definitionen, sök referenser, byt namn och formatering behöver inget eget API — de bara navigerar eller redigerar text, vilket IEditorService redan uttrycker (caret, openFile, setCaret, setText), så de förblir kommandon inuti insticksmodulen. Komplettering återanvänder befintliga ICompletionRegistry. Ett nytt kontrakt läggs till bara när det finns en rit- eller väljaryta i kärnan och potentiellt flera leverantörer — vilket är fallet för diagnostik, pekhjälp, signaturhjälp och symboler.

Diagnostik — push

En leverantör skjuter ut diagnostik så snart den har den (den avgör när resultaten är klara); redigeraren ritar dem som vågiga understrykningar. Nå mottagaren via IPluginContext::diagnostics(). Värdetyperna är enkla 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 är en ersättningsnyckel per (doc, source). Att publicera för en källa ersätter den källans tidigare diagnostik i dokumentet, så att flera producenter (LSP, stavningskontroll, byggogg) samexisterar utan att skriva över varandra.
  • Publicera en tom vektor för en källa för att dra tillbaka dess diagnostik.
  • Allvarlighetsgrader ritas med distinkta färger (fel/varning/information/tips); redigeraren vidgar dessutom mycket korta intervall så att en markör på ett enda tecken förblir synlig.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Värden tillhandahåller alltid en giltig mottagare (en nulloperation när redigeraren inte har någon diagnostikyta), så du behöver aldrig nollkontrollera diagnostics().

Pekhjälp — asynkron hämtning

Pekhjälpen drivs av redigeraren (musen som vilar över en symbol). En insticksmodul registrerar en leverantör som svarar asynkront; nå registret 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 returnerar en HoverToken (RAII); förstör den i shutdown() för att avregistrera.
  • Varje leverantör registrerar sig med en prioritet. Välj det band som beskriver vad du svarar, inte hur viktigt det känns: kFallback för information på ord- eller textnivå (en ordboksdefinition, målet för en URL), kLanguage för språkinformation om symbolen (en språkservers hover, en linters regel), kActiveSession för information som måste vinna medan den är aktiv (värdet under pekaren i en felsökare). Värden mellan banden (kLanguage + 10) är tillåtna när du måste ordna dig i förhållande till en känd granne.
  • När pekaren vilar frågar värden alla leverantörer samtidigt och visar det icke-tomma svaret från leverantören med högst prioritet; vid lika prioritet vinner den leverantör som registrerades först. Ett svar med lägre prioritet som kommer först hålls kvar tills varje högre leverantör har svarat tomt. När alla svarar tomt visas ingenting.
  • En långsam leverantör kan inte hålla tillbaka verktygstipset hur länge som helst: 300 ms efter förfrågan visas det bästa svaret som har kommit dit och senare svar på den förfrågan kastas. En kActiveSession-leverantör måste därför svara tomt genast när den inte har något att säga (ingen session) — annars fördröjer den varje hover i redigeraren med upp till tidsgränsen.
  • Svar som kommer efter att användaren gått vidare kastas.
  • Svaret är vanlig uppmärkningstext och återges som Markdown i ett popup-fönster, så att rubriker, fetstil och kodblock fungerar — håll kontraktet sträng in / sträng 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);

Signaturhjälp — asynkron hämtning

Som pekhjälpen, men driven av att ett anropets ( eller ett , skrivs. En leverantör svarar med den aktiva signaturen; värden ritar den som ett anropstips med den aktiva parametern färgmarkerad. Nå registret 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ärden visar det första icke-tomma svaret; [activeStart, activeEnd) är den aktiva parameterns byteintervall inuti signature, använt för färgmarkeringen.

Symboler — asynkron hämtning (gå till symbol)

En leverantör räknar upp ett dokuments symboler (och söker i arbetsytan); värden ritar väljaren (kommandopalettens symbollägen Ctrl+Shift+O / Ctrl+T) och varje insticksmodul kan hämta de samlade symbolerna — dockan Funktionslista gör det, vilket gör detta till ett verkligt kontrakt med flera förbrukare. Nå det 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;
};
  • Platser uttrycks som värden navigerar: en path i filsystemet (tom betyder begärans dokument) plus en line räknad från 0 och en column i byte i den — ingen ytterligare konvertering behövs på anropsplatsen.
  • Värden levererar den första icke-tomma symbollistan, eller en tom lista när varje leverantör svarat tomt (så att en väntande väljare alltid får veta något).
  • Förbrukarsidan: requestDocumentSymbols / requestWorkspaceSymbols på registret är hämtningsmotsvarigheten till registerProvider — en insticksmodul anropar dem för att läsa vad andra insticksmoduler (till exempel en språkserver) bidrar med. Svaret utlöses en gång, på gränssnittstråden, möjligen senare (asynkront). Dockan Funktionslista använder detta mönster med fråga och falla tillbaka: den visar sin egen översikt baserad på reguljära uttryck omedelbart och, om ett icke-tomt svar kommer, uppgraderar till det; ett tomt svar rensar aldrig grunden. Skydda ett sent svar mot nedrivning och mot en nyare begäran (dokument-id + generation).

Språk — räkna upp, byta, bidra

Syntaxfärgning är en funktion i kärnan: värden äger en colorizer-katalog (de medföljande filerna colorizers/*.json plus användarens egna), identifierar ett dokuments språk från dess sökväg och första rad, och färgar vyn utan någon insticksmodul. ctx.languages() (ILanguageRegistry) är hur en insticksmodul är med:

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

Ett bidraget språk beter sig precis som ett medföljande: värden löser namnen mot backendlexerns metadata och det aktiva temat levererar färgerna. Att registrera ett id som redan finns ersätter det så länge symbolen lever; när symbolen dör kommer den undanträngda definitionen tillbaka och öppna dokument som använder id:t färgas om — så att en insticksmodul kan åsidosätta ett medföljande språk utan att skada katalogen permanent. Bidrag överlever också den omläsning av katalogen som ett Inställningar ▸ Verkställ eller ett temabyte utlöser. Värden kan inte få nya backendlexrar under körning — lexer måste namnge en som finns. Alltid giltigt: en värd utan färgningslager tillhandahåller en nulloperation som inte räknar upp något och avvisar varje ändring.

Status

Alla fyra kontrakten (diagnostik, pekhjälp, signaturhjälp, symboler) lades till före utgåvan utan att MTE_PLUGIN_API_VERSION höjdes; språkytan (ILanguageRegistry) följde på samma sätt. Deras första förbrukare är LSP-insticksmodulen; symbolkontraktet är med avsikt gjort för flera förbrukare (kommandopaletten i dag, dockan Funktionslista sedan).