Ga naar inhoud

Een Python-plug-in voor de Modular Text Editor schrijven met Claude

Doelgroep: een AI-assistent (Claude / ChatGPT / enzovoort). Deze pagina is een zelfvoorzienende instructieset: geef haar samen met de opdracht aan de assistent («schrijf me alsjeblieft een plug-in die X doet») en de assistent hoort een volledige, correcte, werkende plug-inmap op te leveren zonder de rest van de documentatie te hoeven lezen.

Bent u een mens die met de hand een plug-in wil schrijven, begin dan hier — deze pagina is voor gebruik door een AI geoptimaliseerd en slaat de vriendelijke voorbeelden over.


1. Wat u oplevert

Een map met precies twee bestanden (plus eventuele extra's):

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

Lever de map op met één codeblok per bestand. De gebruiker zet de map ofwel in de plug-inmap van de editor, ofwel comprimeert de map en hernoemt het archief <name>.mteplugin, om het daarna via Voorkeuren → Plug-ins → Plug-in installeren... te installeren. Beide wegen vergen een herstart om te laden.

De twee bestanden worden hieronder uitputtend beschreven. Volg de regels letterlijk — de host voert een statische sonde uit en wijst plug-ins af die ze breken.

2. plugin.json — canoniek sjabloon

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

Harde regels

  1. "apiVersion" moet 1 zijn (de huidige versie van de host).
  2. "entry" moet "python" zijn — niets anders leidt naar deze backend.
  3. "module" moet "main" zijn tenzij er een uitdrukkelijke reden is om de module anders te noemen — en nooit een pad met punten (fase 1 ondersteunt alleen platte modulenamen).
  4. "id" moet in omgekeerde DNS-stijl zijn (org.<vendor>.<name>) en wereldwijd uniek over de geïnstalleerde plug-ins van de gebruiker. Heeft de gebruiker geen leverancier genoemd, gebruik dan org.example..
  5. menus[] moet ten minste één item bevatten waarvan title overeenkomt met het meest linkse segment van elk menupad dat u in main.py gebruikt. Geef de voorkeur aan "Plugins" voor alles wat op de gebruiker gericht is en niet duidelijk in een kernmenu thuishoort.

Optionele velden

  • "pyRequires": [] — een array met pip-vereistetekenreeksen (bijvoorbeeld ["requests>=2.31"]). Is die niet leeg, dan richt de editor bij de eerste start een venv per plug-in in (pip install — heeft die ene keer netwerk nodig) en draait hij de plug-in daarin. Geef de voorkeur aan de standaardbibliotheek wanneer die volstaat (geen vertraging door het inrichten, werkt zonder netwerk); verklaar pyRequires wanneer de opdracht werkelijk een pakket van derden nodig heeft, en zeg de gebruiker dat de eerste start zal pauzeren om het te installeren.
  • "permissions": [] — grove verklaringen van mogelijkheden (bijvoorbeeld ["network"]), alleen-lezen getoond op de pagina Plug-ins. Verklaar "network" wanneer de plug-in uitgaande aanroepen doet; het is een eerlijk etiket voor de gebruiker, geen afdwinging.
  • "requires": [] — id's van ANDERE plug-ins zonder welke deze niet kan werken (geen pip-pakketten). Die starten eerst; ontbreekt er een of staat die uit, dan start deze plug-in niet en zegt de pagina Plug-ins waarom. Laat het leeg, tenzij de taak van de gebruiker echt van een andere plug-in afhangt.
  • "apiMinorVersion": 0 — de laagste minorversie van de editor-API die de plug-in nodig heeft. Laat het op 0, tenzij je een aanroep gebruikt die als nieuwer gedocumenteerd is.

Waarden die u NIET moet verzinnen

  • Elk veld dat hierboven niet genoemd is. De host negeert onbekende sleutels; ze toevoegen is een onderhoudsrisico.

3. main.py — canoniek geraamte

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

Harde regels

  1. register(ctx) MOET een functie op het hoogste niveau zijn. De statische sonde vereist een regel die op kolom 0 met def register( begint. Nestelt u haar in een klasse, decoreert u haar op moduleniveau, of maakt u er een async def van, dan wordt de plug-in bij het vinden in stilte afgewezen.
  2. Voer in vanuit mte wanneer u zich op gebeurtenissen inschrijft — de getypeerde dataclasses (mte.DocumentSaved, enzovoort) komen uit dat pakket. import mte bovenaan is veilig, ook wanneer u het niet rechtstreeks gebruikt; de # noqa: F401 hierboven voorkomt geruis van de linter.
  3. Gebruik nooit print(). De stdout van de worker is het RPC-kanaal; erin schrijven bederft het protocol. Gebruik ctx.log.info / warning / error.
  4. Voer nooit iets in dat u niet verklaard hebt. Buiten de standaardbibliotheek en mte om moet elke invoer van derden in pyRequires van plugin.json staan (de editor installeert die bij de eerste start met pip in het venv van de plug-in) of in de plug-inmap meegeleverd zijn. Een niet-verklaarde invoer werpt op de machine van de gebruiker een ModuleNotFoundError.
  5. Afhandelaars van opdrachten en gebeurtenissen mogen kort blokkeren — ze draaien op de threadpool van de worker, dus synchrone aanroepen naar ctx.editor / ctx.settings / ctx.data_dir zijn toegestaan. Maar callbacks van aanvullingsleveranciers (@ctx.completions.provider(...)) MOGEN NIET blokkeren: die draaien op de gebeurtenislus met een deadline van ~30 ms. Geen I/O, geen aanroepen naar ctx.editor.*, alleen cache. Bouw indexen elders (bijvoorbeeld bij document.opened) en antwoord uit een woordenboek.
  6. Voer Qt, PySide, PyQt of Scintilla niet in. Die zijn niet beschikbaar in de worker.
  7. Opdracht-id's moeten voorafgegaan worden door de naamruimte van uw plug-in om botsingen met de kern of andere plug-ins te vermijden. <id>.<verb> hierboven; bijvoorbeeld wordcount.count, hello.sayHi.
  8. Menupaden moeten beginnen met een menu van het hoogste niveau dat u ook in menus[] van plugin.json verklaard hebt. Afspraak: "Plugins/<Plugin Name>/<Verb>".

4. Volledige API — alles op ctx

U hebt achttien diensten: commands, menus, events, log, status, editor, languages, settings, app_settings, document_settings, theme, diagnostics, compare, completions, docks, settings_page, ui, en data_dir()/diagnostics_dir(). Alles wat hier niet genoemd is bestaat niet — verzin geen diensten. (Venvs per plug-in worden via pyRequires verklaard, niet met een dienst van ctx; de protocolondergrond met stromen en annuleren heeft nog geen API die naar plug-ins gekeerd is.)

ctx.commands

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

Decorator. Registreert een opdracht en koppelt _handler eraan (een functie zonder argumenten die None oplevert). Uitzonderingen in _handler worden door de worker opgevangen en naar stderr geschreven; ze laten de editor niet crashen.

Zet requires_editor wanneer de opdracht het actieve document leest (buffer, selectie, cursor) en requires_writable_editor wanneer zij de tekst wijzigt. De host maakt de opdracht dan grijs waar zij niet zou kunnen werken — op een tabblad zonder document (een vergelijking of een logweergave) en, bij de schrijfvlag, op een alleen-lezen document — in plaats van de gebruiker een stille niet-bewerking te laten aanroepen. De schrijfvlag impliceert de andere; laat beide uit voor opdrachten die alleen een venster of een paneel openen.

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 is "Top/Sub/…/Label". Tussenliggende submenu's worden op verzoek gemaakt.

set_item_checked maakt van het item een aanvinkbaar menu-item en zet het vinkje. Gebruik het voor blijvende aan/uit-schakelaars. De eerste aanroep maakt het aanvinkbaar; latere kantelen alleen. Stille niet-bewerking bij onbekende id's.

set_item_enabled maakt het item grijs. Gebruik het wanneer de opdracht een voorwaarde nodig heeft, zodat een niet-beschikbaar item er niet-beschikbaar uitziet in plaats van bij een klik niets te doen. Idempotent; items beginnen ingeschakeld; stille niet-bewerking bij onbekende id's.

ctx.events

Zeventien onderwerpen, elk met een zoete decorator en een kale vorm.

Zoet Kaal onderwerp Inhoud
@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) — bij elke bewerking, houd het triviaal
@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() — één keer per uitvoering, na het eerste beeld
@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) — een klik in de bladwijzerkolom; bits van modifiers: mte.MODIFIER_*

Kale vorm:

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

Verzin geen nieuwe onderwerpen — de kale vorm valideert ze en werpt een ValueError voor alles wat niet in de tabel staat.

ctx.log

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

Afvuren-en-vergeten. Regels krijgen in het diagnostieklogboek van de host het voorvoegsel [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 moet "info" / "warning" / "error" zijn. timeout_ms=0 betekent dat de host een standaard kiest. show() is voor vluchtige meldingen; alles wat op het scherm blijft (een levende telling, een modusvlag) is een segment — de twee overschrijven elkaar nooit.

ctx.editor (fase 2)

Elke methode is synchroon: hij blokkeert de aanroepende thread. Prima vanuit een callback met def voor een opdracht of gebeurtenis (de worker verdeelt synchrone afhandelaars over een threadpool); niet toegestaan vanuit afhandelaars met async def en vanuit leveranciers van ctx.completions (die rechtstreeks op de gebeurtenislus draaien).

Documenten worden aangeduid met ondoorzichtige int-id's (0 = geen document).

doc     = ctx.editor.active_document()
docs    = ctx.editor.open_documents()
ctx.editor.set_active_document(doc)
path    = ctx.editor.file_path(doc)                 # str or None

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

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

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

new_doc = ctx.editor.open_file("/path/to/file")     # 0 on failure
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 (fase 2)

Getypeerde sleutel/waarde-opslag per plug-in (ctx.settings) plus een alleen-lezen kijk op de instellingen van de hele toepassing (ctx.app_settings). Het type wordt uit het Python-type van de standaardwaarde afgeleid.

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 werpen een RuntimeError. In uw eigen logica moet u bij het lezen de boolean vóór de integer controleren (in Python is isinstance(True, int) == True).

ctx.completions (fase 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)]

Velden op req: document, prefix, position, language_id, manual. Soorten: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Lever list[CompletionItem], gewone tekenreeksen of woordenboeken op. De callback MOET binnen ~30 ms terugkeren, anders laat de host uw resultaten voor die toetsaanslag vallen — geen I/O, geen ctx.editor.text(...), alleen cache. Bouw indexen elders (bijvoorbeeld bij document.opened).

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

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

Gebruik data_dir() voor caches en indexen. diagnostics_dir() is alleen-lezen (de crashmelder schrijft daarheen).

ctx.docks — declaratieve dokpanelen (fase 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")]),
    ]),
)

Bouw de formulierspecificatie met de hulpjes van mte.ui. Het vocabulaire van besturingselementen ligt vast: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List binnen Column / Row / Group.

  • binds="settings:<key>" — de waarde van het besturingselement wordt automatisch via ctx.settings bewaard (dezelfde opslag als ctx.settings.set).
  • on_click="<cmd.id>" — alleen voor Button; roept een geregistreerde opdracht aan.
  • on_change=<handler> — ken er een toe via ctx.docks.on_change(callback); gaat af bij bewerkingen van de gebruiker.

Callbacks van on_change draaien op de gebeurtenislus van de worker (NIET op de threadpool). Houd ze goedkoop — geen editoraanroepen, geen lang werk. Wijzig een kleine staat in het geheugen, noteer iets, of zet een opdracht in gang; doe zwaar werk vanuit de opdracht.

Geef initially_visible=False mee voor een paneel dat bij het starten niet mag opduiken, en toon het daarna vanuit zijn opdracht met ctx.docks.set_panel_visible(panel_id, True) (dat brengt het ook naar voren; is idempotent; onbekende id's worden genegeerd). ctx.docks.is_panel_visible(panel_id) zegt of een paneel open is (een achterliggend tabblad telt mee). Meerdere panelen die bij elkaar horen: geef dezelfde tab_group="<plugin.id>/panels" en dezelfde area mee, dan delen ze één dok met tabbladen.

ctx.line_markers — markeringen op documentregels

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

Registreer soorten één keer in register(); de editor heeft maar een paar plaatsen. Claim de margeklik alleen als de plug-in er een betekenis aan geeft — klikken komen hoe dan ook binnen via @ctx.events.on_margin_clicked.

ctx.languages — syntaxiskleuring

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 moet een bestaande lexer van de backend noemen; styles en keywords gebruiken de eigen namen van de lexer (dezelfde vorm als de meegeleverde colorizers/*.json), nooit numerieke id's.

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

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

Stuur elke keer de VOLLEDIGE verzameling voor uw source; die vervangt de vorige. source is verplicht.

ctx.theme — tematiseerbare kleuren voor uw eigen interface

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 — diff naast elkaar

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

ctx.events.publish

Alleen workspace.rootChanged / workspace.filesChanged mogen door een plug-in gepubliceerd worden; al het andere werpt een ValueError. Op elke hostgebeurtenis behalve document.aboutToSave kan ingeschreven worden (zie de lijst met decorators); de native gebeurtenis DocumentAboutToClose heeft helemaal geen Python-onderwerp — gebruik document.closed.

ctx.settings_page — declaratieve Voorkeurenpagina's (fase 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"),
    ]),
)

Dezelfde bouwers, dezelfde semantiek van binds. Twee besturingselementen die een binds="settings:<key>" delen spiegelen elkaar in real time — het dokpaneel en de Voorkeurenpagina kunnen beide een vinkje voor dezelfde instelling dragen, en de een omschakelen kantelt de ander.

Voorbehoud bij fase 3

Waarden worden gretig bij elke bewerking van de gebruiker bewaard — Annuleren draait niet terug wat de gebruiker gewijzigd heeft. Dat wordt in een latere verfijning beter.

ctx.ui.get / ctx.ui.set — toegang tot besturingselementen tijdens het draaien (fase 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 levert de voornaamste waarde als tekenreeks op:

  • Label / Button / LineEdit / ComboBox → de tekst
  • Checkbox → "true" / "false"
  • NumberSpin → het gehele getal als decimaal
  • List → de tekst van het op dat moment geselecteerde item (lege tekenreeks als er geen is)

Een set die door de host aangestuurd wordt is stil: hij laat de on_change van het besturingselement NIET opnieuw afgaan, zodat u uw eigen widgets vanuit een opdrachtafhandelaar kunt bijwerken zonder oneindige lussen.


5. Volledig uitgewerkt voorbeeld — de plug-in wordcount

Kopieer dit letterlijk en wijzig de id, de name en de titles naar uw geval. Het oefent elke dienst uit fase 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")

Verwacht verloop voor de gebruiker: open de editor, sla een bestand drie keer op, en klik dan op Plugins → Word Count → Report save count. De statusbalk toont 3 save(s) this session. Het logboek noteert elke stap.


6. Veelvoorkomende taken — recepten om te kopiëren en plakken

Elk recept is een fragment van alleen het lichaam: plak het binnen def register(ctx):.

Een menuopdracht toevoegen

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

Reageren op opslaan

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

Waarschuwen bij het afsluiten

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

Noteren op een bepaald niveau

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

Meerdere verwante opdrachten

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

Blijvende aan/uit-schakelaar met een zichtbaar vinkje

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)

Zijpaneel met een tekstinvoer, een vinkje en twee knoppen

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

Voorkeurenpagina die de instellingen van een dok 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. Antipatronen — doe deze dingen NIET

Fout:

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

Goed:

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

register binnen een klasse

Fout:

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

Goed:

def register(ctx):
    ...

async def register

Fout:

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

Goed:

def register(ctx):
    ...

Invoer van derden zonder de wheel mee te leveren

Fout:

import requests   # <-- ImportError on most user machines

Goed (fase 1): houd u aan de standaardbibliotheek. Hebt u werkelijk iets van derden nodig, lever de wheel dan in de plug-inmap mee en breid sys.path in de eerste regels van main.py uit; zie Verpakken.

Invoer van Qt

Fout:

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

Goed: nergens Qt. Alle interface die u kunt bijdragen loopt via ctx.menus, ctx.status, ctx.diagnostics, ctx.compare en de declaratieve dok- en instellingenpanelen. Nooit een kale widget.

Blokkeren in een afhandelaar

Fout:

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

Goed (fase 1): geef het aan een thread door en communiceer via ctx.log:

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

Fout:

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

Goed: houd het segment van het hoogste niveau in het pad gelijk aan menus[]. Gebruik "Plugins/Mine/Do" hierboven.

ctx bewaren voor later gebruik

Fout:

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

Goed: leg de diensten die u nodig hebt vast in de closure die ze gebruikt. register is waar het koppelen gebeurt; al het overige is een afhandelaar die er via de closure al toegang toe heeft.


8. Controlelijst voor foutopsporing, voor de gebruiker

Neem deze aan het einde van uw antwoord op wanneer u een plug-in oplevert, zodat de gebruiker weet wat hij moet nagaan als het niet werkt:

  1. Start de editor vanuit een shell waarin een Python-interpreter 3.9+ in het PATH staat (of waarin MTE_PYTHON er een aanwijst).
  2. Zoek naar PluginHost: Python plugin backend enabled; interpreter=… in het logboek. Ontbreekt die, ga dan naar Problemen oplossen.
  3. Zoek naar [python:<your.id>] register() ran (als u het voorbeeld wordcount gevolgd hebt) — dat bewijst dat register uitgevoerd is.
  4. Ontbreekt het menu-item, zoek dan in het logboek naar de uitleg PythonPluginBackend: skipping '<id>': ….
  5. Doet het aanklikken van het menu niets, zoek dan in het logboek naar regels [worker stderr] … — Python-tracebacks belanden daar.

9. Voorwaarden om aan het einde van elk antwoord te herhalen

Wanneer u een plug-in oplevert, vertel de gebruiker altijd:

  • Installeer haar op een van beide manieren:
    • comprimeer de inhoud van de map als <name>.mteplugin en gebruik Voorkeuren → Plug-ins → Plug-in installeren... (dat valideert het pakket voordat er iets aangeraakt wordt), of
    • zet de map in de plug-inmap per gebruiker van de editor (%LOCALAPPDATA%/MTE/Plugins/ op Windows; ~/Library/Application Support/MTE/Plugins/ op macOS; ~/.local/share/MTE/Plugins/ op Linux).
  • Herstart de editor.
  • Stel MTE_PYTHON in wanneer het logboek «no Python interpreter found» zegt.
  • Rapporteer de logregel [python:<id>] … terug, zodat we kunnen bevestigen dat ze gedraaid heeft.

10. Bron van waarheid

Is een uitspraak op deze pagina in strijd met wat het Python-cliëntpakket werkelijk implementeert, dan wint het pakket. Het komt naast de editor mee in <exe>/Python/mte/ — lees die broncode voordat u over gedrag beslist. De C++-kant van het protocol staat in Components/PluginHost/Src/PyServiceBridge.cpp en de draadspecificatie in Python/PROTOCOL.md.