Vai al contenuto

Intelligenza del linguaggio (diagnostica, suggerimenti al passaggio del mouse, aiuto sulla firma, simboli)

Una piccola famiglia di contratti neutri rispetto al backend per l'intelligenza del linguaggio: i dati che un fornitore dà in pasto alle superfici di intelligenza del codice dell'editor. Segue la regola del progetto «i plugin contribuiscono dati, il nucleo disegna»: le sottolineature ondulate, la finestra a comparsa al passaggio del mouse e i salti del cursore vivono nella vista dell'editor e non sono mai esposti ai plugin; un plugin fornisce solo dati POD / std::string, così che lo stesso contratto possa in seguito essere soddisfatto da un backend Python.

Il primo consumatore è il plugin LSP, ma i canali sono generici: un analizzatore statico o un correttore ortografico può pubblicare diagnostiche allo stesso modo.

Che cosa non c'è qui

Vai alla definizione, trova riferimenti, rinomina e formattazione non richiedono un'API propria: si limitano a navigare o a modificare il testo, cosa che IEditorService già esprime (caret, openFile, setCaret, setText), quindi restano comandi interni al plugin. Il completamento riusa l'ICompletionRegistry esistente. Un nuovo contratto viene aggiunto solo quando esiste una superficie di disegno / di scelta nel nucleo e potenzialmente più fornitori — il che vale per diagnostica, suggerimenti al passaggio del mouse, aiuto sulla firma e simboli.

Diagnostica — a spinta

Un fornitore spinge le diagnostiche non appena le ha (è lui a decidere quando i risultati sono pronti); l'editor le disegna come sottolineature ondulate. Raggiungi il collettore tramite IPluginContext::diagnostics(). I tipi valore sono semplici 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 è una chiave di sostituzione per (doc, source). Pubblicare per una sorgente sostituisce le diagnostiche precedenti di quella sorgente sul documento, così che più produttori (LSP, controllo ortografico, log di compilazione) coesistano senza calpestarsi a vicenda.
  • Pubblica un vettore vuoto per una sorgente per ritirarne le diagnostiche.
  • Le gravità vengono disegnate con colori distinti (errore/avviso/informazione/ suggerimento); l'editor allarga inoltre gli intervalli molto brevi, così che un marcatore di un solo carattere resti visibile.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

L'host fornisce sempre un collettore valido (un'operazione nulla quando l'editor non ha una superficie per le diagnostiche), quindi non devi mai verificare che diagnostics() non sia nullo.

Passaggio del mouse — a richiesta asincrona

Il suggerimento al passaggio del mouse è guidato dall'editor (il puntatore che indugia su un simbolo). Un plugin registra un fornitore che risponde in modo asincrono; raggiungi il registro tramite 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 restituisce un HoverToken (RAII); distruggilo in shutdown() per annullare la registrazione.
  • Ogni fornitore si registra con una priorità. Scegli la fascia che descrive che cosa rispondi, non quanto ti sembri importante: kFallback per informazioni a livello di parola o di testo (la definizione di un dizionario, la destinazione di un URL), kLanguage per informazioni di linguaggio sul simbolo (l'hover di un language server, la regola di un linter), kActiveSession per informazioni che devono prevalere finché sono attive (il valore sotto il puntatore in un debugger). I valori tra le fasce (kLanguage + 10) sono ammessi quando devi posizionarti rispetto a un vicino noto.
  • Quando il puntatore indugia, l'host interroga tutti i fornitori insieme e mostra la risposta non vuota del fornitore con la priorità più alta; a parità di priorità vince il fornitore registrato per primo. Una risposta di priorità inferiore che arriva per prima viene trattenuta finché ogni fornitore di priorità superiore non ha risposto vuoto. Quando tutti rispondono vuoto, non viene mostrato nulla.
  • Un fornitore lento non può trattenere il tooltip all'infinito: 300 ms dopo la richiesta viene mostrata la migliore risposta ricevuta fino a quel momento e le risposte successive a quella richiesta vengono scartate. Un fornitore kActiveSession deve quindi rispondere vuoto subito quando non ha nulla da dire (nessuna sessione), altrimenti ritarda ogni hover dell'editor fino al timeout.
  • Le risposte che arrivano dopo che l'utente si è spostato vengono scartate.
  • La risposta è semplice testo di markup e viene resa come Markdown in una finestra a comparsa, quindi titoli, grassetto e blocchi di codice funzionano: mantieni il contratto stringa in ingresso / stringa in uscita.
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);

Aiuto sulla firma — a richiesta asincrona

Come il passaggio del mouse, ma guidato dalla digitazione della ( o di una , di una chiamata. Un fornitore risponde con la firma attiva; l'host la disegna come un suggerimento di chiamata con il parametro attivo evidenziato. Raggiungi il registro tramite 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'host mostra la prima risposta non vuota; [activeStart, activeEnd) è l'intervallo di byte del parametro attivo dentro signature, usato per l'evidenziazione.

Simboli — a richiesta asincrona (vai al simbolo)

Un fornitore enumera i simboli di un documento (e cerca nell'area di lavoro); l'host disegna il selettore (le modalità Ctrl+Shift+O / Ctrl+T della tavolozza dei comandi) e qualsiasi plugin può richiedere i simboli aggregati — lo fa il pannello Elenco delle funzioni, il che rende questo un contratto davvero multi-consumatore. Raggiungilo tramite 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;
};
  • Le posizioni sono espresse come l'host naviga: un path del filesystem (vuoto significa il documento della richiesta) più una line a base 0 e una column in byte al suo interno — nessuna ulteriore conversione nel punto di chiamata.
  • L'host consegna il primo elenco di simboli non vuoto, oppure un elenco vuoto una volta che ogni fornitore ha risposto vuoto (così a un selettore in attesa viene sempre detto qualcosa).
  • Lato consumatore: requestDocumentSymbols / requestWorkspaceSymbols sul registro sono la controparte a richiesta di registerProvider — un plugin le chiama per leggere ciò che altri plugin (per esempio un language server) contribuiscono. La risposta scatta una volta, sul thread dell'interfaccia, eventualmente più tardi (asincrona). Il pannello Elenco delle funzioni usa questo schema di chiedi e ripiega: mostra istantaneamente il proprio schema basato su espressioni regolari e, se arriva una risposta non vuota, passa a quella; una risposta vuota non azzera mai la base. Proteggi una risposta tardiva dallo smontaggio e da una richiesta più recente (id del documento + generazione).

Linguaggi — enumerare, cambiare, contribuire

La colorazione della sintassi è una funzionalità del nucleo: l'host possiede un catalogo di colorizzatori (i file colorizers/*.json distribuiti più quelli dell'utente), rileva il linguaggio di un documento dal suo percorso e dalla prima riga, e colora la vista senza alcun plugin. ctx.languages() (ILanguageRegistry) è il modo in cui un plugin partecipa:

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

Un linguaggio contribuito si comporta esattamente come uno distribuito: l'host risolve i nomi rispetto ai metadati del lexer del backend e il tema attivo fornisce i colori. Registrare un id che esiste già lo sostituisce per tutta la vita del token; quando il token muore la definizione spodestata torna e i documenti aperti che usano quell'id vengono ricolorati — così un plugin può scavalcare un linguaggio distribuito senza danneggiare in modo permanente il catalogo. I contributi sopravvivono anche alla ricarica del catalogo innescata da un Preferenze ▸ Applica o da un cambio di tema. L'host non può acquisire nuovi lexer di backend a tempo di esecuzione: lexer deve nominarne uno esistente. Sempre valido: un host senza uno strato di colorazione fornisce un'operazione nulla che non enumera nulla e rifiuta ogni modifica.

Stato

Tutti e quattro i contratti (diagnostica, passaggio del mouse, aiuto sulla firma, simboli) sono stati aggiunti prima del rilascio senza incrementare MTE_PLUGIN_API_VERSION; la superficie dei linguaggi (ILanguageRegistry) ha seguito la stessa strada. Il loro primo consumatore è il plugin LSP; il contratto dei simboli è intenzionalmente multi-consumatore (oggi la tavolozza dei comandi, poi il pannello Elenco delle funzioni).