Zum Inhalt

Sprachintelligenz (Diagnostik, Hover, Signaturhilfe, Symbole)

Eine kleine, Backend-neutrale Vertragsfamilie für Sprachintelligenz — die Daten, die ein Provider den Code-Intelligenz-Flächen des Editors zuführt. Sie folgt der Projektregel „Plugins liefern Daten, der Kern rendert“: Die Unterschlängelungen, das Hover-Popup und Caret-Sprünge leben in der Editor-Ansicht und werden Plugins niemals offengelegt; ein Plugin liefert nur POD-/std::string-Daten, sodass derselbe Vertrag später von einem Python-Backend erfüllt werden kann.

Der erste Konsument ist das LSP-Plugin, aber die Kanäle sind generisch — ein Linter oder eine Rechtschreibprüfung kann Diagnosen auf demselben Weg veröffentlichen.

Was hier nicht enthalten ist

Go-to-Definition, Find-References, Rename und Formatierung brauchen keine eigene API — sie navigieren oder bearbeiten nur Text, was IEditorService bereits ausdrückt (caret, openFile, setCaret, setText), also bleiben sie Plugin-interne Commands. Die Vervollständigung nutzt das bestehende ICompletionRegistry weiter. Ein neuer Vertrag wird nur hinzugefügt, wenn es eine zentrale Render-/Auswahlfläche im Kern gibt und potenziell mehrere Provider — was für Diagnostik, Hover, Signaturhilfe und Symbole der Fall ist.

Diagnostik — Push

Ein Provider pusht Diagnosen, wann immer er welche hat (er entscheidet, wann Ergebnisse fertig sind); der Editor rendert sie als geschlängelte Unterstreichungen. Den Sink erreichen Sie über IPluginContext::diagnostics(). Die Wertetypen sind reine 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 ist ein Ersetzungsschlüssel pro (doc, source). Das Veröffentlichen für eine Quelle ersetzt die vorherigen Diagnosen dieser Quelle im Dokument, sodass mehrere Produzenten (LSP, Rechtschreibprüfung, Build-Log) koexistieren, ohne sich gegenseitig zu überschreiben.
  • Veröffentlichen Sie einen leeren Vektor für eine Quelle, um deren Diagnosen zurückzuziehen.
  • Schweregrade werden mit unterschiedlichen Farben gerendert (Error/Warning/Info/Hint); der Editor verbreitert außerdem sehr kurze Bereiche, damit eine Ein-Zeichen-Markierung sichtbar bleibt.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Der Host stellt immer einen gültigen Sink bereit (ein No-op, wenn der Editor keine Diagnostikfläche hat), sodass Sie diagnostics() niemals auf null prüfen müssen.

Hover — asynchroner Pull

Hover wird vom Editor gesteuert (Verweilen der Maus über einem Symbol). Ein Plugin registriert einen Provider, der asynchron antwortet; die Registry erreichen Sie über 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 liefert ein HoverToken (RAII) zurück; zerstören Sie es in shutdown(), um die Registrierung aufzuheben.
  • Jeder Provider registriert sich mit einer Priorität. Wählen Sie das Band, das beschreibt, was Sie beantworten, nicht wie wichtig es sich anfühlt: kFallback für Informationen auf Wort- oder Textebene (eine Wörterbuchdefinition, das Ziel einer URL), kLanguage für sprachbezogene Informationen über das Symbol (der Hover eines Language-Servers, die Regel eines Linters), kActiveSession für Informationen, die gewinnen müssen, solange sie aktiv sind (der Wert unter dem Mauszeiger in einem Debugger). Werte zwischen den Bändern (kLanguage + 10) sind erlaubt, wenn Sie sich gegenüber einem bekannten Nachbarn einordnen müssen.
  • Beim Verweilen fragt der Host alle Provider gleichzeitig und zeigt die nicht-leere Antwort des Providers mit der höchsten Priorität; bei gleicher Priorität gewinnt der zuerst registrierte Provider. Eine Antwort niedrigerer Priorität, die zuerst eintrifft, wird zurückgehalten, bis jeder höhere Provider leer geantwortet hat. Antworten alle leer, wird nichts angezeigt.
  • Ein langsamer Provider kann den Tooltip nicht ewig aufhalten: 300 ms nach der Anfrage wird die beste bis dahin erhaltene Antwort gezeigt, und spätere Antworten auf diese Anfrage werden verworfen. Ein kActiveSession-Provider muss deshalb sofort leer antworten, wenn er nichts zu sagen hat (keine Sitzung) — sonst verzögert er jeden Hover im Editor um bis zu diese Zeitspanne.
  • Antworten, die eintreffen, nachdem der Benutzer weitergezogen ist, werden verworfen.
  • Die Antwort ist einfacher Markup-Text und wird als Markdown in einem Popup gerendert, sodass Überschriften, Fettdruck und Codeblöcke funktionieren — halten Sie den Vertrag String-rein / String-raus.
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);

Signaturhilfe — asynchroner Pull

Wie Hover, aber ausgelöst durch das Tippen des ( eines Aufrufs oder eines ,. Ein Provider beantwortet die aktive Signatur; der Host rendert sie als Call-Tip mit hervorgehobenem aktiven Parameter. Die Registry erreichen Sie über 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; };

Der Host zeigt die erste nicht-leere Antwort; [activeStart, activeEnd) ist der Byte-Bereich des aktiven Parameters innerhalb von signature, der für die Hervorhebung verwendet wird.

Symbole — asynchroner Pull (Go-to-Symbol)

Ein Provider zählt die Symbole eines Dokuments auf (und durchsucht den Workspace); der Host rendert den Auswahldialog (die Modi Ctrl+Shift+O / Ctrl+T der Command Palette), und jedes Plugin kann die aggregierten Symbole pullen — das Function-List-Dock tut es, was diesen Vertrag zu einem echten Multi-Konsumenten-Vertrag macht. Sie erreichen ihn über 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;
};
  • Orte werden so ausgedrückt, wie der Host navigiert: ein Dateisystem-path (leer bedeutet das Dokument der Anfrage) plus eine 0-basierte line und eine Byte-column darin — am Aufrufort ist keine weitere Umrechnung nötig.
  • Der Host liefert die erste nicht-leere Symbolliste, oder eine leere Liste, sobald jeder Provider leer geantwortet hat (ein wartender Auswahldialog bekommt also immer eine Antwort).
  • Konsumentenseite: requestDocumentSymbols / requestWorkspaceSymbols auf der Registry sind das Pull-Gegenstück zu registerProvider — ein Plugin ruft sie auf, um zu lesen, was andere Plugins (z. B. ein Language-Server) beisteuern. Die Antwort feuert einmal, auf dem UI-Thread, möglicherweise später (asynchron). Das Function-List-Dock nutzt dieses Fragen-und-Zurückfallen: Es zeigt sofort seine eigene Regex-Gliederung und wertet, falls eine nicht-leere Antwort eintrifft, auf diese auf; eine leere Antwort löscht die Basislinie niemals. Sichern Sie eine späte Antwort gegen den Abbau und gegen eine neuere Anfrage ab (Dokument-Id + Generation).

Sprachen — aufzählen, umschalten, beisteuern

Syntaxfärbung ist ein Kern-Feature: der Host besitzt einen Colorizer-Katalog (die mitgelieferten colorizers/*.json plus die eigenen des Nutzers), erkennt die Sprache eines Dokuments an Pfad und erster Zeile und färbt die Ansicht ohne jedes Plugin. ctx.languages() (ILanguageRegistry) lässt ein Plugin mitmachen:

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

Eine beigesteuerte Sprache verhält sich genau wie eine mitgelieferte: der Host löst die Namen gegen die Metadaten des Backend-Lexers auf, und das aktive Theme liefert die Farben. Das Registrieren einer bereits vorhandenen Id ersetzt sie, solange das Token lebt; stirbt das Token, kehrt die verdrängte Definition zurück und offene Dokumente mit dieser Id werden neu gefärbt — ein Plugin kann also eine mitgelieferte Sprache überschreiben, ohne den Katalog dauerhaft zu beschädigen. Beiträge überleben auch das Neuladen des Katalogs, das Einstellungen ▸ Übernehmen oder ein Theme-Wechsel auslöst. Der Host gewinnt zur Laufzeit keine neuen Backend-Lexer — lexer muss einen vorhandenen nennen. Immer gültig: ein Host ohne Färbungsschicht liefert ein No-op, das nichts aufzählt und jede Änderung ablehnt.

Status

Alle vier Verträge (Diagnostik, Hover, Signaturhilfe, Symbole) wurden vor der Veröffentlichung hinzugefügt, ohne MTE_PLUGIN_API_VERSION zu erhöhen; die Sprach-Oberfläche (ILanguageRegistry) folgte auf demselben Weg. Ihr erster Konsument ist das LSP-Plugin; der Symbolvertrag ist bewusst multi-konsumentenfähig (heute die Command Palette, als Nächstes das Function-List-Dock).