Plugin API — visión general¶
Todos los tipos de PluginApi viven en namespace MTE::plugin. Las cabeceras
públicas no pueden incluir ninguna cabecera de Qt, de Scintilla ni interna de
MTE: solo la biblioteca estándar. La única excepción deliberada es la carpeta
opcional PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h),
para plugins que aportan sus propios widgets de Qt.
El mismo contrato lo satisfacen dos backends: el cargador nativo de Qt
(bibliotecas compartidas) y el backend de Python fuera de proceso
(plugins de Python). Esta sección documenta la
superficie de C++; cada servicio de aquí que sea neutral respecto al backend
tiene su equivalente de Python en ctx.
Ciclo de vida de un plugin¶
Un plugin nativo implementa una clase:
class IPlugin {
public:
virtual PluginInfo info() const = 0; // id, name, version, apiVersion
virtual bool initialize(IPluginContext& ctx) = 0; // register everything here
virtual void shutdown() noexcept = 0; // release every token
};
info()debe devolver el mismoidyapiVersionqueplugin.json; el anfitrión los coteja y se niega a cargar si no coinciden.initialize()se ejecuta una vez al arrancar (después de detectar todos los plugins). Registra aquí los comandos, los menús, los proveedores y los paneles; devuelvefalsesolo ante un fallo real (el anfitrión descarga el plugin y lo anota en el registro).shutdown()se ejecuta al salir (o cuando se está descargando el plugin). No debe lanzar excepciones; hazreset()de todos los testigos y pon a nulo los punteros a servicios que hayas guardado.
Testigos RAII¶
Cada registro devuelve un testigo (CommandToken, MenuToken, DockToken,
SettingsPageToken, Subscription, …): un BasicToken que solo se puede mover
y que posee el registro; destruirlo o hacerle reset() revoca el comando, el
elemento de menú, el panel o la suscripción. Guarda cada testigo como miembro y
libéralo en shutdown(). Importan dos accesores:
id()— una vistaTokenIdno destructiva, para las API que se dirigen al elemento sin tomarlo (p. ej.IMenuRegistry::setItemChecked/setItemEnabled).release()— desvincula y devuelve el identificador; el testigo deja de revocar nada. Rara vez es lo que quieres.
El localizador de servicios¶
initialize() recibe un IPluginContext, un localizador de servicios propiedad
del anfitrión. No guardes el contexto más allá de initialize(); guarda las
referencias a los servicios concretos que necesites (cada servicio sobrevive a
todos los plugins).
| Accesor | Interfaz | Se usa para |
|---|---|---|
ctx.editor() |
IEditorService |
leer y editar el búfer, la selección, el cursor, los archivos, la búsqueda, los marcadores, la sangría y los grupos de vista dividida |
ctx.commands() |
ICommandRegistry |
registrar e invocar acciones de usuario (con atajos) |
ctx.menus() |
IMenuRegistry |
elementos de menú, separadores, conmutadores marcables, atenuar elementos |
ctx.docks() |
IDockRegistry |
paneles laterales acoplables |
ctx.documents() |
IDocumentTypeRegistry |
tipos de pestaña de primera clase de la ventana principal (diff, vista de registro, …) |
ctx.compare() |
ICompareService |
abrir la vista de comparación de dos paneles |
ctx.diagnostics() |
IDiagnosticsSink |
publicar líneas onduladas y glifos de margen |
ctx.events() |
IEventBus |
suscribirse a eventos del editor y publicarlos |
ctx.completions() |
ICompletionRegistry |
aportar candidatos de autocompletado |
ctx.contextMenu() |
IContextMenuRegistry |
entradas dinámicas en el menú contextual del editor |
ctx.snippetSessions() |
ISnippetSessionService |
proporcionar fragmentos expandibles con Tab (el anfitrión ejecuta la sesión de campos) |
ctx.hover() |
IHoverRegistry |
descripciones emergentes al pasar el puntero |
ctx.signatureHelp() |
ISignatureHelpRegistry |
avisos de llamada al escribir ( o , |
ctx.symbols() |
ISymbolRegistry |
símbolos del documento y del espacio de trabajo (ir al símbolo) |
ctx.languages() |
ILanguageRegistry |
listar lenguajes, leer y cambiar el lenguaje de un documento, aportar una definición de lenguaje |
ctx.statusBar() |
IStatusBar |
mensajes pasajeros y segmentos persistentes |
ctx.settings() |
ISettings |
configuración persistente clave/valor por plugin |
ctx.appSettings() |
const ISettings& |
leer los ajustes de toda la aplicación (AppSettingsKeys.h) |
ctx.documentSettings() |
IDocumentSettings |
configuración por documento en capas con .editorconfig |
ctx.settingsRegistry() |
ISettingsRegistry |
añadir una página al diálogo de Preferencias |
ctx.themePalette() |
IThemePalette |
colores de tema con nombre, integración con el Editor de temas |
ctx.lineMarkers() |
ILineMarkerService |
glifos en el margen y tintes de línea que siguen a las ediciones (puntos de interrupción, franjas de cambios); la reclamación del clic en el margen |
ctx.log() |
ILogger |
trace / debug / info / warning / error |
ctx.pluginDataDir() |
std::filesystem::path |
tu directorio privado con permiso de escritura |
ctx.diagnosticsDir() |
std::filesystem::path |
directorio compartido de artefactos de fallo y registros (de solo lectura para los plugins) |
Ampliar la API más adelante significa añadir una nueva interfaz de servicio y su accesor, una adición compatible según la regla de versionado: los plugins existentes se siguen cargando. La cadena de herramientas con la que debe coincidir la compilación de un plugin (compilador, tiempo de ejecución, versión menor de Qt) se lista en Escribir un plugin, «Requisitos de compilación».
IEditorService — el modelo de documento¶
El editor tal como lo ven los plugins: sin Scintilla, sin Qt. Los documentos se
referencian mediante un DocumentId opaco (std::uint64_t, 0 = no válido).
Los desplazamientos son desplazamientos en bytes dentro del texto UTF-8;
MatchRange es un intervalo semiabierto {start, end} y noMatch() significa
«no encontrado»; EditorPosition es un {line, column} que empieza en 0.
// Documents
DocumentId activeDocument() const;
std::vector<DocumentId> openDocuments() const;
void setActiveDocument(DocumentId);
std::optional<std::filesystem::path> filePath(DocumentId) const;
DocumentId openFile(std::filesystem::path);
DocumentId openFileAt(std::filesystem::path, EditorPosition); // jump there
void revealLine(DocumentId, std::int64_t line); // show, don't move
bool saveDocument(DocumentId);
bool closeDocument(DocumentId); // prompts if modified
bool isModified(DocumentId) const;
DocumentEncoding documentEncoding(DocumentId) const;
void setDocumentEncoding(DocumentId, DocumentEncoding);
LineEnding lineEnding(DocumentId) const;
void setLineEnding(DocumentId, LineEnding);
// Text, selection, caret
std::string text(DocumentId) const; // whole buffer
std::string textRange(DocumentId, start, end) const;
void setText(DocumentId, std::string_view);
std::int64_t length(DocumentId) const;
std::string selectedText(DocumentId) const;
void replaceSelection(DocumentId, std::string_view);
MatchRange selectionRange(DocumentId) const;
void setSelection(DocumentId, MatchRange);
EditorPosition caret(DocumentId) const;
void setCaret(DocumentId, EditorPosition);
// Search & replace (SearchOptions: case, whole-word, regex, wrap)
MatchRange findInRange(DocumentId, needle, range, SearchOptions) const;
void replaceTarget(DocumentId, MatchRange, std::string_view);
// Bookmarks, marks, URL highlighting
void toggleBookmark(DocumentId, line); std::vector<std::int64_t> bookmarkedLines(DocumentId) const;
void markRange(DocumentId, MatchRange); void clearMarks(DocumentId);
// Language & indentation
std::string colorizerId(DocumentId) const; // "cpp", "" if none
IndentationStyle indentation(DocumentId) const; // {useTabs, width}
void setIndentation(DocumentId, bool useTabs, int width);
void setWrapIndent(DocumentId, WrapIndentMode); // where wrapped
WrapIndentMode wrapIndent(DocumentId) const; // continuation rows
// start: LeftMargin / SameAsLine / Indented
// Semantic classification (from the active colorizer's style rules;
// cheap -- no re-lex). CategorySpan = {start, length, SyntaxCategory}.
// The spell checker keeps to Comment/Documentation/String spans this way.
std::vector<CategorySpan> textCategories(DocumentId, start, end) const;
// Clipboard
std::string clipboardText() const;
También expone la disposición de grupos de pestañas de la vista dividida en
términos neutrales respecto al backend (groupCount(), groupOf(),
groupWeights(), moveDocumentToGroup(), setGroupWeights()), de modo que un
plugin pueda capturar y restaurar disposiciones de paneles sin ver el divisor de
Qt. Consulta PluginApi/IEditorService.h para la lista completa: la cabecera es
la referencia que manda.
Un documento mostrado en dos paneles es un documento con un único
DocumentId, así que groupOf() solo puede nombrar uno de sus asientos. Dos
métodos describen y reconstruyen una división:
std::vector<int> groupsOf(DocumentId doc) const; // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group); // put it in one more
showDocumentInGroup() activa una vista que ya esté en ese panel y, si no, crea
ahí una segunda vista compartida: lo mismo que hace Ver ▸ Dividir documento
a la derecha, así que no se emite ningún DocumentOpened y ambas vistas editan
un único documento. Los paneles son el concepto de cara a los plugins; las
vistas individuales no lo son, y por eso la llamada nombra un panel en lugar de
devolver una vista.
Llevar al usuario a un lugar¶
openFileAt(path, position) es la única llamada detrás de «ir al error de
compilación», «saltar al marco de pila» o «abrir el resultado de búsqueda», de
modo que todas aterrizan igual. Abre el archivo exactamente como openFile()
(la pestaña de un archivo ya abierto se activa, no se abre por segunda vez),
lo convierte en el documento activo, coloca el cursor sin selección, despliega
un plegado que oculte la línea, desplaza la línea hasta que se vea —
centrada si estaba fuera de la pantalla, sin desplazar si ya era visible —
y da el foco del teclado al editor. La posición se ajusta, nunca se
rechaza: una línea más allá del final cae en la última línea, una columna
más allá del final de la línea cae en su final, los valores negativos cuentan
como 0. Devuelve el documento en el que se colocó el cursor, o 0 si no se
pudo abrir el archivo, y en ese caso no se ha movido nada.
revealLine(doc, line) muestra un lugar sin llevar allí al usuario: despliega
y desplaza el panel que muestra doc, pero no mueve el cursor ni la
selección, no cambia de pestaña y no toma el foco; lo usa un panel que
previsualiza un marco de pila o sigue su propia selección.
Codificación del documento¶
La codificación de caracteres en disco de un documento es estado del núcleo,
no de un plugin. El anfitrión la detecta al leer el archivo y vuelve a escribir la
misma codificación en cada guardado, de modo que un archivo UTF-8-BOM conserva su
marca y uno UTF-16 sigue siendo UTF-16. DocumentEncoding
(PluginApi/DocumentEncoding.h) es un enum POD independiente del backend que
cubre las cinco principales Unicode/ANSI más las páginas de códigos heredadas.
setDocumentEncoding() registra una elección: no cambia ningún byte ni
redescodifica nada —el búfer ya es Unicode— y la codificación nueva surte efecto
en el siguiente guardado. Por eso, un plugin que ofrezca una acción de «convertir
a» debería llamar también a setModified(doc, true); uno que ofrezca «interpretar
como» recarga primero el texto por su cuenta y después registra la elección. El
anfitrión publica DocumentEncodingChanged siempre que el valor cambia de
verdad. Un charset del .editorconfig del proyecto sigue ganando en el momento
de guardar.
Finales de línea del documento¶
El terminador de línea de un documento también es estado del núcleo. El
anfitrión lo detecta al leer el archivo, lo empuja a la vista para que Intro
inserte esa secuencia, y vuelve a escribir los bytes del búfer literalmente: así,
un archivo Unix editado en Windows sigue siendo LF y un guardado sin ediciones es
idéntico byte a byte. LineEnding (PluginApi/LineEnding.h) es un enum POD
independiente del backend —Unknown / Lf / CrLf / Cr— que nombra la
secuencia de bytes en lugar de una constante del backend. Trae dos ayudantes en
línea: lineEndingBytes() y platformLineEnding() (con qué empieza un búfer
nuevo: CRLF en Windows, LF en el resto).
setLineEnding() registra la elección, reajusta la vista del documento para que
el siguiente Intro la inserte, y decide qué escribe el siguiente guardado. No
convierte ningún texto existente: un plugin que reescriba todo el búfer (los
comandos de Conversión de fin de línea) sustituye primero el texto por su cuenta,
de modo que la conversión siga siendo una única edición reversible. El anfitrión
publica DocumentLineEndingChanged siempre que el valor cambia de verdad. Un
end_of_line del .editorconfig del proyecto sigue ganando en el momento de
guardar, y además pasa el documento a esa convención.
Hilos
Todas las llamadas al contexto y a los servicios ocurren en el hilo de interfaz del anfitrión. No llames a los servicios desde un hilo en segundo plano que hayas creado; vuelve primero al hilo correcto.
Versionado¶
El contrato lleva una versión en dos partes, ambas en PluginApi/PluginInfo.h:
constexpr std::uint32_t MTE_PLUGIN_API_VERSION = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor
Lo que decide entre ellas es quién implementa la interfaz:
| Cambio | Tipo | Versión |
|---|---|---|
Un método añadido al final de una interfaz que solo implementa el editor y que los plugins solo llaman (IEditorService, IPluginContext, cada I…Registry, ILineMarkerService, …) |
compatible | menor + 1 |
| Una interfaz nueva, una estructura de evento nueva, un enumerador nuevo al final de una enumeración | compatible | menor + 1 |
Cualquier cambio en una estructura que cruza la frontera (PluginInfo, ContextMenuRequest, DockPanelDescriptor, una estructura de evento) |
incompatible | mayor + 1 |
| Una firma cambiada, un método eliminado o reordenado, una nueva sobrecarga de un nombre existente | incompatible | mayor + 1 |
Cualquier cambio en una interfaz que implementa un plugin (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) |
incompatible | mayor + 1 |
| Una versión compilada con una cadena de herramientas cuya interfaz binaria difiere de la anterior: otra familia de compiladores u otra biblioteca estándar, una nueva versión mayor de Qt | incompatible | mayor + 1 |
Un conjunto de herramientas más reciente de la misma familia de compiladores, otro nivel del estándar de C++ y una versión menor más reciente de Qt no son una ruptura de ese tipo y no cambian ninguno de los dos números; consulta Requisitos de compilación para saber con qué debe coincidir un plugin nativo.
El editor carga un plugin solo cuando
apiVersionenplugin.jsonyIPlugin::info().apiVersionson iguales a su propia versión mayor, yapiMinorVersionenplugin.json(opcional,0de forma predeterminada) yIPlugin::info().apiMinorVersionno superan su propia versión menor. El valor deinfo()es por defecto la versión menor de las cabeceras con las que se compiló el plugin; un plugin que sabe que no usa nada más reciente puede indicar un número menor enplugin.json, y aun así debe superar la segunda comprobación.
Un plugin rechazado sigue apareciendo en Preferencias ▸ Complementos con
el motivo («Incompatible (la API no coincide)», «Necesita una versión más
reciente de OctoEdit») y recibe una entrada en el registro; nunca un fallo. Un
plugin Python no tiene un info() compilado con las cabeceras, así que su
plugin.json es su única declaración: indica la versión menor de la llamada
más reciente que uses.
Política previa a la publicación
Antes de la primera publicación pública, el contrato todavía puede cambiar sin
subir ninguno de los dos números; hasta entonces ambos se quedan en 1 /
0. A partir de la primera publicación, cada cambio se juzga según la tabla
anterior.