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) = 0;
};
  • registerProvider liefert ein HoverToken (RAII) zurück; zerstören Sie es in shutdown(), um die Registrierung aufzuheben.
  • Beim Verweilen fragt der Host jeden registrierten Provider ab und zeigt die erste nicht-leere Antwort; 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);

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

Status

Alle vier Verträge (Diagnostik, Hover, Signaturhilfe, Symbole) wurden vor der Veröffentlichung hinzugefügt, ohne MTE_PLUGIN_API_VERSION zu erhöhen. 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).