Crear un plugin de Python para Modular Text Editor con Claude¶
Público: un asistente de IA (Claude / ChatGPT / etc.). Esta página es un conjunto de instrucciones autocontenido: entrégasela al asistente junto con la tarea («por favor, escríbeme un plugin que haga X») y el asistente debería producir una carpeta de plugin completa, correcta y funcional sin necesidad de leer el resto de la documentación.
Si eres una persona que intenta escribir un plugin a mano, empieza aquí: esta página está optimizada para el consumo por parte de una IA y se salta los ejemplos amables.
1. Qué estás produciendo¶
Una carpeta con exactamente dos archivos (más extras opcionales):
Devuelve la carpeta como un bloque de código por archivo. El usuario o bien deja
la carpeta en el directorio de plugins del editor, o bien comprime la carpeta y
renombra el archivo como <name>.mteplugin y después lo instala mediante
Preferencias → Complementos → Instalar un complemento.... Ambas vías necesitan un reinicio para
cargar.
Los dos archivos se describen exhaustivamente más abajo. Sigue las reglas literalmente: el anfitrión realiza un sondeo estático y rechaza los plugins que las incumplen.
2. plugin.json — plantilla canónica¶
{
"id": "org.example.<short-name>",
"name": "<Human Display Name>",
"version": "1.0.0",
"vendor": "<Author or Team>",
"description": "<One sentence about what it does.>",
"apiVersion": 1,
"entry": "python",
"module": "main",
"order": 900,
"menus": [
{ "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
}
Reglas estrictas¶
"apiVersion"debe ser1(la versión actual del anfitrión)."entry"debe ser"python": ninguna otra cosa se encamina a este backend."module"debe ser"main"salvo que haya un motivo explícito para darle otro nombre al módulo, y nunca una ruta con puntos (la fase 1 solo admite nombres de módulo planos)."id"debe tener estilo DNS inverso (org.<vendor>.<name>) y ser único a nivel global entre los plugins instalados del usuario. Si el usuario no indicó un fabricante, usaorg.example..menus[]debe contener al menos una entrada cuyotitlecoincida con el segmento más a la izquierda de todas las rutas de menú que uses enmain.py. Prefiere"Plugins"para cualquier cosa de cara al usuario que no pertenezca obviamente a un menú del núcleo.
Campos opcionales¶
"pyRequires": []— una matriz de cadenas de requisitos de pip (p. ej.["requests>=2.31"]). Cuando no está vacía, el editor aprovisiona un entorno virtual por plugin en el primer arranque (pip install, que necesita red esa única vez) y ejecuta el plugin bajo él. Prefiere la biblioteca estándar cuando baste (sin retraso de aprovisionamiento, funciona sin conexión); declarapyRequirescuando la tarea necesite de verdad un paquete de terceros, y avisa al usuario de que el primer arranque se detendrá para instalarlo."permissions": []— declaraciones gruesas de capacidades (p. ej.["network"]), que se muestran en modo de solo lectura en la página de Plugins. Declara"network"cuando el plugin haga llamadas hacia fuera; es un etiquetado honesto para el usuario, no una imposición."requires": []— identificadores de OTROS plugins sin los que este no puede funcionar (no paquetes pip). Se inician primero; si falta alguno o está desactivado, este plugin no se inicia y la página de Plugins explica por qué. Déjalo vacío salvo que la tarea del usuario dependa de verdad de otro plugin."apiMinorVersion": 0— la versión menor mínima de la API del editor que necesita el plugin. Déjala en0salvo que uses una llamada documentada como más reciente.
Valores que NO debes inventar¶
- Cualquier campo que no esté listado arriba. El anfitrión ignora las claves desconocidas; añadirlas es un riesgo de mantenimiento.
3. main.py — esqueleto canónico¶
"""<Short description of what this plugin does.>"""
import mte # noqa: F401 -- pulls in dataclasses for event payloads
def register(ctx):
# Register commands, menu items, and event subscriptions here.
# Everything below is illustrative -- delete what you don't need.
@ctx.commands.command(id="<id>.<verb>",
title="<Human Label>",
shortcut="Ctrl+Alt+<letter>", # optional
category="<Group>") # optional
def _handler():
ctx.status.show("hello", timeout_ms=2000)
ctx.log.info("<id>: handler ran")
ctx.menus.add_item("Plugins/<Human Label>/<Item>", "<id>.<verb>")
Reglas estrictas¶
register(ctx)DEBE ser una función de nivel superior. El sondeo estático exige una línea que empiece pordef register(en la columna 0. Si la anidas dentro de una clase, la decoras en el ámbito del módulo o la hacesasync def, el plugin se rechaza en silencio en el momento de la detección.- Importa desde
mtecuando te suscribas a eventos: las dataclasses tipadas (mte.DocumentSaved, etc.) vienen de ese paquete.import mteal principio es seguro aunque no lo uses directamente; el# noqa: F401de arriba evita el ruido del analizador de estilo. - Nunca uses
print(). La salida estándar del trabajador es el canal RPC; escribir en ella corrompe el protocolo. Usactx.log.info / warning / error. - Nunca importes nada que no hayas declarado. Más allá de la biblioteca
estándar y de
mte, toda importación de terceros debe aparecer en elpyRequiresdeplugin.json(el editor la instala con pip en el entorno virtual del plugin en el primer arranque) o estar incluida dentro de la carpeta del plugin. Una importación no declarada lanzaModuleNotFoundErroren la máquina del usuario. - Los manejadores de comandos y de eventos pueden bloquear brevemente: se
ejecutan en el grupo de hilos del trabajador, así que las llamadas síncronas a
ctx.editor/ctx.settings/ctx.data_dirson legales. Pero las retrollamadas de los proveedores de autocompletado (@ctx.completions.provider(...)) NO DEBEN bloquear: se ejecutan en el bucle de eventos con un plazo de ~30 ms. Sin entrada/salida, sin llamadas actx.editor.*, solo caché. Construye los índices en otro sitio (p. ej. endocument.opened) y responde desde un diccionario. - No importes Qt, PySide, PyQt ni Scintilla. No están disponibles en el trabajador.
- Los identificadores de comando deben llevar el prefijo del espacio de
nombres de tu plugin, para evitar colisiones con el núcleo o con otros plugins.
<id>.<verb>como arriba; p. ej.wordcount.count,hello.sayHi. - Las rutas de menú deben empezar por un menú de primer nivel que también
hayas declarado en el
menus[]deplugin.json. Convención:"Plugins/<Plugin Name>/<Verb>".
4. API completa — todo lo que hay en ctx¶
Tienes dieciocho servicios: commands, menus, events, log, status, editor,
languages, settings, app_settings, document_settings, theme, diagnostics,
compare, completions, docks, settings_page, ui y
data_dir()/diagnostics_dir(). Todo lo que no esté listado aquí no existe: no
inventes servicios. (Los entornos virtuales por plugin se declaran mediante
pyRequires, no con un servicio de ctx; el sustrato de protocolo con flujo y
cancelación todavía no tiene API de cara a los plugins.)
ctx.commands¶
@ctx.commands.command(*, id, title, shortcut="", category="",
requires_editor=False, requires_writable_editor=False)
def _handler(): ...
Decorador. Registra un comando y enlaza _handler (una función sin argumentos que
devuelve None). Las excepciones en _handler las captura el trabajador y las
escribe en stderr; no hacen fallar el editor.
Pon requires_editor cuando el comando lea el documento activo (búfer, selección,
cursor) y requires_writable_editor cuando cambie el texto. El anfitrión atenúa
entonces el comando allí donde no podría funcionar —en una pestaña sin documento
(un diff o una vista de registro) y, con el indicador de escritura, en un
documento de solo lectura— en lugar de dejar que el usuario invoque algo que no
hace nada. El indicador de escritura implica al otro; deja ambos apagados para
comandos que solo abren un diálogo o un panel.
ctx.menus¶
ctx.menus.add_item(path, command_id) # -> handler_id (str)
ctx.menus.add_separator(path) # -> handler_id (str)
ctx.menus.set_item_checked(handler_id, checked) # -> None
ctx.menus.set_item_enabled(handler_id, enabled) # -> None
path es "Superior/Sub/…/Etiqueta". Los submenús intermedios se crean bajo
demanda.
set_item_checked convierte el elemento en una entrada de menú marcable y fija su
marca. Úsalo para conmutadores persistentes de encendido/apagado. La primera
llamada lo promueve; las posteriores solo cambian la marca. No hace nada en
silencio con identificadores desconocidos.
set_item_enabled atenúa el elemento. Úsalo cuando el comando necesite una
condición previa, para que una entrada no disponible lo parezca en lugar de no
hacer nada al pulsarla. Es idempotente; los elementos empiezan activados; no hace
nada en silencio con identificadores desconocidos.
ctx.events¶
Diecisiete temas, cada uno con un decorador azucarado y una forma en bruto.
| Azucarado | Tema en bruto | Carga útil |
|---|---|---|
@ctx.events.on_document_opened |
document.opened |
mte.DocumentOpened(document: int, path: str) |
@ctx.events.on_document_closed |
document.closed |
mte.DocumentClosed(document: int) |
@ctx.events.on_document_saved |
document.saved |
mte.DocumentSaved(document: int) |
@ctx.events.on_document_modified_changed |
document.modifiedChanged |
mte.DocumentModifiedChanged(document: int, modified: bool) |
@ctx.events.on_document_encoding_changed |
document.encodingChanged |
mte.DocumentEncodingChanged(document: int, encoding: mte.DocumentEncoding) |
@ctx.events.on_document_line_ending_changed |
document.lineEndingChanged |
mte.DocumentLineEndingChanged(document: int, ending: mte.LineEnding) |
@ctx.events.on_text_changed |
text.changed |
mte.TextChanged(document, position, added_bytes, removed_bytes) — en cada edición, mantenlo trivial |
@ctx.events.on_selection_changed |
selection.changed |
mte.SelectionChanged(document: int) |
@ctx.events.on_active_document_changed |
document.activeChanged |
mte.ActiveDocumentChanged(document: int) |
@ctx.events.on_settings_changed |
settings.changed |
mte.SettingsChanged(scope: str) |
@ctx.events.on_theme_changed |
theme.changed |
mte.ThemeChanged(variant: str) |
@ctx.events.on_workspace_root_changed |
workspace.rootChanged |
mte.WorkspaceRootChanged(root: str) |
@ctx.events.on_workspace_files_changed |
workspace.filesChanged |
mte.WorkspaceFilesChanged(files: list[mte.WorkspaceFile]) |
@ctx.events.on_command_shortcut_changed |
command.shortcutChanged |
mte.CommandShortcutChanged(command_id: str, sequence: str) |
@ctx.events.on_host_window_shown |
host.windowShown |
mte.HostWindowShown() — una vez por ejecución, después del primer fotograma |
@ctx.events.on_app_about_to_quit |
app.aboutToQuit |
mte.AppAboutToQuit() |
@ctx.events.on_margin_clicked |
margin.clicked |
mte.MarginClicked(document: int, line: int, margin: str, modifiers: int): un clic en la columna de marcadores; bits de modifiers: mte.MODIFIER_* |
Forma en bruto:
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
No inventes temas nuevos: la forma en bruto los valida y lanza ValueError con
cualquier cosa que no esté en la tabla.
ctx.log¶
Se lanza y se olvida. Las líneas llevan el prefijo [python:<plugin.id>] en el
registro de diagnóstico del anfitrión.
ctx.status¶
ctx.status.show(message: str, timeout_ms: int = 0, level: str = "info")
ctx.status.clear()
seg = ctx.status.add_segment(id: str, priority: int = 100) # persistent
seg.set_text(str); seg.set_visible(bool); seg.remove()
level debe ser "info" / "warning" / "error". timeout_ms=0 significa que
el anfitrión elige un valor predeterminado. show() es para notificaciones
pasajeras; todo lo que se quede en pantalla (un recuento en vivo, un indicador de
modo) es un segmento: los dos nunca se pisan.
ctx.editor (fase 2)¶
Todos los métodos son síncronos: bloquean el hilo que llama. Está bien desde
una retrollamada def de comando o de evento (el trabajador despacha los
manejadores síncronos en un grupo de hilos); es ilegal desde manejadores
async def y desde los proveedores de ctx.completions (que se ejecutan
directamente en el bucle de eventos).
Los documentos se referencian mediante identificadores int opacos (0 = ningún
documento).
doc = ctx.editor.active_document()
docs = ctx.editor.open_documents()
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc) # str or None
text = ctx.editor.text(doc)
slice = ctx.editor.text_range(doc, start, end) # [start, end) bytes
ctx.editor.set_text(doc, "new text")
n = ctx.editor.length(doc)
sel = ctx.editor.selection_range(doc) # mte.MatchRange
s = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "hi")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))
pos = ctx.editor.caret(doc) # mte.EditorPosition
ctx.editor.set_caret(doc, mte.EditorPosition(line=5, column=0))
new_doc = ctx.editor.open_file("/path/to/file") # 0 on failure
here = ctx.editor.open_file_at("/path/to/file", mte.EditorPosition(41, 0)) # open + caret + scroll + focus
ctx.editor.reveal_line(doc, 120) # scroll only; caret and focus stay
ok = ctx.editor.save_document(doc)
closed = ctx.editor.close_document(doc) # prompts if modified; False = cancelled
dirty = ctx.editor.is_modified(doc)
hit = ctx.editor.find_in_range(doc, "TODO", 0,
ctx.editor.length(doc),
mte.SearchOptions(match_case=True))
new_end = ctx.editor.replace_target(doc, hit, "DONE",
mte.SearchOptions())
style = ctx.editor.indentation(doc) # IndentationStyle
ctx.editor.set_indentation(doc,
mte.IndentationStyle(use_tabs=False, width=2))
ctx.editor.set_wrap_indent(doc, "same") # "left" | "same" | "indent"
lang = ctx.editor.colorizer_id(doc) # "python", "cpp", ...
mte.SearchOptions.mode: "normal" / "extended" / "regex".
mte.SearchOptions.direction: "forward" / "backward".
ctx.settings / ctx.app_settings (fase 2)¶
Almacén tipado de clave/valor acotado por plugin (ctx.settings), más una vista de
solo lectura de los ajustes de toda la aplicación (ctx.app_settings). El tipo se
infiere del tipo de Python del valor predeterminado.
ctx.settings.set("wrap", True)
ctx.settings.set("count", 42)
ctx.settings.set("theme", "dark")
wrap = ctx.settings.get("wrap", False) # bool
count = ctx.settings.get("count", 0) # int
theme = ctx.settings.get("theme", "light") # str
ctx.settings.contains("wrap")
ctx.settings.remove("stale")
# App-wide (read-only)
tw = ctx.app_settings.get("editor.tabWidth", 4)
ctx.app_settings.set/remove/sync lanzan RuntimeError. En tu propia lógica,
comprueba bool antes que int al leer (en Python,
isinstance(True, int) == True).
ctx.completions (fase 2)¶
@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
for w in ("hello", "hello_world")
if w.startswith(req.prefix)]
Campos de req: document, prefix, position, language_id, manual. Tipos:
KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.
Devuelve list[CompletionItem], cadenas simples o diccionarios. La retrollamada
DEBE devolver el control en ~30 ms o el anfitrión descarta tus resultados para
esa pulsación: sin entrada/salida, sin ctx.editor.text(...), solo caché.
Construye los índices en otro sitio (p. ej. en document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (fase 2)¶
Usa data_dir() para cachés e índices. diagnostics_dir() es de solo lectura (ahí
escribe el informador de fallos).
ctx.docks — paneles acoplables declarativos (fase 3)¶
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
def _on_needle_changed(value: str) -> None:
ctx.log.info(f"needle -> {value!r}")
needle_handler = ctx.docks.on_change(_on_needle_changed)
panel_id = ctx.docks.add_panel(
id="myplugin.panel", # returned; use it for ctx.ui.get / ctx.ui.set
title="My Plugin",
area="right", # "left" / "right" / "top" / "bottom"
form=Column([
Label(text="Search:"),
LineEdit(id="needle", placeholder="text...",
binds="settings:lastNeedle",
on_change=needle_handler),
Checkbox(id="cs", label="Case sensitive",
binds="settings:caseSensitive"),
Row([Button(text="Run", on_click="myplugin.run")]),
]),
)
Construye la especificación de formulario con los ayudantes de
mte.ui. El vocabulario de controles es fijo: Label / Button /
Checkbox / LineEdit / NumberSpin / ComboBox / List dentro de Column / Row /
Group.
binds="settings:<key>"— el valor del control se guarda automáticamente a través dectx.settings(el mismo almacén quectx.settings.set).on_click="<cmd.id>"— solo en Button; invoca un comando registrado.on_change=<handler>— asígnalo mediantectx.docks.on_change(callback); se dispara con las ediciones del usuario.
Las retrollamadas de on_change se ejecutan en el bucle de eventos del trabajador
(NO en el grupo de hilos). Mantenlas baratas: sin llamadas al editor, sin trabajo
largo. Cambia un pequeño estado en memoria, escribe en el registro o dispara un
comando; haz el trabajo pesado desde el comando.
Pasa initially_visible=False para un panel que no deba aparecer al arrancar y
después muéstralo desde su comando con
ctx.docks.set_panel_visible(panel_id, True) (que además lo trae al frente; es
idempotente; los identificadores desconocidos se ignoran).
ctx.docks.is_panel_visible(panel_id) dice si un panel está abierto (una
pestaña en segundo plano cuenta). Varios paneles que van juntos: pasa el
mismo tab_group="<plugin.id>/panels" y la misma area, y compartirán un
único panel acoplable con pestañas.
ctx.line_markers — marcas en las líneas del documento¶
kind = ctx.line_markers.register_kind(glyph=mte.GLYPH_CIRCLE,
glyph_color="<category id>", # from ctx.theme
line_tint="", z_order=0) # RpcError when no slot is left
m = ctx.line_markers.add_marker(kind, doc, line) # 0 = refused; follows edits
ctx.line_markers.line_of(m) # -1 = gone (document closed)
ctx.line_markers.markers(kind, doc) # [mte.LineMarkerPlacement(id, line)]
ctx.line_markers.remove_marker(m)
claim = ctx.line_markers.claim_margin_click() # stop bookmark toggling on margin clicks
Registra los tipos una sola vez en register(); el editor solo tiene unas
pocas ranuras. Reclama el clic en el margen solo cuando el plugin le dé un
significado: los clics llegan por @ctx.events.on_margin_clicked en
cualquier caso.
ctx.languages — coloreado de sintaxis¶
ctx.languages.available() # list[LanguageInfo(id, name)]
ctx.languages.language_for(doc) # "" when uncoloured
ctx.languages.set_language_for(doc, "json") # "" clears; False = unknown id
ctx.languages.auto_detect_for(doc)
h = ctx.languages.register(id=, name=, lexer=, extensions=[...],
keywords=[{"group": ..., "words": ...}],
styles={"<LEXER STYLE NAME>": "comment", ...})
ctx.languages.unregister(h) # restores what it displaced
lexer debe nombrar un analizador léxico del backend que ya exista; styles y
keywords usan los nombres propios del analizador (la misma forma que los
colorizers/*.json distribuidos), nunca identificadores numéricos.
ctx.document_settings / mte.keys¶
ctx.document_settings.get_bool(doc, key, default) # .editorconfig-layered, read-only
ctx.document_settings.get_int / get_double / get_string / contains
ctx.app_settings.get(mte.keys.EDITOR_FONT_SIZE, mte.keys.DEFAULT_EDITOR_FONT_SIZE)
ctx.diagnostics — líneas onduladas¶
ctx.diagnostics.publish(doc, source: str, [mte.Diagnostic(start=, end=,
message=, severity=mte.SEVERITY_WARNING, code="")])
ctx.diagnostics.publish(doc, source, []) # retract only your source
ctx.diagnostics.clear(doc) # clears EVERY producer's set
Envía cada vez el conjunto COMPLETO de tu source; sustituye al anterior.
source es obligatorio.
ctx.theme — colores adaptables al tema para tu propia interfaz¶
h = ctx.theme.register_category(id, display_label, default_light=0x..., default_dark=0x...)
rgb = ctx.theme.resolve_color(id) # 0xRRGGBB; re-read on on_theme_changed
ctx.theme.has_override(id); ctx.theme.active_variant(); ctx.theme.categories()
ctx.compare — diff en paralelo¶
ctx.compare.open(mte.CompareSource(title, path=..., content=...),
mte.CompareSource(...), ignore_whitespace=False,
ignore_case=False, ignore_eol=False) # -> bool
ctx.events.publish¶
Un plugin solo puede publicar workspace.rootChanged /
workspace.filesChanged; cualquier otra cosa lanza ValueError. A todos los
eventos del anfitrión salvo document.aboutToSave se puede uno suscribir
(consulta la lista de decoradores); el evento nativo DocumentAboutToClose
no tiene ningún tema en Python: usa document.closed.
ctx.settings_page — páginas de Preferencias declarativas (fase 3)¶
from mte.ui import Column, Checkbox, NumberSpin
ctx.settings_page.register(
category="Plugins/My Plugin", # slash-separated tree path
title="My Plugin",
form=Column([
Checkbox(id="verbose", label="Verbose logging",
binds="settings:verbose"),
NumberSpin(id="cap", default=100, min=1, max=10_000,
binds="settings:cap"),
]),
)
Los mismos constructores, la misma semántica de binds. Dos controles que
comparten un binds="settings:<key>" se reflejan mutuamente en tiempo real: el
panel acoplable y la página de Preferencias pueden tener ambos una casilla para el
mismo ajuste, y cambiar una cambia la otra.
Salvedad de la fase 3
Los valores se guardan con avidez en cada edición del usuario: Cancelar no revierte lo que el usuario haya cambiado. Esto mejorará en un refinamiento futuro.
ctx.ui.get / ctx.ui.set — acceso en tiempo de ejecución a los controles de un panel (fase 3)¶
# Read the current value; returns str or None.
current = ctx.ui.get(panel_id, "needle")
# Write. Returns bool (False when panel/control unknown, or when the
# value can't be coerced to the target type).
ctx.ui.set(panel_id, "needle", "hello")
ctx.ui.set(panel_id, "cs", True) # bool -> "true"/"false"
get devuelve el valor principal como cadena:
- Label / Button / LineEdit / ComboBox → el texto
- Checkbox →
"true"/"false" - NumberSpin → el entero en decimal
- List → el texto del elemento seleccionado en ese momento (cadena vacía si no hay ninguno)
Un set iniciado por el anfitrión es silencioso: NO vuelve a disparar el
on_change del control, así que puedes actualizar tus propios widgets desde un
manejador de comando sin bucles infinitos.
5. Ejemplo completo resuelto — el plugin wordcount¶
Cópialo literalmente y cambia el id, el name y los title para adaptarlo a tu
caso. Ejercita todos los servicios de la fase 1.
{
"id": "org.example.wordcount",
"name": "Word Count",
"version": "1.0.0",
"vendor": "Example, Inc.",
"description": "Counts words the user has typed since launch and logs saves.",
"apiVersion": 1,
"entry": "python",
"module": "main",
"order": 900,
"menus": [
{ "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
}
"""Reports a session word-count total via a menu command and logs saves."""
import mte # noqa: F401
# Module-level state is fine -- one worker per plugin, one register() call.
_state = {"saves": 0}
def register(ctx):
ctx.log.info("wordcount: register() ran")
@ctx.commands.command(id="wordcount.report",
title="Report save count",
shortcut="Ctrl+Alt+W",
category="Text")
def _report():
n = _state["saves"]
ctx.status.show(f"{n} save(s) this session", timeout_ms=2500)
ctx.log.info(f"wordcount: user asked for count; {n} saves so far")
ctx.menus.add_item("Plugins/Word Count/Report save count",
"wordcount.report")
@ctx.events.on_document_saved
def _saved(ev: mte.DocumentSaved):
_state["saves"] += 1
ctx.log.info(f"wordcount: doc {ev.document} saved -- total now {_state['saves']}")
@ctx.events.on_app_about_to_quit
def _quit(_ev):
ctx.log.info(f"wordcount: shutting down after {_state['saves']} saves")
Flujo esperado del usuario: abrir el editor, guardar un archivo tres veces y
después hacer clic en Plugins → Word Count → Report save count. La barra de
estado muestra 3 save(s) this session. El registro anota cada paso.
6. Tareas habituales — recetas para copiar y pegar¶
Cada receta es un fragmento de solo cuerpo: pégalo dentro de
def register(ctx):.
Añadir un comando de menú¶
@ctx.commands.command(id="mine.hello", title="Say Hi", shortcut="Ctrl+Alt+Y")
def _hi():
ctx.status.show("hi")
ctx.menus.add_item("Plugins/Mine/Say Hi", "mine.hello")
Reaccionar al guardado¶
Avisar al salir¶
@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
ctx.log.warning("mine: last chance to persist state")
Registrar en un nivel concreto¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Separador de menú¶
Varios comandos relacionados¶
@ctx.commands.command(id="mine.a", title="Do A")
def _a(): ...
@ctx.commands.command(id="mine.b", title="Do B")
def _b(): ...
ctx.menus.add_item("Plugins/Mine/Do A", "mine.a")
ctx.menus.add_item("Plugins/Mine/Do B", "mine.b")
Conmutador persistente de encendido/apagado con marca visible¶
KEY = "verbose"
item = ctx.menus.add_item("View/Verbose Logging", "mine.toggleVerbose")
# Seed the mark from the persisted state so it matches on launch.
ctx.menus.set_item_checked(item, ctx.settings.get(KEY, False))
@ctx.commands.command(id="mine.toggleVerbose", title="Verbose Logging")
def _toggle():
on = not ctx.settings.get(KEY, False)
ctx.settings.set(KEY, on)
ctx.menus.set_item_checked(item, on)
Panel lateral con una entrada de texto, una casilla y dos botones¶
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
def _on_needle(value): ctx.log.info(f"needle={value!r}")
needle_h = ctx.docks.on_change(_on_needle)
panel = ctx.docks.add_panel(
id="mine.panel", title="Mine", area="right",
form=Column([
Label(text="Search:"),
LineEdit(id="needle", placeholder="text...",
binds="settings:needle", on_change=needle_h),
Checkbox(id="cs", label="Case", binds="settings:cs"),
Row([Button(text="Run", on_click="mine.run"),
Button(text="Clear", on_click="mine.clear")]),
]),
)
@ctx.commands.command(id="mine.run", title="Run")
def _run():
needle = ctx.ui.get(panel, "needle") or ""
cs = ctx.ui.get(panel, "cs") == "true"
ctx.status.show(f"searching for {needle!r} (case={cs})")
@ctx.commands.command(id="mine.clear", title="Clear")
def _clear():
ctx.ui.set(panel, "needle", "")
Página de Preferencias que refleja los ajustes de un panel¶
from mte.ui import Column, Checkbox
# Both controls bind to the SAME settings key -- toggling one
# flips the other in real time.
ctx.docks.add_panel(id="mine.panel", title="Mine", area="right",
form=Column([Checkbox(id="cs", label="Case",
binds="settings:cs")]))
ctx.settings_page.register(category="Plugins/Mine", title="Mine",
form=Column([Checkbox(id="cs", label="Case sensitive",
binds="settings:cs")]))
7. Antipatrones — NO hagas esto¶
Usar print() para diagnóstico¶
Mal:
Bien:
register dentro de una clase¶
Mal:
Bien:
async def register¶
Mal:
Bien:
Importaciones de terceros sin distribuir el wheel¶
Mal:
Bien (fase 1): cíñete a la biblioteca estándar. Si de verdad necesitas algo de
terceros, distribuye el wheel dentro de la carpeta del plugin y amplía sys.path
en las primeras líneas de main.py; consulta Empaquetado.
Importaciones de Qt¶
Mal:
Bien: nada de Qt en ningún sitio. Toda la interfaz que puedes aportar pasa por
ctx.menus, ctx.status, ctx.diagnostics, ctx.compare y los paneles y
páginas de ajustes declarativos. Nunca un widget en bruto.
Bloquear en un manejador¶
Mal:
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Bien (fase 1): delega en un hilo y comunícate mediante ctx.log:
def _handler():
import threading
def _work():
time.sleep(30)
ctx.log.info("done")
threading.Thread(target=_work, daemon=True).start()
ctx.status.show("started")
Ruta de menú cuyo segmento superior no está en plugin.json¶
Mal:
Bien: mantén el segmento de primer nivel de la ruta alineado con menus[]. Usa
"Plugins/Mine/Do" en el caso anterior.
Guardar ctx para usarlo después¶
Mal:
_ctx = None
def register(ctx):
global _ctx
_ctx = ctx # <-- fine for the lifetime of this worker,
# but confusing and unnecessary
def some_other_function():
_ctx.log.info("...") # <-- called from where?
Bien: captura en el cierre que los usa los servicios que necesitas. register es
donde se hace el cableado; todo lo demás son manejadores que ya tienen acceso por
cierre.
8. Lista de comprobación de depuración para el usuario¶
Inclúyela al final de tu respuesta cuando produzcas un plugin, para que el usuario sepa qué comprobar si no funciona:
- Lanza el editor desde un intérprete de comandos en el que haya un intérprete de
Python 3.9 o superior en el
PATH(o en el queMTE_PYTHONapunte a uno). - Busca
PluginHost: Python plugin backend enabled; interpreter=…en el registro. Si no está, ve a Solución de problemas. - Busca
[python:<your.id>] register() ran(si seguiste el ejemplo dewordcount): demuestra queregisterse ejecutó. - Si falta el elemento de menú, busca en el registro explicaciones del tipo
PythonPluginBackend: skipping '<id>': …. - Si al hacer clic en el menú no pasa nada, busca en el registro líneas
[worker stderr] …: ahí acaban las trazas de Python.
9. Restricciones que hay que repetir al final de cada respuesta¶
Cuando produzcas un plugin, dile siempre al usuario:
- Instálalo de cualquiera de las dos formas:
- comprime el contenido de la carpeta como
<name>.mtepluginy usa Preferencias → Complementos → Instalar un complemento... (valida el paquete antes de tocar nada), o - deja la carpeta en el directorio de plugins por usuario del editor
(
%LOCALAPPDATA%/MTE/Plugins/en Windows;~/Library/Application Support/MTE/Plugins/en macOS;~/.local/share/MTE/Plugins/en Linux).
- comprime el contenido de la carpeta como
- Reinicia el editor.
- Define
MTE_PYTHONsi el registro dice «no Python interpreter found». - Informa de la línea de registro
[python:<id>] …para que podamos confirmar que se ejecutó.
10. Fuente de verdad¶
Si alguna afirmación de esta página entra en conflicto con lo que el paquete
cliente de Python implementa realmente, gana el paquete. Se distribuye junto al
editor en <exe>/Python/mte/: lee ese código fuente antes de decidir un
comportamiento. El lado de C++ del protocolo está en
Components/PluginHost/Src/PyServiceBridge.cpp y la especificación del canal está
en Python/PROTOCOL.md.