Pular para conteúdo

Inteligência de linguagem (diagnósticos, dicas ao passar o mouse, ajuda de assinatura, símbolos)

Uma pequena família de contratos neutros em relação ao backend para a inteligência de linguagem — os dados que um provedor alimenta às superfícies de inteligência de código do editor. Ela segue a regra do projeto "plugins contribuem dados, o núcleo renderiza": os traços ondulados, o popup de dica e os saltos de cursor vivem na visualização do editor e nunca são expostos aos plugins; um plugin apenas fornece dados POD ou std::string, de modo que o mesmo contrato possa ser atendido depois por um backend Python.

O primeiro consumidor é o plugin LSP, mas os canais são genéricos — um analisador estático ou um verificador ortográfico pode publicar diagnósticos do mesmo jeito.

O que não está aqui

Ir para a definição, localizar referências, renomear e formatar não precisam de API própria — eles apenas navegam ou editam texto, o que o IEditorService já expressa (caret, openFile, setCaret, setText), então permanecem como comandos dentro do plugin. O autocompletar reutiliza o ICompletionRegistry existente. Um contrato novo só é acrescentado quando existe uma superfície de renderização/seleção no núcleo e potencialmente vários provedores — que é o caso dos diagnósticos, das dicas ao passar o mouse, da ajuda de assinatura e dos símbolos.

Diagnósticos — empurrar

Um provedor empurra diagnósticos sempre que os tem (ele decide quando os resultados estão prontos); o editor os renderiza como sublinhados ondulados. Alcance o coletor por IPluginContext::diagnostics(). Os tipos de valor são POD simples (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 é uma chave de substituição por (documento, fonte). Publicar para uma fonte substitui os diagnósticos anteriores daquela fonte no documento, então vários produtores (LSP, verificação ortográfica, log de build) coexistem sem atropelar uns aos outros.
  • Publique um vetor vazio para uma fonte para retirar os diagnósticos dela.
  • As severidades são renderizadas com cores distintas (erro/aviso/informação/dica); o editor também alarga os intervalos muito curtos, para que um marcador de um só caractere continue visível.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

O host sempre fornece um coletor válido (um que não faz nada quando o editor não tem superfície de diagnósticos), então você nunca precisa checar se diagnostics() é nulo.

Dica ao passar o mouse — requisição assíncrona

A dica ao passar o mouse é conduzida pelo editor (o mouse pousa sobre um símbolo). Um plugin registra um provedor que responde de forma assíncrona; alcance o registro por 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 devolve um HoverToken (RAII); destrua-o em shutdown() para cancelar o registro.
  • Cada provedor se registra com uma prioridade. Escolha a faixa que descreve o que você responde, não o quanto parece importante: kFallback para informação no nível da palavra ou do texto (a definição de um dicionário, o destino de uma URL), kLanguage para informação da linguagem sobre o símbolo (o hover de um servidor de linguagem, a regra de um linter), kActiveSession para informação que precisa vencer enquanto está ativa (o valor sob o ponteiro em um depurador). Valores entre as faixas (kLanguage + 10) são permitidos quando você precisa se posicionar em relação a um vizinho conhecido.
  • Quando o ponteiro pousa, o host pergunta a todos os provedores de uma vez e exibe a resposta não vazia do provedor de maior prioridade; em caso de prioridade igual, vence o provedor registrado primeiro. Uma resposta de prioridade menor que chegue antes fica retida até que todo provedor de prioridade maior tenha respondido vazio. Quando todos respondem vazio, nada é exibido.
  • Um provedor lento não consegue segurar a dica para sempre: 300 ms depois da solicitação, a melhor resposta recebida até ali é exibida e as respostas posteriores para essa solicitação são descartadas. Um provedor kActiveSession precisa, portanto, responder vazio na hora quando não tem nada a dizer (sem sessão) — senão atrasa cada hover do editor em até esse tempo limite.
  • Respostas que chegam depois de o usuário ter seguido em frente são descartadas.
  • A resposta é texto de marcação simples e é renderizada como Markdown em um popup, então títulos, negrito e blocos de código funcionam — mantenha o contrato como cadeia de entrada e cadeia de saída.
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);

Ajuda de assinatura — requisição assíncrona

Como a dica ao passar o mouse, mas conduzida por digitar o ( ou uma , de uma chamada. Um provedor responde com a assinatura ativa; o host a renderiza como uma dica de chamada com o parâmetro ativo destacado. Alcance o registro por 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; };

O host exibe a primeira resposta não vazia; [activeStart, activeEnd) é o intervalo de bytes do parâmetro ativo dentro de signature, usado para o destaque.

Símbolos — requisição assíncrona (ir para símbolo)

Um provedor enumera os símbolos de um documento (e pesquisa no espaço de trabalho); o host renderiza o seletor (os modos Ctrl+Shift+O / Ctrl+T da paleta de comandos) e qualquer plugin pode puxar os símbolos agregados — o painel de Lista de funções faz isso, o que torna este um contrato genuinamente multiconsumidor. Alcance-o por 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;
};
  • As localizações são expressas do jeito que o host navega: um path do sistema de arquivos (vazio significa o documento da requisição) mais uma line começando em 0 e uma column em bytes dentro dela — sem necessidade de conversão adicional no ponto de chamada.
  • O host entrega a primeira lista de símbolos não vazia, ou uma lista vazia assim que todo provedor tiver respondido vazio (de modo que um seletor à espera sempre recebe alguma coisa).
  • Lado do consumidor: requestDocumentSymbols / requestWorkspaceSymbols no registro são a contraparte de requisição de registerProvider — um plugin os chama para ler o que outros plugins (por exemplo, um servidor de linguagem) contribuem. A resposta dispara uma vez, na thread de interface, possivelmente mais tarde (assíncrona). O painel de Lista de funções usa este perguntar-e-recorrer: ele exibe na hora a própria estrutura por expressões regulares e, se chegar uma resposta não vazia, adota-a; uma resposta vazia nunca apaga a base. Proteja uma resposta atrasada contra o desmonte e contra uma requisição mais nova (id do documento + geração).

Linguagens — enumerar, trocar, contribuir

A coloração de sintaxe é um recurso do núcleo: o host possui um catálogo de colorizadores (os arquivos colorizers/*.json distribuídos mais os do próprio usuário), detecta a linguagem de um documento pelo caminho e pela primeira linha, e colore a visualização sem plugin algum. ctx.languages() (ILanguageRegistry) é como um plugin participa:

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

Uma linguagem contribuída se comporta exatamente como uma distribuída: o host resolve os nomes contra os metadados do analisador léxico do backend e o tema ativo fornece as cores. Registrar um id que já existe o substitui enquanto o token viver; quando o token morre, a definição deslocada volta e os documentos abertos que usam o id são recoloridos — então um plugin pode substituir uma linguagem distribuída sem danificar o catálogo permanentemente. As contribuições também sobrevivem à recarga do catálogo que um Preferências ▸ Aplicar ou uma troca de tema dispara. O host não pode ganhar analisadores léxicos novos de backend em tempo de execução — lexer precisa nomear um que exista. Sempre válido: um host sem camada de coloração fornece uma implementação vazia que não enumera nada e recusa toda alteração.

Status

Os quatro contratos (diagnósticos, dica ao passar o mouse, ajuda de assinatura e símbolos) foram acrescentados antes do lançamento sem elevar o MTE_PLUGIN_API_VERSION; a superfície de linguagens (ILanguageRegistry) seguiu o mesmo caminho. O primeiro consumidor deles é o plugin LSP; o contrato de símbolos é intencionalmente multiconsumidor (hoje a paleta de comandos, a seguir o painel de Lista de funções).