Aller au contenu

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

<plugin-id>/
├── plugin.json      # required
└── main.py          # required

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 <nom>.mteplugin et l'installe via Préférences → Extensions → Installer…. 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

  1. "apiVersion" doit être 1 (la version actuelle de l'hôte).
  2. "entry" doit être "python" — rien d'autre ne route vers ce backend.
  3. "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).
  4. "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é, utilisez org.example..
  5. menus[] doit contenir au moins une entrée dont le title correspond au segment le plus à gauche de chaque chemin de menu utilisé dans main.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éclarez pyRequires quand 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.

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

  1. register(ctx) DOIT être une fonction de niveau supérieur. La sonde statique exige une ligne commençant par def register( en colonne 0. L'imbriquer dans une classe, la décorer au niveau du module ou en faire un async def fait rejeter silencieusement l'extension à la découverte.
  2. Importez depuis mte quand vous vous abonnez aux événements — les dataclasses typées (mte.DocumentSaved, etc.) viennent de ce paquet. import mte en tête est sûr même inutilisé ; le # noqa: F401 évite le bruit du linter.
  3. Jamais print(). Le stdout du worker est le canal RPC ; y écrire corrompt le protocole. Utilisez ctx.log.info / warning / error.
  4. N'importez jamais rien de non déclaré. Au-delà de la bibliothèque standard et de mte, chaque import tiers doit figurer dans pyRequires de plugin.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ève ModuleNotFoundError chez l'utilisateur.
  5. 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_dir sont 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'appels ctx.editor.*, cache uniquement. Construisez les index ailleurs (p. ex. sur document.opened) et répondez depuis un dict.
  6. N'importez pas Qt, PySide, PyQt ni Scintilla. Ils ne sont pas disponibles dans le worker.
  7. 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.
  8. Les chemins de menu doivent commencer par un menu de premier niveau aussi déclaré dans menus[] de plugin.json. Convention : "Plugins/<Nom>/<Verbe>".

4. API complète — tout ce qu'il y a sur ctx

Vous avez douze services (phases 1 + 2 + 3). 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="")
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.

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

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.

ctx.events

Six 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_selection_changed selection.changed mte.SelectionChanged(document: int)
@ctx.events.on_active_document_changed document.activeChanged mte.ActiveDocumentChanged(document: int)
@ctx.events.on_app_about_to_quit app.aboutToQuit mte.AppAboutToQuit()

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

ctx.log.trace  (str)
ctx.log.debug  (str)
ctx.log.info   (str)
ctx.log.warning(str)
ctx.log.error  (str)

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

level doit être "info" / "warning" / "error". timeout_ms=0 laisse l'hôte choisir un défaut.

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
ok      = ctx.editor.save_document(doc)
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))
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)

p = ctx.data_dir()   # pathlib.Path -- writable, host-created

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:<clé>" — la valeur du contrôle persiste automatiquement via ctx.settings (même magasin que ctx.settings.set).
  • on_click="<cmd.id>" — Button uniquement ; invoque une commande enregistrée.
  • on_change=<handler> — alloué via ctx.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.

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:<clé>" 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.

wordcount/plugin.json
{
  "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 }
  ]
}
wordcount/main.py
"""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

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

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

ctx.menus.add_separator("Plugins/Mine")

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

Faux :

def _handler():
    print("did the thing")   # <-- corrupts the RPC channel

Juste :

def _handler():
    ctx.log.info("did the thing")

register dans une classe

Faux :

class Plugin:
    def register(self, ctx):   # <-- static probe rejects this
        ...

Juste :

def register(ctx):
    ...

async def register

Faux :

async def register(ctx):   # <-- probe accepts but runtime never awaits
    ...

Juste :

def register(ctx):
    ...

Imports tiers sans livrer la wheel

Faux :

import requests   # <-- ImportError on most user machines

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 :

from PySide6 import QtWidgets   # <-- not available; no Qt in the worker

Juste : pas de Qt du tout. Toute l'interface que vous pouvez apporter passe par ctx.menus, ctx.status et (depuis la phase 3) 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 :

"menus": [ { "title": "Plugins", "barPriority": 570 } ]
ctx.menus.add_item("Tools/Mine/Do", "mine.do")   # <-- Tools ≠ Plugins

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 :

  1. Lancer l'éditeur depuis un shell où un interpréteur Python 3.9+ est dans le PATH (ou MTE_PYTHON pointe dessus).
  2. Chercher PluginHost: Python plugin backend enabled; interpreter=… dans le journal. S'il manque, aller au Dépannage.
  3. Chercher [python:<votre.id>] register() ran (si vous avez suivi l'exemple wordcount) — preuve que register s'est exécuté.
  4. Si l'entrée de menu manque, chercher dans le journal les explications PythonPluginBackend: skipping '<id>': ….
  5. 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 <nom>.mteplugin et utiliser Préférences → Extensions → Installer… (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).
  • Redémarrer l'éditeur.
  • Définir MTE_PYTHON si 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.