Zum Inhalt

ctx — der Plugin-Kontext

register(ctx) erhält ein Argument: einen PluginContext. Er stellt die folgenden Dienste bereit (die gesamte IPluginContext-Oberfläche, die in das Out-of-Process-Modell passt). 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 / files / search / bookmarks / groups
    ctx.languages      # list languages, read / switch a document's language, contribute one
    ctx.settings       # per-plugin key/value store
    ctx.app_settings   # editor-wide read-only settings view (keys: mte.keys)
    ctx.document_settings  # .editorconfig-layered per-document reads
    ctx.theme          # themeable colour categories for your own UI
    ctx.diagnostics    # publish squiggles + margin glyphs
    ctx.compare        # open the side-by-side compare view
    ctx.completions    # register auto-completion providers
    ctx.docks          # declarative dock panels (+ set_panel_visible, is_panel_visible)
    ctx.line_markers   # margin glyphs / line tints that follow edits; the margin-click claim
    ctx.settings_page  # declarative Preferences pages
    ctx.ui             # runtime get/set for panel controls

Zwei Teile des nativen Vertrags haben noch kein Python-Gegenstück: Hover-Provider (die Hover-Prioritätsbänder gelten daher nicht für Python-Plugins) und Provider für das Editor-Kontextmenü (daher auch nicht die Unterscheidung Text / Rand im Kontextmenü). Beide werden beantwortet, während der Benutzer wartet; das braucht dieselbe Deadline-Behandlung wie bei Completion-Providern, bevor ein Umweg über den Worker sicher ist.

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

ctx.menus.set_item_enabled(handler_id, enabled)

Aktiviert einen zuvor hinzugefügten Eintrag oder graut ihn aus. Nutzen Sie das, wenn das Command eine Voraussetzung braucht — etwas zu Schließendes, etwas Wiederherstellbares — damit das Menü die Nichtverfügbarkeit zeigt, statt einen Eintrag anzubieten, der still nichts tut.

item = ctx.menus.add_item("File/Close Thing", "myplugin.closeThing")

# Nothing to close yet:
ctx.menus.set_item_enabled(item, False)

# ... and from every place that changes the state the command needs:
ctx.menus.set_item_enabled(item, thing_is_open)

Ein deaktivierter Eintrag bleibt sichtbar, lässt sich aber nicht auslösen — weder per Klick noch über sein Tastenkürzel. Der Aufruf ist idempotent, Sie müssen sich den vorherigen Zustand also nie merken, und eine unbekannte handler_id ist ein stiller No-op wie oben. Einträge starten aktiviert: ein Aufruf direkt nach add_item setzt einen beim Start nicht verfügbaren Eintrag.


events

Editor-Ereignisse abonnieren. Zwei gleichwertige APIs — die Zucker-Dekoratoren sind 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_document_modified_changed Das Flag sauber/geändert kippte (modified: bool)
on_text_changed Jede Änderung (position, added_bytes, removed_bytes) — trivial halten
on_settings_changed Einstellungen übernommen (scope: "" editorweit, "plugins/<id>")
on_theme_changed Theme gewechselt oder bearbeitet (variant) — ctx.theme-Farben neu lesen
on_workspace_root_changed Workspace-Ordner geöffnet / geschlossen (root)
on_workspace_files_changed Dateimenge des Workspace geändert (files)
on_host_window_shown Einmal pro Lauf, nach dem ersten Frame — langsame Startarbeit hier einplanen
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 können nach dem entsprechenden nativen Handler laufen; behandeln Sie die Ereigniszustellung als informativ, nicht autoritativ.

Veröffentlichen

ctx.events.publish(topic, payload) sendet an native und Python-Abonnenten gleichermaßen — akzeptiert nur für die zwei Workspace-Topics (workspace.rootChanged, workspace.filesChanged); alles andere wirft ValueError. Siehe die Ereignis-Referenz.


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. ctx.status.clear() löscht die gerade angezeigte flüchtige Meldung.

Persistente Segmente

Für Live-Zustand (eine Wortzahl, ein Serverstatus) nutzen Sie ein Segment, das Sie besitzen und auffrischen — es überschreibt nie eine Meldung und umgekehrt:

seg = ctx.status.add_segment("wordcount.total", priority=100)
seg.set_text("2,431 words")
seg.set_visible(False)      # hide without giving it up
seg.remove()                # take it off the bar (idempotent)

priority ordnet Plugin-Segmente von links nach rechts (niedriger = weiter links; Gleichstand behält die Registrierungsreihenfolge). Das Herunterfahren des Plugins entfernt alles noch Gehaltene.


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

ranges = ctx.editor.selection_ranges(doc)           # list[MatchRange]: one per caret
block  = ctx.editor.is_rectangular_selection(doc)   # Alt+drag column block?

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
here    = ctx.editor.open_file_at("/path/to/file", mte.EditorPosition(41, 0))
ctx.editor.reveal_line(doc, 120)                    # scroll there; caret and focus stay
ok      = ctx.editor.save_document(doc)
closed  = ctx.editor.close_document(doc)            # prompts if modified; False = user cancelled
dirty   = ctx.editor.is_modified(doc)
ctx.editor.set_modified(doc, True)                  # force the tab asterisk
ro      = ctx.editor.is_read_only(doc)              # e.g. Monitoring (tail -f) mode
enc     = ctx.editor.document_encoding(doc)         # mte.DocumentEncoding, e.g. UTF8_BOM
ctx.editor.set_document_encoding(doc, mte.DocumentEncoding.UTF8)  # takes effect on the next save
eol     = ctx.editor.line_ending(doc)               # mte.LineEnding, e.g. CRLF
ctx.editor.set_line_ending(doc, mte.LineEnding.LF)  # Enter inserts it; the next save writes it
scratch = ctx.editor.create_untitled("text", "Untitled-3")   # new buffer, doc-id
sid     = ctx.editor.stable_id(doc)                 # survives Save As; compare with ==
clip    = ctx.editor.clipboard_text()               # system clipboard, "" if no text

# Declare that you snapshot unsaved buffers (the host then stops asking
# "save changes?" when the window closes):
ctx.editor.set_unsaved_changes_preserved(True)
ctx.editor.unsaved_changes_preserved()              # bool, global flag

open_file_at ist der eine Aufruf für „bring den Benutzer dorthin“ (ein Build-Fehler, ein Stack-Frame, ein Suchtreffer): Er öffnet die Datei oder aktiviert ihren Tab, setzt die Einfügemarke ohne Auswahl, klappt eine Faltung auf, die die Zeile verbirgt, scrollt die Zeile in den sichtbaren Bereich — zentriert, wenn sie außerhalb des Bildschirms lag — und fokussiert den Editor. Zeile und Spalte sind 0-basiert (die Spalte in UTF-8-Bytes) und werden begrenzt, nie abgelehnt; er liefert die Dokument-Id oder 0, wenn die Datei nicht geöffnet werden konnte, und dann hat sich nichts bewegt. reveal_line zeigt nur eine Zeile: Es bewegt die Einfügemarke nicht, wechselt keine Tabs und nimmt keinen Fokus.

Lesezeichen, Markierungen, URL-Hervorhebung

ctx.editor.toggle_bookmark(doc, line)               # 0-based line
lines = ctx.editor.bookmarked_lines(doc)            # list[int]
nxt   = ctx.editor.next_bookmark(doc, from_line, wrap=True)      # -1 = none
prv   = ctx.editor.previous_bookmark(doc, from_line, wrap=True)
ctx.editor.clear_bookmarks(doc)

ctx.editor.mark_range(doc, mte.MatchRange(4, 9))    # "mark" indicator
ctx.editor.clear_marks(doc)

ctx.editor.set_url_highlight_enabled(doc, True)
ctx.editor.is_url_highlight_enabled(doc)
url = ctx.editor.url_at_caret(doc)                  # "" when none
ctx.editor.open_url_at_caret(doc)                   # bool

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.

Inkrementelle Suche (das Muster wächst beim Tippen, ab einem gesetzten Anker):

ctx.editor.set_incremental_search_anchor(doc)
hit = ctx.editor.incremental_search(doc, "TOD", opts)   # MatchRange

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

# Where word-wrapped continuation rows start:
# "left" (window edge) / "same" (align with the line; default) / "indent"
ctx.editor.set_wrap_indent(doc, "same")
mode = ctx.editor.wrap_indent(doc)                  # same strings back

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

# Semantic classification from the active colorizer (cheap, no re-lex):
# list[mte.CategorySpan(start, length, category)] with category names
# "comment" / "string" / "keyword" / ... -- a spell checker keeps to
# comments and strings this way.
spans = ctx.editor.text_categories(doc, 0, ctx.editor.length(doc))

ctx.editor.trigger_completion(doc)                  # pop the list as if Ctrl+Space

Split-View / Tab-Gruppen

n  = ctx.editor.group_count()                       # >= 1
g  = ctx.editor.group_of(doc)                       # -1 if not open
w  = ctx.editor.group_weights()                     # relative widths
ctx.editor.move_document_to_group(doc, 1)           # creates trailing groups on demand
ctx.editor.set_group_weights([2, 1])
ctx.editor.set_document_tab_index(doc, 0)           # reorder within its pane

gs = ctx.editor.groups_of(doc)                      # EVERY pane showing it
ctx.editor.show_document_in_group(doc, 1)           # split it into pane 1

Ein in zwei Bereichen gezeigtes Dokument ist ein Dokument mit einer id, sodass group_of() nur einen seiner Plätze nennen kann. groups_of() liefert alle, und show_document_in_group() legt eine zweite, geteilte Ansicht an — genau das, was Ansicht ▸ Dokument rechts teilen tut, sodass kein document_opened-Ereignis ausgelöst wird.

ctx.editor spiegelt das native IEditorService jetzt eins zu eins.


languages

Syntaxfärbung ist ein Kern-Feature — der Host erkennt die Sprache eines Dokuments und färbt es ohne jedes Plugin. ctx.languages lässt Sie mitmachen: auflisten, was der Host kennt, die Sprache eines Dokuments lesen oder überschreiben, die Erkennung neu anstoßen und ganze Sprachdefinitionen beisteuern.

for lang in ctx.languages.available():              # list[mte.LanguageInfo(id, name)]
    ...
lang_id = ctx.languages.language_for(doc)           # "" when uncoloured
ok      = ctx.languages.set_language_for(doc, "json")   # "" clears; False = unknown
ctx.languages.auto_detect_for(doc)                  # undo a manual override

handler = ctx.languages.register(
    id="mylang", name="My Language", lexer="cpp",
    extensions=["mylang", "ml2"],
    keywords=[{"group": "Primary keywords and identifiers",
               "words": "if else while"}],
    styles={"SCE_C_COMMENT": "comment", "SCE_C_STRING": "string"})
...
ctx.languages.unregister(handler)                   # restores what it displaced

register nimmt dieselbe Form wie eine mitgelieferte colorizers/*.json (oder ein ganzes definition=-Dict): lexer nennt einen vorhandenen Backend-Lexer (der Host gewinnt zur Laufzeit keine neuen), styles bildet die Stil-Namen dieses Lexers auf semantische Kategorien ab, und keywords adressiert seine Wortlisten über die Slot-Beschreibung des Lexers — der Host löst beides genau wie bei einer mitgelieferten Datei auf, Engine-Ids schreiben Sie nie. Das Registrieren einer vorhandenen Id ersetzt sie, solange Sie die Handler-Id halten; das Herunterfahren des Plugins zieht alles noch Gehaltene zurück.


document_settings

Schreibgeschützte, dokumentbezogene Einstellungen, die die .editorconfig eines Projekts über Ihre ctx.settings legen. Für Schlüssel k und Dokument doc löst ein Lesezugriff mte.<plugin.id>.<k> aus der für dieses Dokument geltenden .editorconfig auf, dann ctx.settings, dann Ihren Standard:

wrap  = ctx.document_settings.get_bool(doc, "wrap", False)
width = ctx.document_settings.get_int(doc, "width", 80)
ratio = ctx.document_settings.get_double(doc, "ratio", 1.0)
name  = ctx.document_settings.get_string(doc, "profile", "")
ctx.document_settings.contains(doc, "wrap")          # set in either layer?

Schreibzugriffe laufen über ctx.settings — nie zurück in die .editorconfig.


diagnostics

Wellenlinien und Rand-Glyphen für ein Dokument pushen — aus einem Linter, einem Sprachserver, einem Build-Log. Der Host rendert sie; Sie berühren die Ansicht nie. Veröffentlichen ersetzt alles, was Sie zuvor für das Paar (Dokument, source) veröffentlicht haben — senden Sie also jedes Mal die vollständige Menge:

ctx.diagnostics.publish(doc, "mylinter", [
    mte.Diagnostic(start=12, end=18, message="unused variable",
                   severity=mte.SEVERITY_WARNING, code="W0612"),
])
ctx.diagnostics.publish(doc, "mylinter", [])        # retract only your own
ctx.diagnostics.clear(doc)                          # clears EVERY producer's set on doc

source ist Pflicht — es hält Ihre Menge von denen anderer Erzeuger (LSP-Plugin, Rechtschreibprüfung) auf demselben Dokument getrennt. Schweregrade: SEVERITY_ERROR / SEVERITY_WARNING / SEVERITY_INFORMATION / SEVERITY_HINT. Offsets sind halboffene Byte-Bereiche [start, end).


theme

Registrieren Sie die Farben, mit denen Ihre eigene UI malt, als benannte, theme-fähige Kategorien; der Theme-Editor listet sie automatisch, und die Überschreibungen des Nutzers wandern in die Theme-Datei:

h = ctx.theme.register_category("com.example.my/match", "Match highlight",
                                default_light=0xFFF3B0, default_dark=0x8A6D00)
rgb     = ctx.theme.resolve_color("com.example.my/match")   # 0xRRGGBB int
themed  = ctx.theme.has_override("com.example.my/match")
variant = ctx.theme.active_variant()                        # "light" / "dark" / ""
cats    = ctx.theme.categories()                            # list[mte.ThemeCategory]
ctx.theme.unregister_category(h)

Lesen Sie resolve_color bei ctx.events.on_theme_changed neu.


compare

Öffnet (oder fokussiert) die Vergleichsansicht des Editors über zwei Quellen. Jede Seite ist eine Datei auf der Platte (path=), wörtlicher Text (content=) oder beides (gezeigt wird der Inhalt, der Pfad gibt der Sitzungspersistenz eine wiederherstellbare Identität):

shown = ctx.compare.open(
    mte.CompareSource("before", path="/tmp/a.txt"),
    mte.CompareSource("after", content=new_text),
    ignore_whitespace=True)                          # ignore_case / ignore_eol too

Liefert False, wenn eine Seite nicht gelesen werden kann.


settings

Persistenter typisierter Schlüssel/Wert-Speicher, hostseitig auf plugins/<your.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 settings.changed-Abo (ctx.events.on_settings_changed; siehe Ereignis-Referenz).

app_settings

Nur-Lese-Sicht auf die editorweite Konfiguration. Die bekannten Schlüsselnamen sind als Konstanten in mte.keys exportiert (Spiegel des C++-Headers PluginApi/AppSettingsKeys.h, der die autoritative Liste bleibt):

import mte
size   = ctx.app_settings.get(mte.keys.EDITOR_FONT_SIZE, mte.keys.DEFAULT_EDITOR_FONT_SIZE)
family = ctx.app_settings.get(mte.keys.EDITOR_FONT_FAMILY, "")   # "" = host default
theme  = ctx.app_settings.get(mte.keys.UI_THEME, mte.keys.DEFAULT_UI_THEME)  # "system"/"light"/"dark"

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, proportional_size=0.0, tab_group="") -> 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. Übergeben Sie False für ein Panel, das beim Start nicht aufspringen soll, und zeigen Sie es später mit set_panel_visible.
  • proportional_size (float) — die Standardgröße des Panels als Anteil am Hauptfenster: seiner Breite in den Bereichen "left" / "right", seiner Höhe in "top" / "bottom". 0.0 (der Standard) behält das Pixelverhalten, das ein Werkzeug-Panel will — das Panel hält seine Größe, und eine Fenstergrößenänderung lässt es in Ruhe. Ein Bruch wie 0.4 ist, was eine Vorschau will: das Maximieren des Fensters macht das Panel mit ihm breiter. Ihnen gehört nur der Standard; sobald der Nutzer den Trenner des Panels zieht, übernimmt und merkt der Host seinen Anteil. Werte außerhalb des Bereichs werden geklemmt, und der Host behält für Panel und Editor eine Mindestausdehnung. Die vollständigen Regeln stehen unter proportionalSize.
  • tab_group (str) — Panels mit derselben nicht-leeren Gruppen-ID in derselben area teilen sich ein Dock als Tabs, statt nebeneinander zu liegen. Geben Sie ihr einen Namensraum wie einer Panel-ID ("renamer/panels"). Nur die Standardanordnung: Ein vom Benutzer gespeichertes Layout hat Vorrang. Siehe tabGroup.

ctx.docks.is_panel_visible(handler_id) -> bool

Ob ein mit add_panel hinzugefügtes Panel geöffnet ist — ein Panel, das als Tab hinter einem anderen liegt, zählt als geöffnet; eines, das mit initially_visible=False registriert und nie gezeigt wurde, ein geschlossenes und eine unbekannte ID nicht. Kombinieren Sie es mit set_panel_visible, um Ihre Panels für eine Aufgabe zu zeigen und danach wiederherzustellen, was der Benutzer hatte:

was_open = ctx.docks.is_panel_visible(panel)
ctx.docks.set_panel_visible(panel, True)
# ... when the task ends:
ctx.docks.set_panel_visible(panel, was_open)

ctx.docks.set_panel_visible(handler_id, visible)

Zeigt oder verbirgt ein mit add_panel hinzugefügtes Panel. Anzeigen hebt das Panel auch über die Docks, mit denen es sich einen Bereich teilt, sodass ein Befehl, der sein Panel „öffnet", es wirklich vor den Nutzer bringt. Idempotent — denselben Wert beliebig wiederholen, kein Zustand zu verfolgen; unbekannte Ids werden still ignoriert.

panel = ctx.docks.add_panel(id="mine.panel", title="Mine", form=form,
                            initially_visible=False)

@ctx.commands.command("mine.show", title="Show My Panel")
def _show():
    ctx.docks.set_panel_visible(panel, True)

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.


line_markers

Randsymbole und Tönungen ganzer Zeilen, die an Dokumentzeilen hängen und den Bearbeitungen des Benutzers folgen — Haltepunkte, ein Pfeil für die aktuelle Anweisung, Änderungsstreifen — sowie der Anspruch auf den Klick in die Lesezeichenspalte. Der native Dienst und alle seine Regeln sind unter Zeilenmarkierungen & Randklick beschrieben.

bp_colour = ctx.theme.register_category("myplugin.breakpoint", "Breakpoint",
                                        default_light=0xCC0000,
                                        default_dark=0xFF5555)
bp = ctx.line_markers.register_kind(glyph=mte.GLYPH_CIRCLE,
                                    glyph_color="myplugin.breakpoint",
                                    line_tint="", z_order=50)

marker = ctx.line_markers.add_marker(bp, doc, line)   # 0 = refused
ctx.line_markers.line_of(marker)                      # where it is NOW; -1 = gone
ctx.line_markers.markers(bp, doc)                     # [mte.LineMarkerPlacement(id, line)]
ctx.line_markers.remove_marker(marker)
ctx.line_markers.clear_markers(bp, doc)               # doc 0 = every document
ctx.line_markers.release(bp)                          # removes the kind and its markers
  • register_kind(glyph=, glyph_color=, line_tint=, z_order=) — glyph ist einer von mte.GLYPH_NONE (nur Tönung), GLYPH_CIRCLE, GLYPH_CIRCLE_HOLLOW, GLYPH_CIRCLE_BAR, GLYPH_ARROW, GLYPH_STRIP; die Farben sind IDs von Kategorien, die Sie mit ctx.theme registriert haben. Der Editor hat für alle Plugins zusammen nur wenige Markierungs-Slots, daher kann dies fehlschlagen — dann wird RpcError ausgelöst. Registrieren Sie Ihre Arten einmal, in register().
  • add_marker(kind, doc, line) — 0-basierte Zeile, nicht begrenzt: Eine Zeile außerhalb des Dokuments liefert 0 und setzt nichts.
  • Markierungen wandern mit ihren Zeilen, überstehen ein Neuladen auf derselben Zeilennummer und verschwinden mit ihrem Dokument — fügen Sie Ihre in ctx.events.on_document_opened erneut hinzu.

Der Randklick

@ctx.events.on_margin_clicked
def _clicked(ev):                       # mte.MarginClicked
    if ev.modifiers & mte.MODIFIER_CONTROL:
        edit_condition(ev.document, ev.line)
    else:
        toggle_breakpoint(ev.document, ev.line)

claim = ctx.line_markers.claim_margin_click()

Jeder Linksklick in die Lesezeichenspalte wird als margin.clicked veröffentlicht (document, line, margin = "symbol", modifiers — Bits mte.MODIFIER_SHIFT / MODIFIER_CONTROL / MODIFIER_ALT / MODIFIER_META). Standardmäßig schaltet der Editor dabei auch ein Lesezeichen um; solange irgendein Plugin claim_margin_click() hält, tut er das nicht und überlässt den Klick den Plugins. ctx.line_markers.release(claim) gibt den Klick zurück.


settings_page

Fügt eine Seite im Einstellungen-Dialog des Editors hinzu. Dasselbe Formular-Vokabular wie bei Dock-Panels; dieselbe binds="settings:<key>"-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:<key>" 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:<key>" 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.