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;
};
sourceist 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;
}
registerProviderliefert einHoverToken(RAII) zurück; zerstören Sie es inshutdown(), 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:
kFallbackfür Informationen auf Wort- oder Textebene (eine Wörterbuchdefinition, das Ziel einer URL),kLanguagefür sprachbezogene Informationen über das Symbol (der Hover eines Language-Servers, die Regel eines Linters),kActiveSessionfü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-basiertelineund eine Byte-columndarin — 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/requestWorkspaceSymbolsauf der Registry sind das Pull-Gegenstück zuregisterProvider— 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).