Zum Inhalt

ctx — der Plugin-Kontext

register(ctx) erhält ein Argument: einen PluginContext. In Phase 1 stellt er fünf Dienste bereit. Jeder ist eine kleine Python-Klasse, die der Worker über dem RPC-Kanal aufbaut; die Quelle liegt in Python/mte/.

def register(ctx):
    ctx.commands       # register/invoke commands
    ctx.menus          # add menu items & separators
    ctx.events         # subscribe to editor events
    ctx.log            # write to the host's diagnostics stream
    ctx.status         # post transient status-bar messages
    ctx.editor         # buffer / caret / selection access (Phase 2)
    ctx.settings       # per-plugin key/value store (Phase 2)
    ctx.app_settings   # editor-wide read-only settings view (Phase 2)
    ctx.completions    # register auto-completion providers (Phase 2)
    ctx.docks          # declarative dock panels (Phase 3)
    ctx.settings_page  # declarative Preferences pages (Phase 3)
    ctx.ui             # runtime get/set for panel controls (Phase 3)

Speichern Sie ctx nicht in einer Globalen, um es später zu verwenden — die Attribute sind zwar über die Lebenszeit des Plugins stabil, behandeln Sie ctx aber als pro-Registrierung. Halten Sie bei Bedarf Referenzen auf die einzelnen Dienste (z. B. log = ctx.log).


commands

ctx.commands.command(*, id, title, shortcut="", category="", scope="")

Dekorator, der einen Befehl registriert und die umschlossene Funktion als seinen Handler bindet.

  • id (str, erforderlich): eindeutiger Bezeichner im Reverse-DNS-Stil (z. B. "wordcount.count"). Dieselben Regeln wie in plugin.json — muss global eindeutig sein.
  • title (str, erforderlich): menschenlesbare Beschriftung, gezeigt wo immer der Befehl auftaucht (Menüs, Befehlspalette).
  • shortcut (str, optional): portable Standard-Kürzelzeichenkette, z. B. "Ctrl+Alt+W". Ctrl wird automatisch auf den primären Modifikator der Plattform abgebildet (Cmd auf macOS). Leer = keins. Der Nutzer kann es unter Einstellungen ▸ Tastenkürzel umbelegen oder lösen; bei menügebundenen Befehlen wendet der Host die Umbelegung für Sie an.
  • category (str, optional): freies Gruppierungsetikett — als Kategorie des Befehls im Tastenkürzel-Zuordner gezeigt.
  • scope (str, optional): ""/"application" (Standard — der Host bindet das Kürzel an die Menüaktion des Befehls) oder "pluginWindow" für Kürzel in plugin-eigener UI: Der Host listet den Befehl nur im Zuordner; lesen Sie beim Aufbau Ihrer Widgets ctx.commands.effective_shortcut(id) und wenden Sie es beim Ereignis "command.shortcutChanged" neu an.
ctx.commands.effective_shortcut(command_id) -> str

Das nach Nutzer-Umbelegung und Konfliktauflösung aktuell wirksame Kürzel ("" = ungebunden oder unbekannt). Aus Handlern sicher aufrufbar.

Die umschlossene Funktion nimmt keine Argumente und gibt nichts zurück. Ausnahmen darin fängt der Worker und protokolliert sie auf stderr; sie bringen den Editor nicht zum Absturz.

@ctx.commands.command(id="wordcount.count",
                      title="Count Words",
                      shortcut="Ctrl+Alt+W",
                      category="Text")
def _count():
    ctx.status.show("counted")

Unter der Haube: Die Python-Seite vergibt lokal eine handlerId, registriert den Callback auf der RPC-Verbindung und sendet commands.registerCommand an den Host. Aktiviert der Nutzer den Befehl (Menüklick, Kürzel, Palette), schickt der Host einen callback-RPC und Ihre Funktion läuft.

Befehls-Ids sind prozessweit, nicht pro Plugin. Präfixen Sie Ihre Ids mit Ihrem Reverse-DNS-Namensraum (wordcount.…), um Kollisionen zu vermeiden.


Menüpfade nutzen / als Trenner. Das linkeste Segment ist ein Menü der obersten Ebene (Plugins, Tools, …); Zwischensegmente erzeugen Untermenüs bei Bedarf; das letzte Segment ist die Beschriftung des Eintrags.

ctx.menus.add_item(path, command_id)

ctx.menus.add_item("Plugins/Word Count/Count", "wordcount.count")
  • path (str): "Top/Sub/…/Beschriftung". Zwischen-Untermenüs werden angelegt, wenn sie fehlen.
  • command_id (str): Id eines zuvor registrierten Befehls. Ein Klick auf den Eintrag ruft den Befehl auf.

Gibt eine opake handlerId-Zeichenkette zurück. Behalten Sie sie, falls Sie den Eintrag später unsubscribe müssen (in Phase 1 selten).

Das linkeste Segment ("Plugins" oben) muss dem title eines Ihrer menus[]-Einträge in plugin.json entsprechen, damit die Platzierung in der Menüleiste übernommen wird.

ctx.menus.add_separator(path)

ctx.menus.add_separator("Plugins/Word Count")

Fügt einen Trenner in ein bestehendes Untermenü ein.

ctx.menus.set_item_checked(handler_id, checked)

Macht den zuvor hinzugefügten Eintrag zu einem abhakbaren Menüpunkt und setzt seinen Zustand — das Standard-Idiom für einen dauerhaften Ein/Aus-Schalter mit sichtbarem Häkchen.

item = ctx.menus.add_item("View/Auto-open Preview",
                          "myplugin.toggleAutoOpen")

# Seed the check mark from the persisted setting so it matches
# reality before the user opens the menu.
ctx.menus.set_item_checked(item, ctx.settings.get("autoOpen", True))

@ctx.commands.command(id="myplugin.toggleAutoOpen",
                      title="Toggle Auto-Open")
def _toggle():
    on = not ctx.settings.get("autoOpen", True)
    ctx.settings.set("autoOpen", on)
    ctx.menus.set_item_checked(item, on)

Der erste Aufruf macht den Eintrag abhakbar; weitere kippen nur das Häkchen. Eine dem Host unbekannte handler_id ist ein stiller No-op (sicher aufrufbar, nachdem Ihr Token verworfen wurde, z. B. aus einem verzögerten Callback während des Herunterfahrens).


events

Editor-Ereignisse abonnieren. Zwei gleichwertige APIs — die Zucker-Dekoratoren sind für den festen Themensatz der Phase 1 empfohlen; das rohe subscribe() steht bereit, wenn Sie das Thema zur Laufzeit berechnen wollen.

Zucker-Dekoratoren

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

@ctx.events.on_document_opened
def _opened(ev):
    ctx.log.info(f"opened {ev.path}")

Die umschlossene Funktion nimmt ein Argument: eine typisierte Dataclass passend zum Thema. Die vollständige Tabelle Thema ↔ Dataclass: Ereignis-Referenz.

Rohes API

handler_id = ctx.events.subscribe("document.saved", _saved)
# later:
ctx.events.unsubscribe(handler_id)

Verfügbare Zucker-Eigenschaften (alle Dekoratoren):

Eigenschaft Feuert bei
on_document_opened Dokument in einem Tab geöffnet
on_document_closed Dokument geschlossen
on_document_saved Dokument auf die Festplatte geschrieben
on_selection_changed Cursor / Auswahl im aktiven Dokument bewegt
on_active_document_changed Vordergrund-Tab gewechselt
on_app_about_to_quit Der Editor schließt — letzte Gelegenheit
on_command_shortcut_changed Das wirksame Kürzel eines Befehls hat sich geändert (Nutzer-Umbelegung / Keymap-Import / Konfliktauflösung); Payload: command_id, sequence ("" = ungebunden)

Unbekannte Themennamen werfen ValueError beim Abonnieren. Abonnenten der Phase 1 können nach dem entsprechenden nativen Handler laufen; behandeln Sie die Ereigniszustellung als informativ, nicht autoritativ.


log

ctx.log.trace  ("very-fine-grained")
ctx.log.debug  ("dev-only detail")
ctx.log.info   ("routine event")
ctx.log.warning("recoverable weirdness")
ctx.log.error  ("something is wrong")

Jede Stufe bildet auf den ILogger des Hosts ab. Zeilen tragen das Präfix [python:<plugin.id>], Ihre Ausgabe ist im Diagnoseprotokoll des Editors also auffindbar:

[python:org.example.wordcount] counted 42 words

Niemals print() aus einem Plugin — der stdout des Workers ist der RPC-Kanal, und Schreiben dorthin korrumpiert das Protokoll. ctx.log.* ist der einzige sanktionierte Weg, Diagnosetext auszugeben.


status

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

Sendet eine flüchtige Benachrichtigung an die Statusleiste.

  • message (str): was gezeigt wird. Nur kurze Zeichenketten — die Statusleiste hat feste Breite.
  • timeout_ms (int, Standard 0): wie lange sie sichtbar bleibt; 0 lässt den Host einen sinnvollen Standard wählen (einige Sekunden).
  • level (str, Standard "info"): "info" / "warning" / "error". Höhere Stufen halten das Bildschirm-Minimum etwas länger, damit ein Fehler nicht sofort von einer Routinemeldung weggewischt wird.
ctx.status.show("saved", timeout_ms=1500)
ctx.status.show("no results", level="warning")

ctx.status.show ist fire-and-forget. Hat der Host keine sichtbare Statusleiste (headless / Minimal-Build), wird der Aufruf still verworfen.



editor

Voller synchroner Zugriff auf die aktiven Dokumente. Jede Methode blockiert den aufrufenden Thread bis zur Antwort des Hosts (typischerweise Mikrosekunden, wenn Worker und Editor eine Maschine teilen). Legal aus jedem Befehls-/Ereignishandler; nie aus einem async def-Callback (siehe die Threading-Notiz am Seitenanfang).

Dokument-Enumeration

doc  = ctx.editor.active_document()      # 0 when no document is open
docs = ctx.editor.open_documents()       # list[int]
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc)         # str or None for untitled

Pufferzugriff

text  = ctx.editor.text(doc)                        # whole buffer
slice = ctx.editor.text_range(doc, start, end)      # [start, end) bytes
n     = ctx.editor.length(doc)                      # int64
ctx.editor.set_text(doc, "...")

text_range vermeidet es, den ganzen Puffer über den RPC zu ziehen, wenn Sie nur einen Ausschnitt brauchen — bevorzugen Sie es für Sortier-/Transformationsläufe auf großen Dateien.

Auswahl

sel = ctx.editor.selection_range(doc)               # mte.MatchRange(start, end)
s   = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "new text")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))

Cursor

pos = ctx.editor.caret(doc)                         # mte.EditorPosition(line, column)
ctx.editor.set_caret(doc, mte.EditorPosition(line=7, column=3))

Dateioperationen

new_doc = ctx.editor.open_file("/path/to/file")     # doc-id or 0 on failure
ok      = ctx.editor.save_document(doc)
dirty   = ctx.editor.is_modified(doc)

Suche

opts = mte.SearchOptions(match_case=True, mode="regex")
hit  = ctx.editor.find_in_range(doc, r"TODO\(.*\)", 0,
                                ctx.editor.length(doc), opts)
if hit.valid:
    new_end = ctx.editor.replace_target(doc, hit, "DONE()", opts)

SearchOptions.mode: "normal" / "extended" / "regex". SearchOptions.direction: "forward" / "backward". Die vollständige Feldliste: mte.SearchOptions.

Einrückung und Syntax

style = ctx.editor.indentation(doc)                 # IndentationStyle(use_tabs, width)
ctx.editor.set_indentation(doc,
    mte.IndentationStyle(use_tabs=False, width=2))

lang  = ctx.editor.colorizer_id(doc)                # e.g. "python", "" if none

settings

Persistenter typisierter Schlüssel/Wert-Speicher, hostseitig auf plugins/<ihre.id>/ begrenzt. Alle Methoden synchron; der get/set-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)             # returns bool
count = ctx.settings.get("count", 0)                # returns int
theme = ctx.settings.get("theme", "light")          # returns str

Typen:

Python-Typ von default Wire-Typ Rückgabetyp von get()
bool bool bool
int (nicht-bool) int int
float double float
str (oder None) string str

Außerdem:

ctx.settings.contains("wrap")           # bool
ctx.settings.remove("stale_key")
ctx.settings.sync()                     # optional; host also syncs on shutdown

Auf Änderungen aus dem Einstellungsdialog reagieren Sie per SettingsChanged-Abo (siehe Ereignis-Referenz; die volle Ereignisfläche kommt mit Phase 3).

app_settings

Nur-Lese-Sicht auf die editorweite Konfiguration. Die Schlüsselnamen leben im C++-Header PluginApi/AppSettingsKeys.h; die autoritative Liste steht dort.

tab_width = ctx.app_settings.get("editor.tabWidth", 4)
theme     = ctx.app_settings.get("editor.theme.variant", "light")

Setter, remove() und sync() auf ctx.app_settings werfen RuntimeError — die Sicht ist absichtlich schreibgeschützt.


completions

Registriert einen Vervollständigungsanbieter, der am Popup des Editors teilnimmt:

@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
    if not req.prefix:
        return []
    return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
            for w in ("hello", "hello_world", "helper")
            if w.startswith(req.prefix)]

Die umschlossene Funktion wird bei jedem Tastendruck (oder Ctrl+Space) aufgerufen, dessen Dokument zum Sprachfilter passt. Felder von req:

Feld Typ Bedeutung
document int Bearbeitetes Dokument
prefix str Wortfragment unmittelbar vor dem Cursor
position int Byte-Offset des Cursors
language_id str Colorizer-Id ("python", "cpp", …)
manual bool True, wenn der Nutzer Ctrl+Space drückte; False bei Auto-Auslösung

Geben Sie eine Liste von mte.CompletionItem zurück (oder einfache Zeichenketten oder Dicts mit {text, detail, kind}). Arten: mte.KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.

Deadline

Der Host sendet Ihrem Anbieter eine Tastendruck-Anfrage mit einer ~30-ms-Deadline. Verpasst Ihr Callback sie, erscheint das Popup für diesen Tastendruck ohne Ihre Kandidaten. Bauen Sie teure Indizes im Hintergrund und antworten Sie aus dem Cache — nie I/O im Callback. LSP-artige Netzwerk-Vervollständigung gehört in eine künftige Async-Fläche, nicht hierher.

languages=[] (oder weggelassen) heißt „jedes Dokument“. Mehrere Anbieter pro Plugin sind erlaubt; jeder Dekorator registriert seinen eigenen.


data_dir

path = ctx.data_dir()                    # pathlib.Path

Gibt das beschreibbare Pro-Plugin-Verzeichnis zurück, das der Host bereits für Sie angelegt hat. Für Caches, Indizes und alle Daten, die Ihnen gehören. ctx.diagnostics_dir() liefert analog das gemeinsame Absturz-/Protokollverzeichnis (für Sie nur lesbar — dort schreibt der Absturzberichter).


docks

Deklarative Dock-Panels — eine Seitenpanel-UI aus einer Formular-Spezifikation, nicht aus Qt-Widgets. Das Builder-Vokabular: mte.ui.

from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button

def register(ctx):
    def _on_needle_changed(value: str) -> None:
        ctx.log.info(f"needle -> {value!r}")
    needle_handler = ctx.docks.on_change(_on_needle_changed)

    panel_id = ctx.docks.add_panel(
        id="renamer.panel",
        title="Renamer",
        area="right",             # left / right / top / bottom
        form=Column([
            Label(text="Search:"),
            LineEdit(id="needle", placeholder="text...",
                     binds="settings:lastNeedle",
                     on_change=needle_handler),
            Checkbox(id="cs", label="Case sensitive",
                     binds="settings:caseSensitive"),
            Row([Button(text="Run", on_click="renamer.run")]),
        ]),
    )

ctx.docks.add_panel(*, id=None, title="", area="right", form, initially_visible=True) -> str

Registriert ein Dock-Panel. Gibt die Panel-Id zurück (automatisch vergeben, wenn id None ist).

  • id (str, optional) — stabiler Bezeichner, mit dem ctx.ui.get / ctx.ui.set Steuerelemente in diesem Panel adressiert. Ohne Angabe vergibt der Host eine py-dock/N-Id und gibt sie zurück.
  • title (str) — auf dem Dock-Kopf gezeigt.
  • area (str)"left" / "right" / "top" / "bottom". Unbekannte Werte fallen auf "right" zurück.
  • form (dict) — eine mit den mte.ui-Buildern gebaute Spezifikation.
  • initially_visible (bool) — ob das Dock beim Laden des Plugins angezeigt wird. Standard True. Der Nutzer kann es danach verbergen; Sie können es nicht wieder erzwingen.

ctx.docks.on_change(callback) -> str

Vergibt eine Handler-Id, um das on_change-Feld eines Steuerelements mit einem Python-Callable zu verdrahten. Der Callback erhält den neuen Wert als Zeichenkette; boolesche Steuerelemente liefern "true" / "false", numerische die Ganzzahl als Zeichenkette.

def _on_volume(value: str) -> None:
    ctx.log.info(f"volume={int(value)}")

handler = ctx.docks.on_change(_on_volume)
form = Column([NumberSpin(id="volume", default=5, on_change=handler)])

on_change-Handler-Ids tragen das Präfix py-dock-cb/N und kollidieren nie mit Befehls-/Ereignis-/Vervollständigungs-Handler-Ids.

Der Callback läuft auf der Ereignisschleife des Workers, NICHT im Thread-Pool — halten Sie ihn schnell. Nutzen Sie ctx.log, ändern Sie kleinen In-Memory-Zustand oder feuern Sie einen Folgebefehl; keine schwere Arbeit hier.


settings_page

Fügt eine Seite im Einstellungen-Dialog des Editors hinzu. Dasselbe Formular-Vokabular wie bei Dock-Panels; dieselbe binds="settings:<schlüssel>"-Persistenz.

from mte.ui import Column, Checkbox, NumberSpin

def register(ctx):
    ctx.settings_page.register(
        category="Plugins/Renamer",       # slash-separated tree path
        title="Renamer",
        form=Column([
            Checkbox(id="cs", label="Case sensitive by default",
                     binds="settings:caseSensitive"),
            NumberSpin(id="results", default=100, min=1, max=10_000,
                       binds="settings:resultCap"),
        ]),
    )

ctx.settings_page.register(*, category, title, form, scope="") -> str

  • category (str, erforderlich) — Slash-getrennter Pfad im Einstellungsbaum, z. B. "Plugins/My Plugin". Verschachtelte Kategorien werden Baumknoten.
  • title (str, erforderlich) — Überschrift über der Seite.
  • form (dict, erforderlich) — eine mit den mte.ui-Buildern gebaute Spezifikation.
  • scope (str, optional) — Ereignis-Scope, der bei SettingsChanged gesendet wird, nachdem der Nutzer OK / Übernehmen drückt.

Gibt die Handler-Id zurück.

Abbrechen macht Änderungen NICHT rückgängig

Steuerelemente mit binds="settings:<schlüssel>" persistieren eifrig, während der Nutzer editiert — Abbrechen im Einstellungsdialog nimmt sie nicht zurück. Das ist eine bekannte Einschränkung der Phase 3; eine spätere Verfeinerung wird die zugrunde liegenden Einstellungen in einen puffernden Proxy hüllen.


ui

Liest und schreibt Steuerelementwerte in bereits registrierten Panels. Werte sind auf dem Draht immer Zeichenketten.

def register(ctx):
    panel_id = ctx.docks.add_panel(id="renamer.panel", ...)

    @ctx.commands.command(id="renamer.echo", title="Echo needle")
    def _echo():
        needle = ctx.ui.get(panel_id, "needle") or ""
        cs = (ctx.ui.get(panel_id, "cs") or "false") == "true"
        ctx.status.show(f"needle={needle!r} case={cs}")

    @ctx.commands.command(id="renamer.clear", title="Clear needle")
    def _clear():
        ctx.ui.set(panel_id, "needle", "")

ctx.ui.get(panel_id, control_id) -> Optional[str]

Liest den aktuellen Wert eines Steuerelements. Gibt None zurück, wenn Panel oder Steuerelement unbekannt sind (kein Fehler — behandeln Sie None und "" gleichermaßen als „leer“).

  • Label / Button / LineEdit / ComboBox — liefert den Text.
  • Checkbox — liefert "true" oder "false".
  • NumberSpin — liefert die Ganzzahl als Dezimal-Zeichenkette.
  • List — liefert den Text des aktuell gewählten Eintrags (leer, wenn nichts gewählt ist).

ctx.ui.set(panel_id, control_id, value) -> bool

Schreibt programmatisch einen Wert in ein Steuerelement. Gibt True zurück, wenn das Schreiben gelang, False, wenn Panel/Steuerelement unbekannt sind oder der Wert nicht wandelbar ist (z. B. eine Nicht-Ganzzahl an eine NumberSpin).

Python-True / False werden vor dem Senden zu "true" / "false"; alles andere geht durch str().

Ein hostgetriebenes set feuert den on_change-Callback des Steuerelements NICHT erneut — Sie können Ihre eigenen Widgets aus einem Befehlshandler aktualisieren, ohne Endlosschleifen.


Zusammenfassung der Laufzeit-Bindungen (Phase 3)

Formular-Feld Laufzeitverhalten
id="…" Steuerelement über ctx.ui.get / ctx.ui.set adressieren.
binds="settings:<schlüssel>" Liest den Anfangswert aus ctx.settings; schreibt bei jeder Nutzeränderung zurück.
on_click="<cmd>" Buttons rufen beim Klick die registrierte Befehls-Id auf.
on_change=handler Feuert den über ctx.docks.on_change(...) registrierten Callback bei Nutzeränderung.

Dieselben Regeln gelten für Dock-Panels und Einstellungsseiten.