Écrire une extension Python pour Modular Text Editor avec Claude¶
Public : un assistant IA (Claude / ChatGPT / etc.). Cette page est un jeu d'instructions autonome : donnez-la à l'assistant avec la tâche (« écris-moi une extension qui fait X »), et l'assistant devrait produire un dossier d'extension complet, correct et fonctionnel sans lire le reste de la documentation.
Si vous êtes un humain écrivant une extension à la main, commencez ici — cette page est optimisée pour la consommation par IA et saute les exemples pédagogiques.
1. Ce que vous produisez¶
Un dossier avec exactement deux fichiers (plus des extras facultatifs) :
Rendez le dossier sous forme d'un bloc de code par fichier. L'utilisateur
dépose le dossier dans le répertoire d'extensions de l'éditeur, ou zippe le
dossier, renomme l'archive <name>.mteplugin et l'installe via
Préférences → Extensions → Installer une extension.... Les deux chemins exigent un
redémarrage pour charger.
Les deux fichiers sont décrits exhaustivement ci-dessous. Suivez les règles à la lettre — l'hôte exécute une sonde statique et rejette les extensions qui les enfreignent.
2. plugin.json — modèle canonique¶
{
"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 }
]
}
Règles strictes¶
"apiVersion"doit être1(la version actuelle de l'hôte)."entry"doit être"python"— rien d'autre ne route vers ce backend."module"doit être"main"sauf raison explicite de nommer le module autrement — et jamais un chemin à points (la phase 1 ne prend en charge que les noms de modules plats)."id"doit être en style DNS inversé (org.<vendor>.<name>) et globalement unique parmi les extensions installées de l'utilisateur. Sans vendor précisé, utilisezorg.example..menus[]doit contenir au moins une entrée dont letitlecorrespond au segment le plus à gauche de chaque chemin de menu utilisé dansmain.py. Préférez"Plugins"pour tout ce qui est destiné à l'utilisateur et n'appartient pas manifestement à un menu du cœur.
Champs facultatifs¶
"pyRequires": []— un tableau de chaînes d'exigences pip (p. ex.["requests>=2.31"]). Non vide, l'éditeur provisionne un venv par extension au premier lancement (pip install— réseau nécessaire cette fois-là) et exécute l'extension dessous. Préférez la bibliothèque standard quand elle suffit (pas de délai de provisionnement, fonctionne hors ligne) ; déclarezpyRequiresquand la tâche exige vraiment un paquet tiers, et prévenez l'utilisateur que le premier lancement marquera une pause d'installation."permissions": []— déclarations grossières de capacités (p. ex.["network"]), affichées en lecture seule sur la page des Extensions. Déclarez"network"quand l'extension fait des appels sortants ; c'est un étiquetage honnête pour l'utilisateur, pas une contrainte."requires": []— identifiants d'AUTRES extensions sans lesquelles celle-ci ne peut pas fonctionner (pas des paquets pip). Elles démarrent d'abord ; si l'une manque ou est désactivée, cette extension ne démarre pas et la page des Extensions dit pourquoi. Laissez vide sauf si la tâche de l'utilisateur dépend vraiment d'une autre extension."apiMinorVersion": 0— la version mineure minimale de l'API de l'éditeur dont l'extension a besoin. Laissez0sauf si vous utilisez un appel documenté comme plus récent.
Valeurs à NE PAS inventer¶
- Tout champ absent de la liste ci-dessus. L'hôte ignore les clés inconnues ; les ajouter est un risque de maintenance.
3. main.py — squelette canonique¶
"""<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>")
Règles strictes¶
register(ctx)DOIT être une fonction de niveau supérieur. La sonde statique exige une ligne commençant pardef register(en colonne 0. L'imbriquer dans une classe, la décorer au niveau du module ou en faire unasync deffait rejeter silencieusement l'extension à la découverte.- Importez depuis
mtequand vous vous abonnez aux événements — les dataclasses typées (mte.DocumentSaved, etc.) viennent de ce paquet.import mteen tête est sûr même inutilisé ; le# noqa: F401évite le bruit du linter. - Jamais
print(). Le stdout du worker est le canal RPC ; y écrire corrompt le protocole. Utilisezctx.log.info / warning / error. - N'importez jamais rien de non déclaré. Au-delà de la bibliothèque
standard et de
mte, chaque import tiers doit figurer danspyRequiresdeplugin.json(l'éditeur le pip-installe dans le venv de l'extension au premier lancement) ou être vendored dans le dossier. Un import non déclaré lèveModuleNotFoundErrorchez l'utilisateur. - Les gestionnaires de commandes / événements peuvent bloquer
brièvement — ils tournent sur le pool de threads du worker, les
appels synchrones vers
ctx.editor/ctx.settings/ctx.data_dirsont donc légaux. Mais les rappels de fournisseurs de complétion (@ctx.completions.provider(...)) NE DOIVENT PAS bloquer : ils tournent sur la boucle d'événements avec une échéance d'environ 30 ms. Pas d'E/S, pas d'appelsctx.editor.*, cache uniquement. Construisez les index ailleurs (p. ex. surdocument.opened) et répondez depuis un dict. - N'importez pas Qt, PySide, PyQt ni Scintilla. Ils ne sont pas disponibles dans le worker.
- Les ids de commandes doivent être préfixés par l'espace de noms de
votre extension pour éviter les collisions avec le cœur / d'autres
extensions.
<id>.<verb>ci-dessus ; p. ex.wordcount.count,hello.sayHi. - Les chemins de menu doivent commencer par un menu de premier
niveau aussi déclaré dans
menus[]deplugin.json. Convention :"Plugins/<Plugin Name>/<Verb>".
4. API complète — tout ce qu'il y a sur ctx¶
Vous avez dix-huit services : commands, menus, events, log, status,
editor, languages, settings, app_settings, document_settings, theme,
diagnostics, compare, completions, docks, settings_page, ui et
data_dir()/diagnostics_dir(). Tout ce qui n'est pas listé ici n'existe
pas — n'inventez pas de services. (Les venvs par
extension se déclarent via pyRequires, pas un service ctx ; le
substrat streaming/cancel n'a pas encore d'API côté extension.)
ctx.commands¶
@ctx.commands.command(*, id, title, shortcut="", category="",
requires_editor=False, requires_writable_editor=False)
def _handler(): ...
Décorateur. Enregistre une commande et lie _handler (fonction sans
argument renvoyant None). Les exceptions dans _handler sont
interceptées par le worker et écrites sur stderr ; elles ne font pas
planter l'éditeur.
Définissez requires_editor quand la commande lit le document actif
(tampon, sélection, curseur) et requires_writable_editor quand elle
modifie le texte. L'hôte grise alors la commande là où elle ne pourrait pas
fonctionner — sur un onglet sans document (vue de différences ou de journal)
et, pour l'indicateur « writable », sur un document en lecture seule — au
lieu de laisser l'utilisateur déclencher une opération sans effet.
L'indicateur « writable » implique l'autre ; pour les commandes qui ouvrent
seulement une boîte de dialogue ou un panneau, laissez les deux désactivés.
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 est "Top/Sub/…/Libellé". Les sous-menus intermédiaires sont créés
à la demande.
set_item_checked rend l'entrée cochable et pose sa coche. Pour les
bascules marche/arrêt persistantes. Le premier appel promeut ; les
suivants ne font que basculer. No-op silencieux sur les ids inconnus.
set_item_enabled grise l'entrée. Pour une commande soumise à une
condition préalable, afin qu'une entrée indisponible en ait l'air au lieu
de ne rien faire au clic. Idempotent ; les entrées démarrent activées ;
no-op silencieux sur les ids inconnus.
ctx.events¶
Dix-sept sujets, chacun avec un décorateur sucré et une forme brute.
| Sucre | Sujet brut | Charge utile |
|---|---|---|
@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) — à chaque modification, restez 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() — une fois par exécution, après la première image |
@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 dans la colonne des signets ; bits de modifiers : mte.MODIFIER_* |
Forme brute :
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
N'inventez pas de nouveaux sujets — la forme brute les valide et lève
ValueError pour tout ce qui n'est pas dans la table.
ctx.log¶
Fire-and-forget. Les lignes portent le préfixe [python:<plugin.id>]
dans le journal de diagnostics de l'hôte.
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 doit être "info" / "warning" / "error". timeout_ms=0
laisse l'hôte choisir un défaut. show() sert aux notifications
transitoires ; tout ce qui reste à l'écran (un compteur vivant, un drapeau
de mode) est un segment — les deux ne s'écrasent jamais.
ctx.editor (phase 2)¶
Chaque méthode est synchrone : elle bloque le thread appelant.
Acceptable depuis un rappel def de commande / d'événement (le worker
répartit les gestionnaires sync sur un pool de threads) ; illégal
depuis un gestionnaire async def et depuis les fournisseurs
ctx.completions (qui tournent directement sur la boucle d'événements).
Les documents sont référencés par des ids int opaques (0 = aucun
document).
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 (phase 2)¶
Magasin clé/valeur typé par extension (ctx.settings) plus une vue en
lecture seule des paramètres de l'application (ctx.app_settings). Le
type est inféré 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) # 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 lèvent RuntimeError. Dans votre
propre logique de lecture, testez bool avant int (en Python,
isinstance(True, int) == True).
ctx.completions (phase 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)]
Champs de req : document, prefix, position, language_id,
manual. Genres : KIND_KEYWORD / KIND_WORD / KIND_SYMBOL /
KIND_SNIPPET / KIND_UNKNOWN. Renvoyez list[CompletionItem], des
chaînes simples ou des dicts. Le rappel DOIT revenir en ~30 ms, sinon
l'hôte abandonne vos résultats pour cette frappe — pas d'E/S, pas de
ctx.editor.text(...), cache uniquement. Construisez les index ailleurs
(p. ex. sur document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (phase 2)¶
data_dir() pour les caches et index. diagnostics_dir() est en lecture
seule (le rapporteur de plantages y écrit).
ctx.docks — panneaux ancrables déclaratifs (phase 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")]),
]),
)
Construisez la spécification du formulaire avec les aides
mte.ui. Le vocabulaire des contrôles est fixe : Label /
Button / Checkbox / LineEdit / NumberSpin / ComboBox / List dans
Column / Row / Group.
binds="settings:<key>"— la valeur du contrôle persiste automatiquement viactx.settings(même magasin quectx.settings.set).on_click="<cmd.id>"— Button uniquement ; invoque une commande enregistrée.on_change=<handler>— alloué viactx.docks.on_change(callback); se déclenche sur les modifications de l'utilisateur.
Les rappels on_change tournent sur la boucle d'événements du worker
(PAS le pool de threads). Restez léger — pas d'appels éditeur, pas de
long travail. Modifiez un petit état en mémoire, journalisez ou
déclenchez une commande ; le gros travail se fait dans la commande.
Passez initially_visible=False pour un panneau qui ne doit pas surgir au
démarrage, puis révélez-le depuis sa commande avec
ctx.docks.set_panel_visible(panel_id, True) (le remonte aussi au premier
plan ; idempotent ; ids inconnus ignorés).
ctx.docks.is_panel_visible(panel_id) indique si un panneau est ouvert (un
onglet à l'arrière-plan compte). Plusieurs panneaux qui vont ensemble :
passez le même tab_group="<plugin.id>/panels" et la même area, et ils
partagent un seul panneau ancré à onglets.
ctx.line_markers — marques sur les lignes du document¶
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
Enregistrez les types une seule fois dans register() ; l'éditeur n'a que
quelques emplacements. Ne revendiquez le clic dans la marge que si
l'extension lui donne un sens — les clics arrivent de toute façon par
@ctx.events.on_margin_clicked.
ctx.languages — coloration syntaxique¶
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 doit nommer un lexer backend existant ; styles et keywords
utilisent les noms du lexer (même forme que les colorizers/*.json
livrés), jamais d'ids numériques.
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 — soulignements ondulés¶
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
Envoyez à chaque fois l'ensemble COMPLET pour votre source ; il remplace
le précédent. source est obligatoire.
ctx.theme — couleurs thémables pour votre propre interface¶
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 côte à côte¶
ctx.compare.open(mte.CompareSource(title, path=..., content=...),
mte.CompareSource(...), ignore_whitespace=False,
ignore_case=False, ignore_eol=False) # -> bool
ctx.events.publish¶
Une extension ne peut publier que workspace.rootChanged /
workspace.filesChanged ; tout le reste lève ValueError. Chaque
événement de l'hôte sauf document.aboutToSave peut être souscrit (voir la
liste des décorateurs) ; l'événement natif DocumentAboutToClose n'a aucun
sujet Python — utilisez document.closed.
ctx.settings_page — pages de Préférences déclaratives (phase 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"),
]),
)
Mêmes constructeurs, même sémantique de binds. Deux contrôles partageant
un binds="settings:<key>" se reflètent en temps réel — le panneau
ancrable et la page de Préférences peuvent tous deux porter une case pour
le même paramètre, et basculer l'un bascule l'autre.
Réserve de la phase 3
Les valeurs persistent au fil des modifications de l'utilisateur — Annuler ne revient pas en arrière. Cela s'améliorera dans un raffinement futur.
ctx.ui.get / ctx.ui.set — accès à l'exécution aux contrôles (phase 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 renvoie la valeur principale sous forme de chaîne :
- Label / Button / LineEdit / ComboBox → texte
- Checkbox →
"true"/"false" - NumberSpin → entier en décimal
- List → texte de l'élément sélectionné (chaîne vide sinon)
Un set piloté par l'hôte est silencieux : il ne redéclenche PAS le
on_change du contrôle, vous pouvez donc mettre à jour vos propres
widgets depuis un gestionnaire de commande sans boucles infinies.
5. Exemple complet — extension wordcount¶
Copiez tel quel et changez id, name et les title pour votre cas.
Il exerce chaque service de la phase 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")
Déroulé attendu : ouvrir l'éditeur, enregistrer un fichier trois fois,
puis cliquer Plugins → Word Count → Report save count. La barre d'état
montre 3 save(s) this session. Le journal consigne chaque étape.
6. Tâches courantes — recettes à copier-coller¶
Chaque recette est un extrait de corps seul : à coller dans
def register(ctx):.
Ajouter une commande de menu¶
@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")
Réagir à l'enregistrement¶
Avertir à la fermeture¶
@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
ctx.log.warning("mine: last chance to persist state")
Journaliser à un niveau précis¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Séparateur de menu¶
Plusieurs commandes liées¶
@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")
Bascule marche/arrêt persistante avec coche 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)
Panneau latéral avec saisie, case et deux boutons¶
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", "")
Page de Préférences miroir des paramètres d'un dock¶
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. Anti-patrons — à NE PAS faire¶
print() pour le diagnostic¶
Faux :
Juste :
register dans une classe¶
Faux :
Juste :
async def register¶
Faux :
Juste :
Imports tiers sans livrer la wheel¶
Faux :
Juste (phase 1) : restez sur la bibliothèque standard. S'il vous faut
vraiment du tiers, livrez la wheel dans le dossier de l'extension et
étendez sys.path dans les premières lignes de main.py ; voir
Empaquetage.
Imports Qt¶
Faux :
Juste : pas de Qt du tout. Toute l'interface que vous pouvez apporter
passe par ctx.menus, ctx.status, ctx.diagnostics, ctx.compare et
les panneaux déclaratifs dock/paramètres. Jamais un widget brut.
Bloquer dans un gestionnaire¶
Faux :
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Juste (phase 1) : déléguez à un thread, communiquez via 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")
Chemin de menu dont le segment de tête n'est pas dans plugin.json¶
Faux :
Juste : alignez le segment de tête du chemin avec menus[]. Utilisez
"Plugins/Mine/Do" ci-dessus.
Stocker ctx pour plus tard¶
Faux :
_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?
Juste : capturez les services nécessaires dans la fermeture qui les
utilise. register est le lieu du câblage ; tout le reste est un
gestionnaire qui a déjà accès par fermeture.
8. Liste de débogage pour l'utilisateur¶
Joignez ceci à la fin de votre réponse quand vous produisez une extension, pour que l'utilisateur sache quoi vérifier en cas de panne :
- Lancer l'éditeur depuis un shell où un interpréteur Python 3.9+ est
dans le
PATH(ouMTE_PYTHONpointe dessus). - Chercher
PluginHost: Python plugin backend enabled; interpreter=…dans le journal. S'il manque, aller au Dépannage. - Chercher
[python:<your.id>] register() ran(si vous avez suivi l'exemplewordcount) — preuve queregisters'est exécuté. - Si l'entrée de menu manque, chercher dans le journal les explications
PythonPluginBackend: skipping '<id>': …. - Si cliquer le menu ne fait rien, chercher les lignes
[worker stderr] …— les tracebacks Python y atterrissent.
9. Contraintes à répéter à la fin de chaque réponse¶
Quand vous produisez une extension, dites toujours à l'utilisateur :
- Installer d'une des deux façons :
- zipper le contenu du dossier en
<name>.mtepluginet utiliser Préférences → Extensions → Installer une extension... (valide le paquet avant de toucher quoi que ce soit), ou - déposer le dossier dans le répertoire d'extensions par utilisateur
(
%LOCALAPPDATA%/MTE/Plugins/sous Windows ;~/Library/Application Support/MTE/Plugins/sous macOS ;~/.local/share/MTE/Plugins/sous Linux).
- zipper le contenu du dossier en
- Redémarrer l'éditeur.
- Définir
MTE_PYTHONsi le journal dit « no Python interpreter found ». - Renvoyer la ligne de journal
[python:<id>] …pour confirmer l'exécution.
10. Source de vérité¶
Si un énoncé de cette page contredit ce que le paquet client Python
implémente réellement, le paquet gagne. Il est livré à côté de l'éditeur
dans <exe>/Python/mte/ — lisez cette source avant de trancher un
comportement. Le côté C++ du protocole est dans
Components/PluginHost/Src/PyServiceBridge.cpp et la spécification de fil
dans Python/PROTOCOL.md.