Saltar a contenido

ctx — el contexto del plugin

register(ctx) recibe un argumento: un PluginContext. Expone los servicios de abajo (toda la superficie de IPluginContext que encaja en el modelo fuera de proceso). Cada uno es una pequeña clase de Python que el trabajador construye sobre el canal RPC; el código fuente está en Python/mte/.

def register(ctx):
    ctx.commands       # register/invoke commands
    ctx.menus          # add menu items & separators
    ctx.events         # subscribe to editor events
    ctx.log            # write to the host's diagnostics stream
    ctx.status         # post transient status-bar messages
    ctx.editor         # buffer / caret / selection / files / search / bookmarks / groups
    ctx.languages      # list languages, read / switch a document's language, contribute one
    ctx.settings       # per-plugin key/value store
    ctx.app_settings   # editor-wide read-only settings view (keys: mte.keys)
    ctx.document_settings  # .editorconfig-layered per-document reads
    ctx.theme          # themeable colour categories for your own UI
    ctx.diagnostics    # publish squiggles + margin glyphs
    ctx.compare        # open the side-by-side compare view
    ctx.completions    # register auto-completion providers
    ctx.docks          # declarative dock panels (+ set_panel_visible, is_panel_visible)
    ctx.line_markers   # margin glyphs / line tints that follow edits; the margin-click claim
    ctx.settings_page  # declarative Preferences pages
    ctx.ui             # runtime get/set for panel controls

Dos partes del contrato nativo aún no tienen equivalente en Python: los proveedores de hover (así que las bandas de prioridad del hover no se aplican a los plugins Python) y los proveedores del menú contextual del editor (así que tampoco la zona de texto / margen del menú contextual). Ambos se responden mientras el usuario espera, lo que requiere el mismo tratamiento de plazos que tienen los proveedores de autocompletado antes de que un viaje de ida y vuelta al trabajador sea seguro.

No guardes ctx en una variable global pensando en usarlo después: aunque los atributos son estables durante la vida del plugin, trata ctx como algo propio del registro. Guarda referencias a los servicios individuales si los necesitas (log = ctx.log, por ejemplo).


commands

ctx.commands.command(*, id, title, shortcut="", category="", scope="")

Decorador que registra un comando y enlaza el invocable envuelto como su manejador.

  • id (str, obligatorio): identificador único al estilo DNS inverso (p. ej. "wordcount.count"). Las mismas reglas que en plugin.json: debe ser único a nivel global.
  • title (str, obligatorio): etiqueta legible que se muestra allí donde aparezca el comando (menús, paleta de comandos).
  • shortcut (str, opcional): cadena de atajo predeterminado portátil, p. ej. "Ctrl+Alt+W". Ctrl se asigna automáticamente al modificador principal de la plataforma (Cmd en macOS). Déjalo vacío para no poner ninguno. El usuario puede reasignarlo o desasignarlo en Preferencias ▸ Atajos de teclado; para los comandos ligados a menús, el anfitrión aplica la reasignación por ti.
  • category (str, opcional): etiqueta de agrupación libre; se muestra como la categoría del comando en la Asignación de atajos.
  • scope (str, opcional): ""/"application" (lo predeterminado: el anfitrión asigna el atajo en la acción de menú del comando) o "pluginWindow" para combinaciones que viven en interfaces propiedad del plugin, en cuyo caso el anfitrión solo lista el comando en la herramienta de asignación; lee ctx.commands.effective_shortcut(id) al construir tus widgets y vuelve a aplicarlo con el evento "command.shortcutChanged".
ctx.commands.effective_shortcut(command_id) -> str

El atajo vigente en este momento tras la reasignación del usuario y la resolución de conflictos ("" = sin asignar o desconocido). Se puede llamar sin problema desde los manejadores.

La función envuelta no toma argumentos y no devuelve nada. Las excepciones que se produzcan dentro las captura el trabajador y las anota en stderr; no hacen fallar el editor.

@ctx.commands.command(id="wordcount.count",
                      title="Count Words",
                      shortcut="Ctrl+Alt+W",
                      category="Text")
def _count():
    ctx.status.show("counted")

Bajo el capó: el lado de Python reserva un handlerId localmente, registra la retrollamada en la conexión RPC y dispara commands.registerCommand hacia el anfitrión. Cuando el usuario activa el comando (clic de menú, atajo, paleta), el anfitrión envía un RPC callback y tu función se ejecuta.

Los identificadores de comando son de todo el proceso, no por plugin. Ponles el prefijo de tu espacio de nombres DNS inverso (wordcount.…) para evitar colisiones.


Las rutas de menú usan / como separador. El segmento más a la izquierda es un menú de primer nivel (Plugins, Tools, …); los segmentos intermedios crean submenús bajo demanda; el segmento final es la etiqueta del elemento.

ctx.menus.add_item(path, command_id)

ctx.menus.add_item("Plugins/Word Count/Count", "wordcount.count")
  • path (str): "Superior/Sub/…/Etiqueta". Los submenús intermedios se crean si no existen.
  • command_id (str): identificador de un comando ya registrado. Hacer clic en el elemento llama al comando.

Devuelve una cadena handlerId opaca. Consérvala si más adelante necesitas hacer unsubscribe del elemento (algo raro en la fase 1).

El segmento más a la izquierda ("Plugins" arriba) debe coincidir con el title de una de tus entradas de menus[] en plugin.json, para que se recoja la colocación en la barra de menús.

ctx.menus.add_separator(path)

ctx.menus.add_separator("Plugins/Word Count")

Inserta un separador en un submenú existente.

ctx.menus.set_item_checked(handler_id, checked)

Convierte el elemento añadido previamente en una entrada de menú marcable y fija su estado de marcado; es el idioma estándar para un conmutador persistente de encendido/apagado que muestra una marca de verificación visible.

item = ctx.menus.add_item("View/Auto-open Preview",
                          "myplugin.toggleAutoOpen")

# Seed the check mark from the persisted setting so it matches
# reality before the user opens the menu.
ctx.menus.set_item_checked(item, ctx.settings.get("autoOpen", True))

@ctx.commands.command(id="myplugin.toggleAutoOpen",
                      title="Toggle Auto-Open")
def _toggle():
    on = not ctx.settings.get("autoOpen", True)
    ctx.settings.set("autoOpen", on)
    ctx.menus.set_item_checked(item, on)

La primera llamada promueve el elemento a marcable; las llamadas posteriores solo cambian la marca. Pasar un handler_id que el anfitrión no conoce no hace nada en silencio (es seguro llamarlo después de haber soltado el testigo, por ejemplo desde una retrollamada retrasada que se dispare durante el cierre).

ctx.menus.set_item_enabled(handler_id, enabled)

Activa o atenúa un elemento añadido previamente. Úsalo cuando el comando necesite una condición previa —algo que cerrar, algo que restaurar—, para que el menú muestre que no está disponible en lugar de ofrecer una entrada que no hace nada en silencio.

item = ctx.menus.add_item("File/Close Thing", "myplugin.closeThing")

# Nothing to close yet:
ctx.menus.set_item_enabled(item, False)

# ... and from every place that changes the state the command needs:
ctx.menus.set_item_enabled(item, thing_is_open)

Un elemento desactivado sigue visible pero no se puede activar, ni con un clic ni con su atajo. La llamada es idempotente, así que nunca tienes que recordar el estado anterior, y un handler_id desconocido no hace nada en silencio, como arriba. Los elementos empiezan activados: una llamada justo después de add_item prepara un elemento que no está disponible al arrancar.


events

Suscríbete a los eventos del editor. Dos API equivalentes: se recomiendan los decoradores azucarados; el subscribe() en bruto está disponible si quieres calcular el tema en tiempo de ejecución.

Decoradores azucarados

@ctx.events.on_document_saved
def _saved(ev):
    ctx.log.info(f"saved doc {ev.document}")

@ctx.events.on_document_opened
def _opened(ev):
    ctx.log.info(f"opened {ev.path}")

La función envuelta toma un argumento: una dataclass tipada correspondiente al tema. Consulta la referencia de eventos para ver la tabla completa de tema ↔ dataclass.

API en bruto

handler_id = ctx.events.subscribe("document.saved", _saved)
# later:
ctx.events.unsubscribe(handler_id)

Propiedades azucaradas disponibles (todas son decoradores):

Propiedad Se dispara con
on_document_opened Documento abierto en una pestaña
on_document_closed Documento cerrado
on_document_saved Documento escrito en el disco
on_selection_changed El cursor o la selección se han movido en el documento activo
on_active_document_changed Ha cambiado la pestaña en primer plano
on_document_modified_changed El indicador de limpio/sucio ha cambiado (modified: bool)
on_text_changed Cada edición (position, added_bytes, removed_bytes); mantenlo trivial
on_settings_changed Se han aplicado las Preferencias (scope: "" en todo el editor, "plugins/<id>")
on_theme_changed Se ha cambiado o editado el tema (variant); vuelve a leer los colores de ctx.theme
on_workspace_root_changed Se ha abierto o cerrado la carpeta del espacio de trabajo (root)
on_workspace_files_changed Ha cambiado el conjunto de archivos del espacio de trabajo (files)
on_host_window_shown Una vez por ejecución, después del primer fotograma; programa aquí el trabajo lento de arranque
on_app_about_to_quit El editor se está cerrando: última oportunidad de ejecutarse
on_command_shortcut_changed Ha cambiado el atajo efectivo de un comando (reasignación del usuario, importación de mapa de teclado, resolución de conflictos); carga útil: command_id, sequence ("" = sin asignar)

Los nombres de tema desconocidos lanzan ValueError en el momento de la suscripción. Los suscriptores pueden ejecutarse después del manejador nativo correspondiente; trata la entrega de eventos como informativa, no como autoritativa.

Publicar

ctx.events.publish(topic, payload) difunde por igual a los suscriptores nativos y a los de Python, y solo se acepta para los dos temas del espacio de trabajo (workspace.rootChanged, workspace.filesChanged); cualquier otro lanza ValueError. Consulta la referencia de eventos.


log

ctx.log.trace  ("very-fine-grained")
ctx.log.debug  ("dev-only detail")
ctx.log.info   ("routine event")
ctx.log.warning("recoverable weirdness")
ctx.log.error  ("something is wrong")

Cada nivel se corresponde con el ILogger del anfitrión. Las líneas llevan el prefijo [python:<plugin.id>] para que tu salida sea localizable en el registro de diagnóstico del editor:

[python:org.example.wordcount] counted 42 words

Nunca uses print() desde un plugin: la salida estándar del trabajador es el canal RPC, y escribir en ella corrompe el protocolo. ctx.log.* es la única forma autorizada de emitir texto de diagnóstico.


status

ctx.status.show(message, timeout_ms=0, level="info")

Publica una notificación pasajera en la barra de estado.

  • message (str): qué mostrar. Solo cadenas cortas: la barra de estado tiene un ancho fijo.
  • timeout_ms (int, 0 de forma predeterminada): cuánto tiempo mantenerla en pantalla; 0 deja que el anfitrión elija un valor sensato (unos segundos).
  • level (str, "info" de forma predeterminada): "info" / "warning" / "error". Los niveles más altos pueden mantener el mínimo en pantalla un poco más, para que un error no lo borre al momento un mensaje informativo rutinario.
ctx.status.show("saved", timeout_ms=1500)
ctx.status.show("no results", level="warning")

ctx.status.show se lanza y se olvida. Si el anfitrión no tiene una barra de estado visible (compilación mínima o sin interfaz), la llamada se descarta en silencio. ctx.status.clear() borra la notificación pasajera que haya en pantalla.

Segmentos persistentes

Para estado en vivo (un recuento de palabras, el estado de un servidor) usa un segmento propio que refresques: nunca pisa una notificación, ni al revés.

seg = ctx.status.add_segment("wordcount.total", priority=100)
seg.set_text("2,431 words")
seg.set_visible(False)      # hide without giving it up
seg.remove()                # take it off the bar (idempotent)

priority ordena los segmentos de los plugins de izquierda a derecha (menor = más a la izquierda; los empates conservan el orden de registro). El cierre del plugin elimina todo lo que siga retenido.


editor

Acceso síncrono completo a los documentos activos. Todos los métodos bloquean el hilo que llama hasta que el anfitrión responde (normalmente microsegundos cuando el trabajador y el editor comparten máquina). Es legal desde cualquier manejador de comando o de evento; nunca desde una retrollamada async def (consulta la nota sobre hilos al principio de esta página).

Enumeración de documentos

doc  = ctx.editor.active_document()      # 0 when no document is open
docs = ctx.editor.open_documents()       # list[int]
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc)         # str or None for untitled

Acceso al búfer

text  = ctx.editor.text(doc)                        # whole buffer
slice = ctx.editor.text_range(doc, start, end)      # [start, end) bytes
n     = ctx.editor.length(doc)                      # int64
ctx.editor.set_text(doc, "...")

text_range evita traer todo el búfer por el RPC cuando solo necesitas un trozo; prefiérelo para ordenar o transformar archivos grandes.

Selección

sel = ctx.editor.selection_range(doc)               # mte.MatchRange(start, end)
s   = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "new text")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))

ranges = ctx.editor.selection_ranges(doc)           # list[MatchRange]: one per caret
block  = ctx.editor.is_rectangular_selection(doc)   # Alt+drag column block?

Cursor

pos = ctx.editor.caret(doc)                         # mte.EditorPosition(line, column)
ctx.editor.set_caret(doc, mte.EditorPosition(line=7, column=3))

Operaciones con archivos

new_doc = ctx.editor.open_file("/path/to/file")     # doc-id or 0 on failure
here    = ctx.editor.open_file_at("/path/to/file", mte.EditorPosition(41, 0))
ctx.editor.reveal_line(doc, 120)                    # scroll there; caret and focus stay
ok      = ctx.editor.save_document(doc)
closed  = ctx.editor.close_document(doc)            # prompts if modified; False = user cancelled
dirty   = ctx.editor.is_modified(doc)
ctx.editor.set_modified(doc, True)                  # force the tab asterisk
ro      = ctx.editor.is_read_only(doc)              # e.g. Monitoring (tail -f) mode
enc     = ctx.editor.document_encoding(doc)         # mte.DocumentEncoding, e.g. UTF8_BOM
ctx.editor.set_document_encoding(doc, mte.DocumentEncoding.UTF8)  # takes effect on the next save
eol     = ctx.editor.line_ending(doc)               # mte.LineEnding, e.g. CRLF
ctx.editor.set_line_ending(doc, mte.LineEnding.LF)  # Enter inserts it; the next save writes it
scratch = ctx.editor.create_untitled("text", "Untitled-3")   # new buffer, doc-id
sid     = ctx.editor.stable_id(doc)                 # survives Save As; compare with ==
clip    = ctx.editor.clipboard_text()               # system clipboard, "" if no text

# Declare that you snapshot unsaved buffers (the host then stops asking
# "save changes?" when the window closes):
ctx.editor.set_unsaved_changes_preserved(True)
ctx.editor.unsaved_changes_preserved()              # bool, global flag

open_file_at es la única llamada para «llevar al usuario allí» (un error de compilación, un marco de pila, un resultado de búsqueda): abre el archivo o activa su pestaña, 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— y da el foco al editor. Línea y columna empiezan en 0 (la columna en bytes UTF-8) y se ajustan, nunca se rechazan; devuelve el identificador del documento, o 0 si no se pudo abrir el archivo, y en ese caso no se ha movido nada. reveal_line solo muestra una línea: no mueve el cursor, no cambia de pestaña y no toma el foco.

Marcadores, marcas y resaltado de URL

ctx.editor.toggle_bookmark(doc, line)               # 0-based line
lines = ctx.editor.bookmarked_lines(doc)            # list[int]
nxt   = ctx.editor.next_bookmark(doc, from_line, wrap=True)      # -1 = none
prv   = ctx.editor.previous_bookmark(doc, from_line, wrap=True)
ctx.editor.clear_bookmarks(doc)

ctx.editor.mark_range(doc, mte.MatchRange(4, 9))    # "mark" indicator
ctx.editor.clear_marks(doc)

ctx.editor.set_url_highlight_enabled(doc, True)
ctx.editor.is_url_highlight_enabled(doc)
url = ctx.editor.url_at_caret(doc)                  # "" when none
ctx.editor.open_url_at_caret(doc)                   # bool

Búsqueda

opts = mte.SearchOptions(match_case=True, mode="regex")
hit  = ctx.editor.find_in_range(doc, r"TODO\(.*\)", 0,
                                ctx.editor.length(doc), opts)
if hit.valid:
    new_end = ctx.editor.replace_target(doc, hit, "DONE()", opts)

SearchOptions.mode: "normal" / "extended" / "regex". SearchOptions.direction: "forward" / "backward". Consulta mte.SearchOptions para la lista completa de campos.

Búsqueda incremental (hacer crecer el patrón mientras el usuario escribe, desde un ancla fijada):

ctx.editor.set_incremental_search_anchor(doc)
hit = ctx.editor.incremental_search(doc, "TOD", opts)   # MatchRange

Sangría y sintaxis

style = ctx.editor.indentation(doc)                 # IndentationStyle(use_tabs, width)
ctx.editor.set_indentation(doc,
    mte.IndentationStyle(use_tabs=False, width=2))

# Where word-wrapped continuation rows start:
# "left" (window edge) / "same" (align with the line; default) / "indent"
ctx.editor.set_wrap_indent(doc, "same")
mode = ctx.editor.wrap_indent(doc)                  # same strings back

lang  = ctx.editor.colorizer_id(doc)                # e.g. "python", "" if none

# Semantic classification from the active colorizer (cheap, no re-lex):
# list[mte.CategorySpan(start, length, category)] with category names
# "comment" / "string" / "keyword" / ... -- a spell checker keeps to
# comments and strings this way.
spans = ctx.editor.text_categories(doc, 0, ctx.editor.length(doc))

ctx.editor.trigger_completion(doc)                  # pop the list as if Ctrl+Space

Vista dividida y grupos de pestañas

n  = ctx.editor.group_count()                       # >= 1
g  = ctx.editor.group_of(doc)                       # -1 if not open
w  = ctx.editor.group_weights()                     # relative widths
ctx.editor.move_document_to_group(doc, 1)           # creates trailing groups on demand
ctx.editor.set_group_weights([2, 1])
ctx.editor.set_document_tab_index(doc, 0)           # reorder within its pane

gs = ctx.editor.groups_of(doc)                      # EVERY pane showing it
ctx.editor.show_document_in_group(doc, 1)           # split it into pane 1

Un documento mostrado en dos paneles es un documento con un solo identificador, así que group_of() solo puede nombrar uno de sus asientos. groups_of() los informa todos, y show_document_in_group() crea una segunda vista compartida: lo mismo que hace Ver > Dividir documento a la derecha, así que no se emite ningún evento document_opened.

ctx.editor refleja ahora el IEditorService nativo uno a uno.


languages

El coloreado de sintaxis es una función del núcleo: el anfitrión detecta el lenguaje de un documento y lo colorea sin ningún plugin. ctx.languages te permite participar: listar lo que el anfitrión conoce, leer o anular el lenguaje de un documento, volver a ejecutar la detección y aportar definiciones de lenguaje completas.

for lang in ctx.languages.available():              # list[mte.LanguageInfo(id, name)]
    ...
lang_id = ctx.languages.language_for(doc)           # "" when uncoloured
ok      = ctx.languages.set_language_for(doc, "json")   # "" clears; False = unknown
ctx.languages.auto_detect_for(doc)                  # undo a manual override

handler = ctx.languages.register(
    id="mylang", name="My Language", lexer="cpp",
    extensions=["mylang", "ml2"],
    keywords=[{"group": "Primary keywords and identifiers",
               "words": "if else while"}],
    styles={"SCE_C_COMMENT": "comment", "SCE_C_STRING": "string"})
...
ctx.languages.unregister(handler)                   # restores what it displaced

register toma la misma forma que un archivo colorizers/*.json distribuido (o un diccionario definition= completo): lexer nombra un analizador léxico del backend que ya existe (el anfitrión no gana analizadores nuevos en tiempo de ejecución), styles asocia los nombres de estilo de ese analizador con categorías semánticas, y keywords se dirige a sus listas de palabras mediante la propia descripción de ranura del analizador; el anfitrión resuelve ambas cosas exactamente igual que para un archivo distribuido, así que nunca deletreas identificadores del motor. Registrar un identificador que ya existe lo sustituye mientras conserves el identificador de manejador; el cierre del plugin retira todo lo que siga retenido.


document_settings

Ajustes de solo lectura, con ámbito de documento, que superponen el .editorconfig de un proyecto a tus propios ctx.settings. Para la clave k y el documento doc, una lectura resuelve mte.<plugin.id>.<k> a partir del .editorconfig que se aplica a ese documento, recurre después a ctx.settings y luego a tu valor predeterminado:

wrap  = ctx.document_settings.get_bool(doc, "wrap", False)
width = ctx.document_settings.get_int(doc, "width", 80)
ratio = ctx.document_settings.get_double(doc, "ratio", 1.0)
name  = ctx.document_settings.get_string(doc, "profile", "")
ctx.document_settings.contains(doc, "wrap")          # set in either layer?

Las escrituras van a través de ctx.settings, nunca de vuelta al .editorconfig.


diagnostics

Empuja líneas onduladas y glifos de margen para un documento, desde un analizador estático, un servidor de lenguaje o un registro de compilación. El anfitrión los dibuja; tú nunca tocas la vista. Publicar sustituye todo lo que hubieras publicado antes para esa pareja (documento, fuente), así que envía el conjunto completo cada vez:

ctx.diagnostics.publish(doc, "mylinter", [
    mte.Diagnostic(start=12, end=18, message="unused variable",
                   severity=mte.SEVERITY_WARNING, code="W0612"),
])
ctx.diagnostics.publish(doc, "mylinter", [])        # retract only your own
ctx.diagnostics.clear(doc)                          # clears EVERY producer's set on doc

source es obligatorio: es lo que mantiene tu conjunto separado de los de otros productores (el plugin LSP, el corrector ortográfico) en el mismo documento. Gravedades: SEVERITY_ERROR / SEVERITY_WARNING / SEVERITY_INFORMATION / SEVERITY_HINT. Los desplazamientos son intervalos de bytes semiabiertos [start, end).


theme

Registra los colores que pinta tu propia interfaz como categorías con nombre y adaptables al tema; el Editor de temas las lista automáticamente y las anulaciones del usuario van y vuelven en el archivo de tema:

h = ctx.theme.register_category("com.example.my/match", "Match highlight",
                                default_light=0xFFF3B0, default_dark=0x8A6D00)
rgb     = ctx.theme.resolve_color("com.example.my/match")   # 0xRRGGBB int
themed  = ctx.theme.has_override("com.example.my/match")
variant = ctx.theme.active_variant()                        # "light" / "dark" / ""
cats    = ctx.theme.categories()                            # list[mte.ThemeCategory]
ctx.theme.unregister_category(h)

Vuelve a leer resolve_color con ctx.events.on_theme_changed.


compare

Abre (o enfoca) la vista de comparación en paralelo del editor sobre dos fuentes. Cada lado es un archivo del disco (path=), texto literal (content=) o ambos (se muestra el contenido, y la ruta le da a la persistencia de sesión una identidad restaurable):

shown = ctx.compare.open(
    mte.CompareSource("before", path="/tmp/a.txt"),
    mte.CompareSource("after", content=new_text),
    ignore_whitespace=True)                          # ignore_case / ignore_eol too

Devuelve False cuando un lado no se puede leer.


settings

Almacén persistente y tipado de clave/valor, acotado a plugins/<your.id>/ en el anfitrión. Todos los métodos son síncronos; get y set se infieren 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)             # returns bool
count = ctx.settings.get("count", 0)                # returns int
theme = ctx.settings.get("theme", "light")          # returns str

Tipos:

Tipo de Python de default Tipo en el canal Tipo devuelto por get()
bool bool bool
int (que no sea bool) int int
float double float
str (o None) string str

Además:

ctx.settings.contains("wrap")           # bool
ctx.settings.remove("stale_key")
ctx.settings.sync()                     # optional; host also syncs on shutdown

Reacciona a los cambios hechos en el diálogo de Preferencias suscribiéndote a settings.changed (ctx.events.on_settings_changed; consulta la referencia de eventos).

app_settings

Vista de solo lectura de la configuración de toda la aplicación del editor. Los nombres de clave conocidos se exportan como constantes en mte.keys (reflejando la cabecera de C++ PluginApi/AppSettingsKeys.h, que sigue siendo la lista que manda):

import mte
size   = ctx.app_settings.get(mte.keys.EDITOR_FONT_SIZE, mte.keys.DEFAULT_EDITOR_FONT_SIZE)
family = ctx.app_settings.get(mte.keys.EDITOR_FONT_FAMILY, "")   # "" = host default
theme  = ctx.app_settings.get(mte.keys.UI_THEME, mte.keys.DEFAULT_UI_THEME)  # "system"/"light"/"dark"

Los asignadores, remove() y sync() en ctx.app_settings lanzan RuntimeError: la vista es de solo lectura a propósito.


completions

Registra un proveedor de autocompletado que participe en la ventana emergente del editor:

@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
    if not req.prefix:
        return []
    return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
            for w in ("hello", "hello_world", "helper")
            if w.startswith(req.prefix)]

El invocable envuelto se llama en cada pulsación (o con Ctrl+Space) cuyo documento coincida con el filtro de lenguaje. Campos de req:

Campo Tipo Significado
document int Documento que se está editando
prefix str Fragmento de palabra inmediatamente anterior al cursor
position int Desplazamiento en bytes del cursor
language_id str Identificador de colorizador ("python", "cpp", …)
manual bool True cuando el usuario pulsó Ctrl+Space; False en la activación automática

Devuelve una lista de mte.CompletionItem (o cadenas simples, o diccionarios con {text, detail, kind}). Tipos: mte.KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.

Plazo

El anfitrión envía a tu proveedor una petición por pulsación con un plazo de ~30 ms. Si tu retrollamada no llega, la ventana emergente se muestra sin tus candidatos para esa pulsación. Construye cualquier índice costoso en segundo plano y responde desde caché; nunca hagas entrada/salida en la retrollamada. El autocompletado por red al estilo LSP corresponde a una futura superficie asíncrona, no a esta.

languages=[] (u omitirlo) significa «todos los documentos». Se permiten varios proveedores por plugin; cada decorador registra el suyo.


data_dir

path = ctx.data_dir()                    # pathlib.Path

Devuelve el directorio por plugin con permiso de escritura que el anfitrión ya ha creado para ti. Úsalo para cachés, índices y cualquier dato que sea tuyo. ctx.diagnostics_dir() devuelve de forma análoga el directorio compartido de fallos y registros (de solo lectura para ti: ahí escribe el informador de fallos).


docks

Paneles acoplables declarativos: una interfaz de panel lateral construida a partir de una especificación de formulario, no de widgets de Qt. Consulta mte.ui para el vocabulario del constructor.

from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button

def register(ctx):
    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="renamer.panel",
        title="Renamer",
        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="renamer.run")]),
        ]),
    )

ctx.docks.add_panel(*, id=None, title="", area="right", form, initially_visible=True, proportional_size=0.0, tab_group="") -> str

Registra un panel acoplable. Devuelve el identificador del panel (asignado automáticamente cuando id es None).

  • id (str, opcional) — identificador estable que usan ctx.ui.get / ctx.ui.set para dirigirse a los controles de dentro de este panel. Si no proporcionas uno, el anfitrión asigna un identificador py-dock/N y lo devuelve.
  • title (str) — se muestra en el encabezado del panel.
  • area (str) — "left" / "right" / "top" / "bottom". Los valores desconocidos recurren a "right".
  • form (dict) — una especificación construida con los constructores de mte.ui.
  • initially_visible (bool) — si el panel se muestra cuando se carga el plugin. True de forma predeterminada. Pasa False para un panel que no debería aparecer al arrancar y muéstralo después con set_panel_visible.
  • proportional_size (float) — el tamaño predeterminado del panel como porción de la ventana principal: de su ancho en las áreas "left" / "right", de su alto en "top" / "bottom". 0.0 (lo predeterminado) conserva el comportamiento en píxeles que quiere un panel de herramienta: el panel mantiene su tamaño y redimensionar la ventana no lo toca. Una fracción como 0.4 es lo que quiere una vista previa: maximizar la ventana ensancha el panel con ella. Solo el valor predeterminado es tuyo; en cuanto el usuario arrastra el separador del panel, el anfitrión adopta y recuerda su porción. Los valores fuera de rango se acotan, y el anfitrión conserva una extensión mínima tanto para el panel como para el editor. Las reglas completas están en proportionalSize.
  • tab_group (str): los paneles con el mismo identificador de grupo no vacío en la misma area comparten un único panel acoplable como pestañas en lugar de colocarse uno al lado del otro. Ponle un espacio de nombres como a un identificador de panel ("renamer/panels"). Es solo la disposición predeterminada: una disposición guardada por el usuario tiene prioridad. Consulta tabGroup.

ctx.docks.is_panel_visible(handler_id) -> bool

Si un panel añadido con add_panel está abierto: un panel en una pestaña detrás de otro cuenta como abierto; uno registrado con initially_visible=False y nunca mostrado, uno cerrado y un identificador desconocido, no. Combínalo con set_panel_visible para mostrar tus paneles durante una tarea y después devolver lo que tenía el usuario:

was_open = ctx.docks.is_panel_visible(panel)
ctx.docks.set_panel_visible(panel, True)
# ... when the task ends:
ctx.docks.set_panel_visible(panel, was_open)

ctx.docks.set_panel_visible(handler_id, visible)

Muestra u oculta un panel añadido con add_panel. Mostrarlo lo levanta además por encima de los paneles acoplables con los que comparte área, de modo que un comando que «abre» su panel lo ponga realmente delante del usuario. Es idempotente —repite el mismo valor cuantas veces quieras, no hay estado que rastrear— y se ignora en silencio para un identificador desconocido.

panel = ctx.docks.add_panel(id="mine.panel", title="Mine", form=form,
                            initially_visible=False)

@ctx.commands.command("mine.show", title="Show My Panel")
def _show():
    ctx.docks.set_panel_visible(panel, True)

ctx.docks.on_change(callback) -> str

Asigna un identificador de manejador para conectar el campo on_change de un control a un invocable de Python. La retrollamada recibe el valor nuevo como cadena; los controles booleanos entregan "true" / "false" y los numéricos entregan el entero como cadena.

def _on_volume(value: str) -> None:
    ctx.log.info(f"volume={int(value)}")

handler = ctx.docks.on_change(_on_volume)
form = Column([NumberSpin(id="volume", default=5, on_change=handler)])

Los identificadores de manejador de on_change llevan el prefijo py-dock-cb/N y nunca chocan con los identificadores de manejador de comandos, eventos o autocompletado.

La retrollamada se invoca en el bucle de eventos del trabajador, NO en el grupo de hilos: mantenla rápida. Usa ctx.log, cambia un pequeño estado en memoria o dispara un comando posterior; no hagas trabajo pesado aquí.


line_markers

Glifos en el margen y tintes de línea completa unidos a líneas del documento que siguen las ediciones del usuario —puntos de interrupción, una flecha de instrucción actual, franjas de cambios—, además de la reclamación del clic en la columna de marcadores. El servicio nativo y todas sus reglas se describen en Marcas de línea y clic en el margen.

bp_colour = ctx.theme.register_category("myplugin.breakpoint", "Breakpoint",
                                        default_light=0xCC0000,
                                        default_dark=0xFF5555)
bp = ctx.line_markers.register_kind(glyph=mte.GLYPH_CIRCLE,
                                    glyph_color="myplugin.breakpoint",
                                    line_tint="", z_order=50)

marker = ctx.line_markers.add_marker(bp, doc, line)   # 0 = refused
ctx.line_markers.line_of(marker)                      # where it is NOW; -1 = gone
ctx.line_markers.markers(bp, doc)                     # [mte.LineMarkerPlacement(id, line)]
ctx.line_markers.remove_marker(marker)
ctx.line_markers.clear_markers(bp, doc)               # doc 0 = every document
ctx.line_markers.release(bp)                          # removes the kind and its markers
  • register_kind(glyph=, glyph_color=, line_tint=, z_order=): glyph es uno de mte.GLYPH_NONE (solo tinte), GLYPH_CIRCLE, GLYPH_CIRCLE_HOLLOW, GLYPH_CIRCLE_BAR, GLYPH_ARROW, GLYPH_STRIP; los colores son identificadores de categorías que registraste con ctx.theme. El editor solo tiene unas pocas ranuras de marcas para todos los plugins juntos, así que esto puede fallar: entonces lanza RpcError. Registra tus tipos una sola vez, en register().
  • add_marker(kind, doc, line): línea que empieza en 0, sin ajustar: una línea fuera del documento devuelve 0 y no coloca nada.
  • Las marcas se mueven con sus líneas, sobreviven a una recarga en el mismo número de línea y desaparecen con su documento: vuelve a añadir las tuyas en ctx.events.on_document_opened.

El clic en el margen

@ctx.events.on_margin_clicked
def _clicked(ev):                       # mte.MarginClicked
    if ev.modifiers & mte.MODIFIER_CONTROL:
        edit_condition(ev.document, ev.line)
    else:
        toggle_breakpoint(ev.document, ev.line)

claim = ctx.line_markers.claim_margin_click()

Cada clic izquierdo en la columna de marcadores se publica como margin.clicked (document, line, margin = "symbol", modifiers: bits mte.MODIFIER_SHIFT / MODIFIER_CONTROL / MODIFIER_ALT / MODIFIER_META). De forma predeterminada el editor también alterna un marcador con él; mientras algún plugin mantenga claim_margin_click(), no lo hace y deja el clic a los plugins. ctx.line_markers.release(claim) devuelve el clic.


settings_page

Añade una página bajo el diálogo de Preferencias del editor. El mismo vocabulario de especificación de formulario que los paneles acoplables; la misma persistencia con binds="settings:<key>".

from mte.ui import Column, Checkbox, NumberSpin

def register(ctx):
    ctx.settings_page.register(
        category="Plugins/Renamer",       # slash-separated tree path
        title="Renamer",
        form=Column([
            Checkbox(id="cs", label="Case sensitive by default",
                     binds="settings:caseSensitive"),
            NumberSpin(id="results", default=100, min=1, max=10_000,
                       binds="settings:resultCap"),
        ]),
    )

ctx.settings_page.register(*, category, title, form, scope="") -> str

  • category (str, obligatorio) — ruta separada por barras en el árbol de Preferencias, p. ej. "Plugins/My Plugin". Las categorías anidadas llegan como nodos del árbol.
  • title (str, obligatorio) — encabezado que se muestra sobre la página.
  • form (dict, obligatorio) — una especificación construida con los constructores de mte.ui.
  • scope (str, opcional) — ámbito de evento que se emite en SettingsChanged después de que el usuario pulse Aceptar o Aplicar.

Devuelve el identificador de manejador.

Cancelar NO revierte los cambios

Los controles con binds="settings:<key>" se guardan con avidez a medida que el usuario edita, así que pulsar Cancelar en el diálogo de Preferencias no los deshace. Es una limitación conocida de la fase 3; un refinamiento futuro envolverá los ajustes subyacentes en un proxy con búfer.


ui

Lee y escribe valores de los controles dentro de paneles que ya has registrado. Los valores van siempre como cadenas por el canal.

def register(ctx):
    panel_id = ctx.docks.add_panel(id="renamer.panel", ...)

    @ctx.commands.command(id="renamer.echo", title="Echo needle")
    def _echo():
        needle = ctx.ui.get(panel_id, "needle") or ""
        cs = (ctx.ui.get(panel_id, "cs") or "false") == "true"
        ctx.status.show(f"needle={needle!r} case={cs}")

    @ctx.commands.command(id="renamer.clear", title="Clear needle")
    def _clear():
        ctx.ui.set(panel_id, "needle", "")

ctx.ui.get(panel_id, control_id) -> Optional[str]

Lee el valor actual de un control. Devuelve None cuando el panel o el control son desconocidos (no es un error: trata tanto None como "" como «vacío»).

  • Label / Button / LineEdit / ComboBox — devuelve el texto.
  • Checkbox — devuelve "true" o "false".
  • NumberSpin — devuelve el entero como cadena decimal.
  • List — devuelve el texto del elemento seleccionado en ese momento (vacío cuando no hay nada seleccionado).

ctx.ui.set(panel_id, control_id, value) -> bool

Escribe un valor en un control mediante programación. Devuelve True cuando la escritura tuvo éxito y False cuando el panel o el control son desconocidos o el valor no se puede convertir (p. ej. un valor no entero enviado a un NumberSpin).

Los True / False de Python se convierten a "true" / "false" antes de enviarse; todo lo demás pasa por str().

Un set iniciado por el anfitrión NO vuelve a disparar la retrollamada on_change del control, así que puedes actualizar tus propios widgets desde un manejador de comando sin bucles infinitos.


Resumen de enlaces en tiempo de ejecución (fase 3)

Campo de la especificación de formulario Comportamiento en tiempo de ejecución
id="…" Dirigirse al control desde ctx.ui.get / ctx.ui.set.
binds="settings:<key>" Lee el valor inicial de ctx.settings; lo vuelve a escribir en cada edición del usuario.
on_click="<cmd>" Los botones invocan al hacer clic el identificador de comando registrado.
on_change=handler Dispara al editar el usuario la retrollamada registrada mediante ctx.docks.on_change(...).

Las mismas reglas se aplican en los paneles acoplables y en las páginas de Preferencias.