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 → Erweiterungen → Plugin 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.
  • "requires": [] — IDs ANDERER Plugins, ohne die dieses nicht funktioniert (keine pip-Pakete). Sie starten zuerst; fehlt eines oder ist es aus, startet dieses Plugin nicht, und die Plugins-Seite sagt, warum. Leer lassen, sofern die Aufgabe des Nutzers nicht wirklich von einem anderen Plugin abhängt.
  • "apiMinorVersion": 0 — die niedrigste Minor-Version der Editor-API, die das Plugin braucht. Bei 0 lassen, außer Sie verwenden einen Aufruf, der als neuer dokumentiert ist.

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 achtzehn Dienste: commands, menus, events, log, status, editor, languages, settings, app_settings, document_settings, theme, diagnostics, compare, completions, docks, settings_page, ui und data_dir()/diagnostics_dir(). 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="",
                      requires_editor=False, requires_writable_editor=False)
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.

Setzen Sie requires_editor, wenn der Befehl das aktive Dokument liest (Puffer, Auswahl, Cursor), und requires_writable_editor, wenn er den Text ändert. Der Host graut den Befehl dann dort aus, wo er nicht funktionieren könnte — auf einem Tab ohne Dokument (Vergleichs- oder Log-Ansicht) und, bei der Writable-Flagge, auch auf einem schreibgeschützten Dokument — statt den Benutzer einen stillen Leerlauf auslösen zu lassen. Die Writable-Flagge impliziert die andere; für Befehle, die nur einen Dialog oder ein Panel öffnen, lassen Sie beide aus.

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

set_item_enabled graut den Eintrag aus. Für Commands mit einer Voraussetzung, damit ein nicht verfügbarer Eintrag auch so aussieht, statt beim Klick nichts zu tun. Idempotent; Einträge starten aktiviert; stiller No-op bei unbekannten Ids.

ctx.events

Siebzehn 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_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) — jede Änderung, trivial halten
@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() — einmal pro Lauf, nach dem ersten Frame
@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) — ein Klick in die Lesezeichenspalte; Bits von modifiers: mte.MODIFIER_*

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")
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 muss "info" / "warning" / "error" sein. timeout_ms=0 heißt, der Host wählt einen Standard. show() ist für flüchtige Meldungen; alles, was auf dem Schirm bleibt (ein Live-Zähler, ein Modus-Flag), ist ein Segment — die beiden überschreiben sich nie.

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

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:<key>" — 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.

Übergeben Sie initially_visible=False für ein Panel, das beim Start nicht aufspringen darf, und zeigen Sie es aus seinem Befehl mit ctx.docks.set_panel_visible(panel_id, True) (hebt es auch nach vorn; idempotent; unbekannte Ids ignoriert). ctx.docks.is_panel_visible(panel_id) sagt, ob ein Panel geöffnet ist (ein hinterer Tab zählt). Mehrere Panels, die zusammengehören: Übergeben Sie dasselbe tab_group="<plugin.id>/panels" und dieselbe area, dann teilen sie sich ein Dock mit Tabs.

ctx.line_markers — Markierungen an Dokumentzeilen

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

Arten einmal in register() registrieren; der Editor hat nur wenige Slots. Den Randklick nur beanspruchen, wenn das Plugin ihm eine Bedeutung gibt — Klicks kommen so oder so über @ctx.events.on_margin_clicked an.

ctx.languages — Syntaxfärbung

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 muss einen vorhandenen Backend-Lexer nennen; styles und keywords verwenden die Namen des Lexers (dieselbe Form wie die mitgelieferten colorizers/*.json), nie numerische Ids.

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 — Wellenlinien

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

Senden Sie jedes Mal die VOLLSTÄNDIGE Menge für Ihre source; sie ersetzt die vorige. source ist Pflicht.

ctx.theme — theme-fähige Farben für die eigene UI

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 — Vergleich nebeneinander

ctx.compare.open(mte.CompareSource(title, path=..., content=...),
                 mte.CompareSource(...), ignore_whitespace=False,
                 ignore_case=False, ignore_eol=False)   # -> bool

ctx.events.publish

Ein Plugin darf nur workspace.rootChanged / workspace.filesChanged veröffentlichen; alles andere wirft ValueError. Abonnieren lässt sich jedes Host-Event außer document.aboutToSave (siehe Decorator-Liste); das native Event DocumentAboutToClose hat überhaupt kein Python-Topic — verwenden Sie document.closed.

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:<key>" 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, ctx.diagnostics, ctx.compare und die deklarativen 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:<your.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 → Erweiterungen → Plugin 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.