Aller au contenu

Intelligence de langage (diagnostics, hover, aide à la signature, symboles)

Une petite famille de contrats, neutre vis-à-vis du backend, pour l'intelligence de langage — les données qu'un fournisseur alimente vers les surfaces d'intelligence de code de l'éditeur. Elle suit la règle du projet « les plugins fournissent des données, le cœur fait le rendu » : les soulignements ondulés, la popup de survol et les sauts de caret vivent dans la vue de l'éditeur et ne sont jamais exposés aux plugins ; un plugin ne fournit que des données POD/std::string, de sorte que le même contrat pourra plus tard être rempli par un backend Python.

Le premier consommateur est le plugin LSP, mais les canaux sont génériques — un linter ou un correcteur orthographique peut publier des diagnostics de la même manière.

Ce qui ne se trouve pas ici

Aller à la définition, rechercher les références, renommer et formater n'ont besoin d'aucune API propre — ils ne font que naviguer ou éditer du texte, ce que IEditorService exprime déjà (caret, openFile, setCaret, setText) ; ils restent donc des commandes internes au plugin. La complétion réutilise l'ICompletionRegistry existant. Un nouveau contrat n'est ajouté que lorsqu'il existe une surface de rendu/sélection dans le cœur et potentiellement plusieurs fournisseurs — ce qui est le cas pour les diagnostics, le survol, l'aide à la signature et les symboles.

Diagnostics — push

Un fournisseur pousse des diagnostics dès qu'il en dispose (c'est lui qui décide quand les résultats sont prêts) ; l'éditeur les rend sous forme de soulignements ondulés. Atteignez le récepteur via IPluginContext::diagnostics(). Les types valeur sont de simples 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 est une clé de remplacement par couple (doc, source). Publier pour une source remplace les diagnostics précédents de cette source sur le document, si bien que plusieurs producteurs (LSP, correction orthographique, log de compilation) coexistent sans s'écraser mutuellement.
  • Publiez un vecteur vide pour une source afin de retirer ses diagnostics.
  • Les sévérités sont rendues avec des couleurs distinctes (error/warning/info/hint) ; l'éditeur élargit aussi les plages très courtes pour qu'un marqueur d'un seul caractère reste visible.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

L'hôte fournit toujours un récepteur valide (un no-op quand l'éditeur n'a pas de surface de diagnostics), vous n'avez donc jamais besoin de vérifier la nullité de diagnostics().

Hover — pull asynchrone

Le survol (hover) est piloté par l'éditeur (pause de la souris sur un symbole). Un plugin enregistre un fournisseur qui répond de manière asynchrone ; atteignez le registre 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) = 0;
};
  • registerProvider renvoie un HoverToken (RAII) ; détruisez-le dans shutdown() pour vous désenregistrer.
  • Lors de la pause de la souris, l'hôte interroge chaque fournisseur enregistré et affiche la première réponse non vide ; les réponses qui arrivent après que l'utilisateur est passé à autre chose sont abandonnées.
  • La réponse est un simple texte de balisage rendu comme Markdown dans une popup, donc les titres, le gras et les blocs de code fonctionnent — gardez le contrat chaîne-en-entrée / chaîne-en-sortie.
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);

Aide à la signature — pull asynchrone

Comme le survol, mais déclenché par la frappe du ( d'un appel ou d'une ,. Un fournisseur répond avec la signature active ; l'hôte la rend sous forme d'info-bulle d'appel (call tip) avec le paramètre actif mis en évidence. Atteignez le registre 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; };

L'hôte affiche la première réponse non vide ; [activeStart, activeEnd) est la plage d'octets du paramètre actif au sein de signature, utilisée pour la mise en évidence.

Symboles — pull asynchrone (aller au symbole)

Un fournisseur énumère les symboles d'un document (et effectue la recherche dans l'espace de travail) ; l'hôte rend le sélecteur (les modes Ctrl+Shift+O / Ctrl+T de la palette de commandes) et tout plugin peut tirer (pull) les symboles agrégés — le panneau Function List le fait, ce qui en fait un contrat véritablement multi-consommateurs. Atteignez-le 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;
};
  • Les emplacements sont exprimés tels que l'hôte navigue : un path du système de fichiers (vide signifie le document de la requête) plus une line en base 0 et une column en octets au sein de celle-ci — aucune conversion supplémentaire n'est nécessaire au point d'appel.
  • L'hôte délivre la première liste de symboles non vide, ou une liste vide une fois que tous les fournisseurs ont répondu vide (ainsi un sélecteur en attente reçoit toujours une réponse).
  • Côté consommateur : requestDocumentSymbols / requestWorkspaceSymbols sur le registre sont le pendant « pull » de registerProvider — un plugin les appelle pour lire ce que d'autres plugins (p. ex. un serveur de langage) contribuent. La réponse se déclenche une fois, sur le thread UI, éventuellement plus tard (asynchrone). Le panneau Function List l'utilise selon le schéma demander-et-se-replier : il affiche instantanément son propre plan basé sur des regex et, si une réponse non vide arrive, s'y met à niveau ; une réponse vide n'efface jamais la base. Protégez une réponse tardive contre le démontage et contre une requête plus récente (id de document + génération).

Statut

Les quatre contrats (diagnostics, survol, aide à la signature, symboles) ont été ajoutés avant la première version sans incrémenter MTE_PLUGIN_API_VERSION. Leur premier consommateur est le plugin LSP ; le contrat de symboles est volontairement multi-consommateurs (la palette de commandes aujourd'hui, le panneau Function List ensuite).