Aller au contenu

ctx — le contexte du plugin

register(ctx) reçoit un seul argument : un PluginContext. Il expose cinq services en phase 1. Chacun est une petite classe Python que le worker construit au-dessus du canal RPC ; le code source se trouve dans 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 access (Phase 2)
    ctx.settings       # per-plugin key/value store (Phase 2)
    ctx.app_settings   # editor-wide read-only settings view (Phase 2)
    ctx.completions    # register auto-completion providers (Phase 2)
    ctx.docks          # declarative dock panels (Phase 3)
    ctx.settings_page  # declarative Preferences pages (Phase 3)
    ctx.ui             # runtime get/set for panel controls (Phase 3)

Ne stockez pas ctx dans une variable globale en espérant l'utiliser plus tard — même si les attributs sont stables pendant la durée de vie du plugin, traitez ctx comme propre à l'enregistrement. Conservez des références vers les services individuels si vous en avez besoin (log = ctx.log, par exemple).


commands

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

Décorateur qui enregistre une commande et lie l'appelable décoré comme son gestionnaire.

  • id (str, obligatoire) : identifiant unique de style DNS inversé (p. ex. "wordcount.count"). Mêmes règles que dans plugin.json — doit être globalement unique.
  • title (str, obligatoire) : libellé lisible affiché partout où la commande apparaît (menus, palette de commandes).
  • shortcut (str, optionnel) : chaîne de raccourci par défaut portable, p. ex. "Ctrl+Alt+W". Ctrl correspond automatiquement au modificateur principal de la plateforme (Cmd sur macOS). Laissez vide pour aucun. L'utilisateur peut le réaffecter ou le délier dans Préférences ▸ Raccourcis ; pour les commandes liées à un menu, l'hôte applique le remappage pour vous.
  • category (str, optionnel) : étiquette de regroupement libre — affichée comme catégorie de la commande dans le mappeur de raccourcis.
  • scope (str, optionnel) : ""/"application" (par défaut — l'hôte lie le raccourci sur l'action de menu de la commande) ou "pluginWindow" pour les combinaisons vivant dans une UI possédée par le plugin : l'hôte se contente de lister la commande dans le mappeur ; lisez ctx.commands.effective_shortcut(id) lors de la construction de vos widgets et réappliquez sur l'événement "command.shortcutChanged".
ctx.commands.effective_shortcut(command_id) -> str

Le raccourci actuellement en vigueur après remappage par l'utilisateur et résolution des conflits ("" = non lié ou inconnu). Peut être appelé sans risque depuis les gestionnaires.

La fonction décorée ne prend aucun argument et ne renvoie rien. Les exceptions qu'elle lève sont interceptées par le worker et journalisées sur stderr ; elles ne font pas planter l'éditeur.

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

Sous le capot : le côté Python alloue localement un handlerId, enregistre le callback sur la connexion RPC et déclenche commands.registerCommand vers l'hôte. Quand l'utilisateur active la commande (clic de menu, raccourci, palette), l'hôte envoie un RPC callback et votre fonction s'exécute.

Les ids de commandes sont à l'échelle du processus, pas par plugin. Préfixez vos ids avec votre espace de noms DNS inversé (wordcount.…) pour éviter les collisions.


Les chemins de menu utilisent / comme séparateur. Le segment le plus à gauche est un menu de premier niveau (Plugins, Tools, …) ; les segments intermédiaires créent des sous-menus à la demande ; le dernier segment est le libellé de l'élément.

ctx.menus.add_item(path, command_id)

ctx.menus.add_item("Plugins/Word Count/Count", "wordcount.count")
  • path (str) : "Top/Sub/…/Label". Les sous-menus intermédiaires sont créés s'ils n'existent pas.
  • command_id (str) : id d'une commande enregistrée au préalable. Un clic sur l'élément appelle la commande.

Renvoie une chaîne handlerId opaque. Conservez-la si vous devrez faire un unsubscribe de l'élément plus tard (rare en phase 1).

Le segment le plus à gauche ("Plugins" ci-dessus) doit correspondre au title de l'une de vos entrées menus[] dans plugin.json pour que le placement dans la barre de menus soit pris en compte.

ctx.menus.add_separator(path)

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

Insère un séparateur dans un sous-menu existant.

ctx.menus.set_item_checked(handler_id, checked)

Transforme l'élément précédemment ajouté en entrée de menu cochable et définit son état coché — l'idiome standard pour une bascule actif/inactif persistante affichant une coche 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)

Le premier appel promeut l'élément en élément cochable ; les appels suivants ne font que basculer la coche. Passer un handler_id inconnu de l'hôte est un no-op silencieux (appel sans risque après l'abandon de votre jeton, p. ex. depuis un callback retardé se déclenchant pendant l'arrêt).


events

Abonnez-vous aux événements de l'éditeur. Deux API équivalentes — les décorateurs sucrés sont recommandés pour l'ensemble fixe des sujets de la phase 1 ; le subscribe() brut est disponible si vous voulez calculer le sujet à l'exécution.

Décorateurs sucrés

@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 fonction décorée prend un argument : une dataclass typée correspondant au sujet. Voir la référence des événements pour le tableau complet sujet ↔ dataclass.

API brute

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

Propriétés sucrées disponibles (toutes des décorateurs) :

Propriété Se déclenche sur
on_document_opened Document ouvert dans un onglet
on_document_closed Document fermé
on_document_saved Document écrit sur le disque
on_selection_changed Caret / sélection déplacé dans le document actif
on_active_document_changed Onglet de premier plan changé
on_app_about_to_quit L'éditeur se ferme — dernière occasion de s'exécuter
on_command_shortcut_changed Le raccourci effectif d'une commande a changé (remappage utilisateur / import de keymap / résolution de conflit) ; charge utile : command_id, sequence ("" = non lié)

Les noms de sujets inconnus lèvent ValueError au moment de l'abonnement. Les abonnés de la phase 1 peuvent s'exécuter après le gestionnaire natif correspondant ; traitez la livraison d'événements comme informative, pas comme faisant autorité.


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")

Chaque niveau correspond à l'ILogger de l'hôte. Les lignes sont préfixées [python:<plugin.id>], de sorte que votre sortie est repérable dans le journal de diagnostics de l'éditeur :

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

Ne faites jamais de print() depuis un plugin — le stdout du worker est le canal RPC, et y écrire corrompt le protocole. ctx.log.* est la seule manière autorisée d'émettre du texte de diagnostic.


status

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

Publie une notification transitoire dans la barre d'état.

  • message (str) : ce qu'il faut afficher. Chaînes courtes uniquement — la barre d'état a une largeur fixe.
  • timeout_ms (int, 0 par défaut) : durée d'affichage à l'écran ; 0 laisse l'hôte choisir une valeur raisonnable (quelques secondes).
  • level (str, "info" par défaut) : "info" / "warning" / "error". Les niveaux supérieurs peuvent prolonger un peu la durée minimale d'affichage afin qu'une erreur ne soit pas immédiatement effacée par un message d'information de routine.
ctx.status.show("saved", timeout_ms=1500)
ctx.status.show("no results", level="warning")

ctx.status.show est de type « lancer et oublier ». Si l'hôte n'a pas de barre d'état visible (headless / build minimale), l'appel est silencieusement abandonné.



editor

Accès synchrone complet au(x) document(s) actif(s). Chaque méthode bloque le thread appelant jusqu'à la réponse de l'hôte (typiquement quelques microsecondes quand le worker et l'éditeur partagent la même machine). Autorisé depuis tout gestionnaire de commande / d'événement ; jamais depuis un callback async def (voir la note sur les threads en haut de cette page).

Énumération des documents

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

Accès au tampon

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 évite de tirer tout le tampon à travers le RPC quand vous n'avez besoin que d'une tranche — préférez-le pour trier/transformer de gros fichiers.

Sélection

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))

Caret

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

Opérations sur les fichiers

new_doc = ctx.editor.open_file("/path/to/file")     # doc-id or 0 on failure
ok      = ctx.editor.save_document(doc)
dirty   = ctx.editor.is_modified(doc)

Recherche

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". Voir mte.SearchOptions pour la liste complète des champs.

Indentation et syntaxe

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

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

settings

Magasin clé/valeur typé et persistant, restreint à plugins/<your.id>/ sur l'hôte. Toutes les méthodes sont synchrones ; get/set sont inférés à partir du type Python de la valeur par défaut.

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

Types :

Type Python de default Type sur le fil Type de retour de get()
bool bool bool
int (non booléen) int int
float double float
str (ou None) string str

Également :

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

Réagissez aux changements effectués dans la boîte de dialogue Préférences en vous abonnant à SettingsChanged (voir la référence des événements ; phase 3 pour la surface d'événements complète).

app_settings

Vue en lecture seule de la configuration globale de l'éditeur. Les noms de clés vivent dans l'en-tête C++ PluginApi/AppSettingsKeys.h ; consultez l'en-tête pour la liste faisant autorité.

tab_width = ctx.app_settings.get("editor.tabWidth", 4)
theme     = ctx.app_settings.get("editor.theme.variant", "light")

Les setters, remove() et sync() sur ctx.app_settings lèvent RuntimeError — la vue est volontairement en lecture seule.


completions

Enregistrez un fournisseur de complétion qui participe à la popup de l'éditeur :

@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)]

L'appelable décoré est invoqué à chaque frappe (ou Ctrl+Espace) dont le document correspond au filtre de langage. Champs de req :

Champ Type Signification
document int Document en cours d'édition
prefix str Fragment de mot immédiatement avant le caret
position int Offset du caret en octets
language_id str Id du coloriseur ("python", "cpp", …)
manual bool True quand l'utilisateur a pressé Ctrl+Espace ; False en déclenchement automatique

Renvoyez une liste de mte.CompletionItem (ou de simples chaînes, ou des dicts avec {text, detail, kind}). Kinds : mte.KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.

Échéance

L'hôte envoie à votre fournisseur une requête de frappe avec une échéance d'environ 30 ms. Si votre callback la dépasse, la popup s'affiche sans vos candidats pour cette frappe. Construisez tout index coûteux en arrière-plan et répondez depuis le cache — ne faites jamais d'E/S dans le callback. La complétion réseau de style LSP relève d'une future surface asynchrone, pas d'ici.

languages=[] (ou omis) signifie « tous les documents ». Plusieurs fournisseurs par plugin sont autorisés ; chaque décorateur enregistre le sien.


data_dir

path = ctx.data_dir()                    # pathlib.Path

Renvoie le répertoire par plugin accessible en écriture que l'hôte a déjà créé pour vous. À utiliser pour les caches, les index et toute donnée qui vous appartient. ctx.diagnostics_dir() renvoie de même le répertoire partagé des crashs/journaux (en lecture seule pour vous — c'est le rapporteur de crash qui y écrit).


docks

Panneaux ancrables déclaratifs — une UI de panneau latéral construite à partir d'une spécification de formulaire, pas de widgets Qt. Voir mte.ui pour le vocabulaire des constructeurs.

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) -> str

Enregistre un panneau ancrable. Renvoie l'id du panneau (alloué automatiquement quand id vaut None).

  • id (str, optionnel) — identifiant stable utilisé par ctx.ui.get / ctx.ui.set pour adresser les contrôles de ce panneau. Si vous n'en fournissez pas, l'hôte alloue un id py-dock/N et le renvoie.
  • title (str) — affiché dans l'en-tête du panneau.
  • area (str)"left" / "right" / "top" / "bottom". Les valeurs inconnues retombent sur "right".
  • form (dict) — une spécification construite avec les constructeurs mte.ui.
  • initially_visible (bool) — si le panneau est affiché au chargement du plugin. True par défaut. L'utilisateur peut ensuite le masquer ; vous ne pouvez pas forcer sa réouverture.

ctx.docks.on_change(callback) -> str

Alloue un id de gestionnaire pour relier le champ on_change d'un contrôle à un appelable Python. Le callback reçoit la nouvelle valeur sous forme de chaîne ; les contrôles booléens livrent "true" / "false", les contrôles numériques livrent l'entier sous forme de chaîne.

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)])

Les ids de gestionnaires on_change sont préfixés py-dock-cb/N et n'entrent jamais en collision avec les ids de gestionnaires de commandes / événements / complétions.

Le callback est invoqué sur la boucle d'événements du worker, PAS sur le pool de threads — restez rapide. Utilisez ctx.log, modifiez un petit état en mémoire ou déclenchez une commande de suivi ; ne faites pas de travail lourd ici.


settings_page

Ajoute une page dans la boîte de dialogue Préférences de l'éditeur. Même vocabulaire de spécification de formulaire que les panneaux ancrables ; même persistance 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, obligatoire) — chemin séparé par des barres obliques dans l'arborescence des Préférences, p. ex. "Plugins/My Plugin". Les catégories imbriquées apparaissent comme des nœuds de l'arborescence.
  • title (str, obligatoire) — titre affiché au-dessus de la page.
  • form (dict, obligatoire) — une spécification construite avec les constructeurs mte.ui.
  • scope (str, optionnel) — portée d'événement émise sur SettingsChanged après que l'utilisateur a cliqué sur OK / Appliquer.

Renvoie l'id du gestionnaire.

Annuler n'annule PAS les changements

Les contrôles avec binds="settings:<key>" persistent immédiatement au fil des modifications de l'utilisateur, donc cliquer sur Annuler dans la boîte de dialogue Préférences ne les défait pas. C'est une limitation connue de la phase 3 ; un raffinement futur enveloppera les réglages sous-jacents dans un proxy de mise en tampon.


ui

Lit et écrit les valeurs des contrôles dans les panneaux que vous avez déjà enregistrés. Les valeurs sont toujours des chaînes sur le fil.

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]

Lit la valeur actuelle d'un contrôle. Renvoie None quand le panneau ou le contrôle est inconnu (ce n'est pas une erreur — traitez None et "" comme « vide »).

  • Label / Button / LineEdit / ComboBox — renvoie le texte.
  • Checkbox — renvoie "true" ou "false".
  • NumberSpin — renvoie l'entier sous forme de chaîne décimale.
  • List — renvoie le texte de l'élément actuellement sélectionné (vide quand rien n'est sélectionné).

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

Écrit une valeur dans un contrôle par programmation. Renvoie True quand l'écriture a réussi, False quand le panneau/contrôle est inconnu ou que la valeur ne peut pas être convertie (p. ex. un non-entier envoyé à un NumberSpin).

Les booléens Python True / False sont convertis en "true" / "false" avant l'envoi ; tout le reste passe par str().

Un set piloté par l'hôte ne redéclenche PAS le callback on_change du contrôle, vous pouvez donc mettre à jour vos propres widgets depuis un gestionnaire de commande sans boucles infinies.


Récapitulatif des liaisons à l'exécution (phase 3)

Champ de la spécification Comportement à l'exécution
id="…" Adresser le contrôle depuis ctx.ui.get / ctx.ui.set.
binds="settings:<key>" Lit la valeur initiale depuis ctx.settings ; réécrit à chaque modification de l'utilisateur.
on_click="<cmd>" Les boutons invoquent l'id de commande enregistré au clic.
on_change=handler Déclenche le callback enregistré via ctx.docks.on_change(...) à chaque modification de l'utilisateur.

Les mêmes règles s'appliquent aux panneaux ancrables et aux pages de Préférences.