Inteligencia de lenguaje (diagnósticos, información al pasar el puntero, ayuda de firma, símbolos)¶
Una pequeña familia de contratos neutrales respecto al backend para la
inteligencia de lenguaje: los datos que un proveedor da de comer a las
superficies de inteligencia de código del editor. Sigue la regla del proyecto
«los plugins aportan datos, el núcleo dibuja»: las líneas onduladas, la
ventana emergente al pasar el puntero y los saltos de cursor viven en la vista
del editor y nunca se exponen a los plugins; un plugin solo suministra datos POD
o std::string, de modo que el mismo contrato lo pueda satisfacer más adelante
un backend de Python.
El primer consumidor es el plugin LSP, pero los canales son genéricos: un analizador estático o un corrector ortográfico pueden publicar diagnósticos de la misma forma.
Qué no está aquí
Ir a la definición, buscar referencias, renombrar y formatear no necesitan
API propia: solo navegan o editan texto, algo que IEditorService ya expresa
(caret, openFile, setCaret, setText), así que siguen siendo comandos
dentro del plugin. El autocompletado reutiliza el ICompletionRegistry
existente. Solo se añade un contrato nuevo cuando hay una superficie de
dibujado o de selección en el núcleo y potencialmente varios proveedores,
que es el caso de los diagnósticos, la información al pasar el puntero, la
ayuda de firma y los símbolos.
Diagnósticos — empuje¶
Un proveedor empuja diagnósticos cuando los tiene (él decide cuándo están
listos los resultados); el editor los dibuja como subrayados ondulados. Llega al
sumidero mediante IPluginContext::diagnostics(). Los tipos de valor son 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;
};
sourcees una clave de sustitución por cada(documento, fuente). Publicar para una fuente sustituye los diagnósticos anteriores de esa fuente en el documento, de modo que varios productores (LSP, corrección ortográfica, registro de compilación) coexistan sin pisarse.- Publica un vector vacío para una fuente para retirar sus diagnósticos.
- Las gravedades se dibujan con colores distintos (error, advertencia, información, sugerencia); el editor además ensancha los intervalos muy cortos para que un marcador de un solo carácter siga siendo visible.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
{ byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});
El anfitrión proporciona siempre un sumidero válido (uno que no hace nada cuando
el editor no tiene superficie de diagnósticos), así que nunca necesitas comprobar
si diagnostics() es nulo.
Información al pasar el puntero — petición asíncrona¶
La información al pasar el puntero la dirige el editor (el ratón se detiene
sobre un símbolo). Un plugin registra un proveedor que responde de forma
asíncrona; llega al registro mediante 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;
}
registerProviderdevuelve unHoverToken(RAII); destrúyelo enshutdown()para anular el registro.- Cada proveedor se registra con una prioridad. Elige la banda que
describe qué respondes, no lo importante que te parezca:
kFallbackpara información a nivel de palabra o de texto (la definición de un diccionario, el destino de una URL),kLanguagepara información del lenguaje sobre el símbolo (el hover de un servidor de lenguaje, la regla de un linter),kActiveSessionpara información que debe ganar mientras está activa (el valor bajo el puntero en un depurador). Se permiten valores entre las bandas (kLanguage + 10) cuando debas ordenarte respecto a un vecino conocido. - Cuando el puntero se detiene, el anfitrión pregunta a todos los proveedores a la vez y muestra la respuesta no vacía del proveedor de mayor prioridad; a igual prioridad gana el proveedor registrado primero. Una respuesta de menor prioridad que llegue antes se retiene hasta que todos los proveedores de mayor prioridad hayan respondido vacío. Si todos responden vacío, no se muestra nada.
- Un proveedor lento no puede retener la información sobre herramientas para
siempre: 300 ms después de la petición se muestra la mejor respuesta
recibida hasta entonces y las respuestas posteriores a esa petición se
descartan. Por eso un proveedor
kActiveSessiondebe responder vacío de inmediato cuando no tiene nada que decir (sin sesión); de lo contrario, retrasa cada hover del editor hasta ese tiempo límite. - Las respuestas que llegan después de que el usuario haya seguido su camino se descartan.
- La respuesta es texto de marcado simple y se dibuja como Markdown en una ventana emergente, así que funcionan los encabezados, la negrita y los bloques de código; mantén el contrato como cadena de entrada y cadena de salida.
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);
Ayuda de firma — petición asíncrona¶
Igual que la información al pasar el puntero, pero dirigida por escribir el ( o
una , de una llamada. Un proveedor responde con la firma activa; el anfitrión
la dibuja como un aviso de llamada con el parámetro activo resaltado. Llega
al registro mediante 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; };
El anfitrión muestra la primera respuesta no vacía; [activeStart, activeEnd)
es el intervalo de bytes del parámetro activo dentro de signature, que se
usa para el resaltado.
Símbolos — petición asíncrona (ir al símbolo)¶
Un proveedor enumera los símbolos de un documento (y busca en el espacio de
trabajo); el anfitrión dibuja el selector (los modos Ctrl+Shift+O / Ctrl+T de
la paleta de comandos) y cualquier plugin puede pedir los símbolos agregados
—el panel de Lista de funciones lo hace—, lo que convierte esto en un contrato
genuinamente multiconsumidor. Se llega a él mediante 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;
};
- Las ubicaciones se expresan tal como navega el anfitrión: una
pathdel sistema de archivos (vacía significa el documento de la petición) más unalineque empieza en 0 y unacolumnen bytes dentro de ella; no hace falta ninguna conversión adicional en el punto de llamada. - El anfitrión entrega la primera lista de símbolos no vacía, o una lista vacía una vez que todos los proveedores han respondido en vacío (de modo que a un selector que espera siempre se le dice algo).
- Lado del consumidor:
requestDocumentSymbols/requestWorkspaceSymbolsen el registro son la contraparte de petición deregisterProvider: un plugin los llama para leer lo que aportan otros plugins (por ejemplo, un servidor de lenguaje). La respuesta se dispara una vez, en el hilo de interfaz, y posiblemente más tarde (asíncrona). El panel de Lista de funciones usa este preguntar y recurrir: muestra al instante su propio esquema por expresiones regulares y, si llega una respuesta no vacía, la adopta; una respuesta vacía nunca borra la base. Protege una respuesta tardía frente al desmontaje y frente a una petición más reciente (identificador de documento + generación).
Lenguajes — enumerar, cambiar, aportar¶
El coloreado de sintaxis es una función del núcleo: el anfitrión posee un
catálogo de colorizadores (los archivos colorizers/*.json distribuidos más los
propios del usuario), detecta el lenguaje de un documento a partir de su ruta y
su primera línea, y colorea la vista sin ningún plugin. ctx.languages()
(ILanguageRegistry) es la forma que tiene un plugin de participar:
// 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 lenguaje aportado se comporta exactamente como uno distribuido: el anfitrión
resuelve los nombres contra los metadatos del analizador léxico del backend y el
tema activo suministra los colores. Registrar un identificador que ya existe lo
sustituye mientras viva el testigo; cuando el testigo muere, la definición
desplazada vuelve y los documentos abiertos que usan ese identificador se
recolorean, de modo que un plugin pueda anular un lenguaje distribuido sin dañar
el catálogo de forma permanente. Las aportaciones sobreviven además a la recarga
del catálogo que provocan un Preferencias ▸ Aplicar o un cambio de tema. El
anfitrión no puede obtener analizadores léxicos nuevos del backend en tiempo de
ejecución: lexer debe nombrar uno que exista. Siempre válido: un anfitrión sin
capa de coloreado suministra una implementación vacía que no enumera nada y
rechaza todos los cambios.
Estado¶
Los cuatro contratos (diagnósticos, información al pasar el puntero, ayuda de
firma y símbolos) se añadieron antes de la publicación sin subir
MTE_PLUGIN_API_VERSION; la superficie de lenguajes (ILanguageRegistry) siguió
el mismo camino. Su primer consumidor es el
plugin LSP; el contrato de símbolos es
intencionadamente multiconsumidor (hoy la paleta de comandos, y a continuación el
panel de Lista de funciones).