Zum Inhalt

Ein Python-Plugin für den Modular Text Editor mit Claude schreiben

Zielgruppe: ein KI-Assistent (Claude / ChatGPT / usw.). Diese Seite ist ein in sich geschlossener Anweisungssatz: Geben Sie sie dem Assistenten zusammen mit der Aufgabe („schreib mir ein Plugin, das X tut“), und der Assistent sollte einen vollständigen, korrekten, funktionierenden Plugin-Ordner produzieren, ohne den Rest der Doku zu lesen.

Wenn Sie ein Mensch sind, der von Hand ein Plugin schreibt, beginnen Sie hier — diese Seite ist für KI-Konsum optimiert und lässt die freundlichen Beispiele aus.


1. Was Sie produzieren

Einen Ordner mit genau zwei Dateien (plus optionale Extras):

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

Geben Sie den Ordner als Codeblock pro Datei zurück. Der Nutzer legt den Ordner entweder in das Plugin-Verzeichnis des Editors, oder er zippt den Ordner, benennt das Archiv in <name>.mteplugin um und installiert es über Einstellungen → Plugins → Installieren…. Beide Wege brauchen einen Neustart zum Laden.

Die zwei Dateien sind unten erschöpfend beschrieben. Befolgen Sie die Regeln wörtlich — der Host führt eine statische Prüfung durch und lehnt Plugins ab, die sie brechen.


2. plugin.json — kanonische Vorlage

{
  "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 }
  ]
}

Harte Regeln

  1. "apiVersion" muss 1 sein (die aktuelle Host-Version).
  2. "entry" muss "python" sein — nichts anderes führt zu diesem Backend.
  3. "module" muss "main" sein, sofern es keinen ausdrücklichen Grund für einen anderen Modulnamen gibt — und nie ein gepunkteter Pfad (Phase 1 unterstützt nur flache Modulnamen).
  4. "id" muss Reverse-DNS-Stil haben (org.<vendor>.<name>) und global eindeutig unter den installierten Plugins des Nutzers sein. Hat der Nutzer keinen Vendor genannt, verwenden Sie org.example..
  5. menus[] muss mindestens einen Eintrag enthalten, dessen title dem linkesten Segment jedes in main.py verwendeten Menüpfads entspricht. Bevorzugen Sie "Plugins" für alles Nutzerseitige, das nicht offensichtlich in ein Kernmenü gehört.

Optionale Felder

  • "pyRequires": [] — ein Array von pip-Requirement-Zeichenketten (z. B. ["requests>=2.31"]). Wenn nicht leer, provisioniert der Editor beim ersten Start eine Pro-Plugin-venv (pip install — braucht dieses eine Mal Netz) und führt das Plugin darunter aus. Bevorzugen Sie die Standardbibliothek, wenn sie genügt (keine Provisionierungsverzögerung, funktioniert offline); deklarieren Sie pyRequires, wenn die Aufgabe wirklich ein Drittpaket braucht, und sagen Sie dem Nutzer, dass der erste Start zum Installieren pausiert.
  • "permissions": [] — grobe Fähigkeitsdeklarationen (z. B. ["network"]), schreibgeschützt auf der Plugins-Seite gezeigt. Deklarieren Sie "network", wenn das Plugin ausgehende Verbindungen aufbaut; das ist ehrliche Kennzeichnung für den Nutzer, keine Durchsetzung.

Werte, die Sie NICHT erfinden sollten

  • Jedes oben nicht gelistete Feld. Der Host ignoriert unbekannte Schlüssel; sie hinzuzufügen ist ein Wartungsrisiko.

3. main.py — kanonisches Skelett

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

Harte Regeln

  1. register(ctx) MUSS eine Funktion oberster Ebene sein. Die statische Prüfung verlangt eine mit def register( in Spalte 0 beginnende Zeile. Verschachteln Sie sie in eine Klasse, dekorieren sie auf Modulebene oder machen sie zu async def, wird das Plugin bei der Erkennung stillschweigend abgelehnt.
  2. Importieren Sie aus mte, wenn Sie Ereignisse abonnieren — die typisierten Dataclasses (mte.DocumentSaved usw.) kommen aus diesem Paket. import mte am Anfang ist auch ungenutzt sicher; das # noqa: F401 oben verhindert Lint-Lärm.
  3. Niemals print(). Der stdout des Workers ist der RPC-Kanal; Schreiben dorthin korrumpiert das Protokoll. Verwenden Sie ctx.log.info / warning / error.
  4. Importieren Sie nie etwas Undeklariertes. Jenseits der Standardbibliothek und mte muss jeder Drittimport in pyRequires der plugin.json stehen (der Editor pip-installiert ihn beim ersten Start in die venv des Plugins) oder im Plugin-Ordner vendored sein. Ein undeklarierter Import wirft auf der Maschine des Nutzers ModuleNotFoundError.
  5. Befehls-/Ereignishandler dürfen kurz blockieren — sie laufen im Thread-Pool des Workers, synchrone Aufrufe in ctx.editor / ctx.settings / ctx.data_dir sind also legal. Aber Vervollständigungs-Callbacks (@ctx.completions.provider(...)) DÜRFEN NICHT blockieren: Sie laufen auf der Ereignisschleife mit einer ~30-ms-Deadline. Kein I/O, keine ctx.editor.*-Aufrufe, nur Cache. Indizes anderswo bauen (z. B. bei document.opened) und aus einem Dict antworten.
  6. Kein Import von Qt, PySide, PyQt oder Scintilla. Sie sind im Worker nicht verfügbar.
  7. Befehls-Ids müssen mit dem Namensraum Ihres Plugins geprefixt sein, um Kollisionen mit dem Kern / anderen Plugins zu vermeiden. <id>.<verb> oben; z. B. wordcount.count, hello.sayHi.
  8. Menüpfade müssen mit einem Menü oberster Ebene beginnen, das Sie auch in menus[] der plugin.json deklariert haben. Konvention: "Plugins/<Plugin-Name>/<Verb>".

4. Vollständige API — alles auf ctx

Sie haben zwölf Dienste (Phasen 1 + 2 + 3). Was hier nicht gelistet ist, existiert nicht — erfinden Sie keine Dienste. (Pro-Plugin-venvs werden über pyRequires deklariert, nicht über einen ctx-Dienst; das Streaming-/Cancel-Protokollsubstrat hat noch keine Plugin-API.)

ctx.commands

@ctx.commands.command(*, id, title, shortcut="", category="")
def _handler(): ...

Dekorator. Registriert einen Befehl und bindet _handler (eine nullstellige Funktion, die None zurückgibt). Ausnahmen in _handler fängt der Worker und schreibt sie auf stderr; sie bringen den Editor nicht zum Absturz.

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 ist "Top/Sub/…/Beschriftung". Zwischen-Untermenüs entstehen bei Bedarf.

set_item_checked macht den Eintrag abhakbar und setzt sein Häkchen. Für dauerhafte Ein/Aus-Schalter. Erster Aufruf befördert; weitere kippen nur. Stiller No-op bei unbekannten Ids.

ctx.events

Sechs Themen, jedes mit Zucker-Dekorator und roher Form.

Zucker Rohes Thema Payload
@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()

Rohe Form:

handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id)  # to unbind

Erfinden Sie keine neuen Themen — die rohe Form validiert sie und wirft ValueError bei allem außerhalb der Tabelle.

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. Zeilen tragen im Diagnoseprotokoll des Hosts das Präfix [python:<plugin.id>].

ctx.status

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

level muss "info" / "warning" / "error" sein. timeout_ms=0 heißt, der Host wählt einen Standard.

ctx.editor (Phase 2)

Jede Methode ist synchron: Sie blockiert den aufrufenden Thread. In Ordnung aus einem def-Befehls-/Ereignis-Callback (der Worker verteilt Sync-Handler auf einen Thread-Pool); illegal aus async def-Handlern und aus ctx.completions-Anbietern (die direkt auf der Ereignisschleife laufen).

Dokumente werden über opake int-Ids referenziert (0 = kein Dokument).

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)

Typisierter Schlüssel/Wert-Speicher pro Plugin (ctx.settings) plus eine Nur-Lese-Sicht der appweiten Einstellungen (ctx.app_settings). Der Typ wird aus dem Python-Typ des Standardwerts abgeleitet.

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 werfen RuntimeError. Prüfen Sie in eigener Leselogik bool vor int (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)]

Felder von req: document, prefix, position, language_id, manual. Arten: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Zurückgeben: list[CompletionItem], einfache Zeichenketten oder Dicts. Der Callback MUSS in ~30 ms zurückkehren, sonst verwirft der Host Ihre Ergebnisse für diesen Tastendruck — kein I/O, kein ctx.editor.text(...), nur Cache. Indizes anderswo bauen (z. B. bei document.opened).

ctx.data_dir() / ctx.diagnostics_dir() (Phase 2)

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

data_dir() für Caches und Indizes. diagnostics_dir() ist nur lesbar (dort schreibt der Absturzberichter).

ctx.docks — deklarative Dock-Panels (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")]),
    ]),
)

Bauen Sie die Formular-Spezifikation mit den mte.ui-Helfern. Das Steuerelement-Vokabular ist fest: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List in Column / Row / Group.

  • binds="settings:<schlüssel>" — der Wert des Steuerelements persistiert automatisch über ctx.settings (derselbe Speicher wie ctx.settings.set).
  • on_click="<cmd.id>" — nur Button; ruft einen registrierten Befehl auf.
  • on_change=<handler> — über ctx.docks.on_change(callback) vergeben; feuert bei Nutzeränderungen.

on_change-Callbacks laufen auf der Ereignisschleife des Workers (NICHT im Thread-Pool). Halten Sie sie billig — keine Editor-Aufrufe, keine lange Arbeit. Kleinen In-Memory-Zustand ändern, loggen oder einen Befehl auslösen; schwere Arbeit im Befehl erledigen.

ctx.settings_page — deklarative Einstellungsseiten (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"),
    ]),
)

Dieselben Builder, dieselbe binds-Semantik. Zwei Steuerelemente mit demselben binds="settings:<schlüssel>" spiegeln sich in Echtzeit — das Dock-Panel und die Einstellungsseite können beide ein Kästchen für dieselbe Einstellung halten, und das Umschalten des einen kippt das andere.

Phase-3-Vorbehalt

Werte persistieren eifrig bei jeder Nutzeränderung — Abbrechen rollt nicht zurück, was der Nutzer geändert hat. Das wird in einer späteren Verfeinerung besser.

ctx.ui.get / ctx.ui.set — Laufzeitzugriff auf Panel-Steuerelemente (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 liefert den Primärwert als Zeichenkette:

  • Label / Button / LineEdit / ComboBox → Text
  • Checkbox → "true" / "false"
  • NumberSpin → Ganzzahl dezimal
  • List → Text des aktuell gewählten Eintrags (leer, wenn keiner)

Hostgetriebenes set ist still: Es feuert das on_change des Steuerelements NICHT erneut — eigene Widgets lassen sich also aus einem Befehlshandler ohne Endlosschleifen aktualisieren.


5. Vollständiges Beispiel — Plugin wordcount

Kopieren Sie es wörtlich und passen Sie id, name und die title an. Es übt jeden Dienst der 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")

Erwarteter Ablauf: Editor öffnen, eine Datei dreimal speichern, dann Plugins → Word Count → Report save count klicken. Die Statusleiste zeigt 3 save(s) this session. Das Protokoll hält jeden Schritt fest.


6. Häufige Aufgaben — Copy-Paste-Rezepte

Jedes Rezept ist ein Nur-Körper-Snippet: in def register(ctx): einfügen.

Einen Menübefehl hinzufügen

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

Auf Speichern reagieren

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

Beim Beenden warnen

@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
    ctx.log.warning("mine: last chance to persist state")

Auf einer bestimmten Stufe loggen

ctx.log.trace  ("very fine")
ctx.log.debug  ("dev")
ctx.log.info   ("routine")
ctx.log.warning("odd")
ctx.log.error  ("wrong")
ctx.menus.add_separator("Plugins/Mine")

Mehrere verwandte Befehle

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

Dauerhafter Ein/Aus-Schalter mit sichtbarem Häkchen

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)

Seitenpanel mit Texteingabe, Kästchen und zwei Buttons

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

Einstellungsseite, die die Einstellungen eines Docks spiegelt

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-Muster — tun Sie das NICHT

Falsch:

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

Richtig:

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

register in einer Klasse

Falsch:

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

Richtig:

def register(ctx):
    ...

async def register

Falsch:

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

Richtig:

def register(ctx):
    ...

Drittimporte ohne mitgeliefertes Wheel

Falsch:

import requests   # <-- ImportError on most user machines

Richtig (Phase 1): bei der Standardbibliothek bleiben. Brauchen Sie wirklich etwas Drittes, liefern Sie das Wheel im Plugin-Ordner mit und erweitern Sie sys.path in den ersten Zeilen von main.py; siehe Paketierung.

Qt-Importe

Falsch:

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

Richtig: nirgends Qt. Alle UI, die Sie beitragen können, läuft über ctx.menus, ctx.status und (ab Phase 3) deklarative Dock-/Einstellungspanels. Nie ein rohes Widget.

Blockieren in einem Handler

Falsch:

def _handler():
    time.sleep(30)   # <-- stalls the worker for 30s
    result = requests.get("https://slow.example")

Richtig (Phase 1): in einen Thread auslagern, über ctx.log kommunizieren:

def _handler():
    import threading
    def _work():
        time.sleep(30)
        ctx.log.info("done")
    threading.Thread(target=_work, daemon=True).start()
    ctx.status.show("started")

Falsch:

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

Richtig: das oberste Pfadsegment an menus[] ausrichten. Oben "Plugins/Mine/Do" verwenden.

ctx für später aufheben

Falsch:

_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?

Richtig: die benötigten Dienste in der Closure einfangen, die sie verwendet. register ist der Ort der Verdrahtung; alles andere ist ein Handler, der über die Closure bereits Zugriff hat.


8. Debugging-Checkliste für den Nutzer

Fügen Sie dies ans Ende Ihrer Antwort an, wenn Sie ein Plugin produzieren, damit der Nutzer weiß, was bei Problemen zu prüfen ist:

  1. Editor aus einer Shell starten, in der ein Python-3.9+-Interpreter im PATH liegt (oder MTE_PYTHON auf einen zeigt).
  2. Im Protokoll nach PluginHost: Python plugin backend enabled; interpreter=… suchen. Fehlt es, weiter zu Fehlerbehebung.
  3. Nach [python:<ihre.id>] register() ran suchen (dem wordcount-Beispiel folgend) — beweist, dass register lief.
  4. Fehlt der Menüpunkt, das Protokoll auf PythonPluginBackend: skipping '<id>': …-Erklärungen prüfen.
  5. Tut ein Menüklick nichts, das Protokoll auf [worker stderr] …-Zeilen prüfen — dort landen Python-Tracebacks.

9. Am Ende jeder Antwort zu wiederholende Auflagen

Wenn Sie ein Plugin produzieren, sagen Sie dem Nutzer immer:

  • Installation auf einem der beiden Wege:
    • den Ordnerinhalt als <name>.mteplugin zippen und Einstellungen → Plugins → Installieren… verwenden (validiert das Paket, bevor irgendwas angefasst wird), oder
    • den Ordner in das Benutzer-Plugin-Verzeichnis des Editors legen (%LOCALAPPDATA%/MTE/Plugins/ unter Windows; ~/Library/Application Support/MTE/Plugins/ unter macOS; ~/.local/share/MTE/Plugins/ unter Linux).
  • Editor neu starten.
  • MTE_PYTHON setzen, wenn das Protokoll „no Python interpreter found“ meldet.
  • Die Protokollzeile [python:<id>] … zurückmelden, damit der Lauf bestätigt werden kann.

10. Quelle der Wahrheit

Widerspricht irgendeine Aussage dieser Seite dem, was das Python-Client-Paket tatsächlich implementiert, gewinnt das Paket. Es liegt neben dem Editor unter <exe>/Python/mte/ — lesen Sie diese Quelle, bevor Sie ein Verhalten entscheiden. Die C++-Seite des Protokolls steht in Components/PluginHost/Src/PyServiceBridge.cpp, die Wire-Spezifikation in Python/PROTOCOL.md.