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;
}
registerProviderdevolve umHoverToken(RAII); destrua-o emshutdown()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:
kFallbackpara informação no nível da palavra ou do texto (a definição de um dicionário, o destino de uma URL),kLanguagepara informação da linguagem sobre o símbolo (o hover de um servidor de linguagem, a regra de um linter),kActiveSessionpara 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
kActiveSessionprecisa, 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
pathdo sistema de arquivos (vazio significa o documento da requisição) mais umalinecomeçando em 0 e umacolumnem 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/requestWorkspaceSymbolsno registro são a contraparte de requisição deregisterProvider— 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).