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¶
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 dansplugin.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".Ctrlcorrespond automatiquement au modificateur principal de la plateforme (Cmdsur 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 ; lisezctx.commands.effective_shortcut(id)lors de la construction de vos widgets et réappliquez sur l'événement"command.shortcutChanged".
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.
menus¶
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)¶
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)¶
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 :
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¶
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 ;0laisse 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 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¶
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é parctx.ui.get / ctx.ui.setpour adresser les contrôles de ce panneau. Si vous n'en fournissez pas, l'hôte alloue un idpy-dock/Net 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 constructeursmte.ui.initially_visible(bool) — si le panneau est affiché au chargement du plugin.Truepar 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 constructeursmte.ui.scope(str, optionnel) — portée d'événement émise surSettingsChangedaprè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.