Saltar a contenido

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 mismo id y apiVersion que plugin.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; devuelve false solo 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; haz reset() 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 vista TokenId no 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

  • apiVersion en plugin.json y IPlugin::info().apiVersion son iguales a su propia versión mayor, y
  • apiMinorVersion en plugin.json (opcional, 0 de forma predeterminada) y IPlugin::info().apiMinorVersion no superan su propia versión menor. El valor de info() 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 en plugin.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.