Gå til indholdet

At skrive et Python-udvidelsesmodul til Modular Text Editor med Claude

Målgruppe: en AI-assistent (Claude, ChatGPT og lignende). Denne side er et selvstændigt sæt instruktioner: giv den til assistenten sammen med opgaven (»skriv venligst et udvidelsesmodul til mig, der gør X«), og assistenten bør frembringe en fuldstændig, korrekt og fungerende modulmappe uden at skulle læse resten af dokumentationen.

Er du et menneske, der prøver at skrive et udvidelsesmodul i hånden, så begynd her i stedet — denne side er tilrettelagt til at blive læst af en AI og springer de venlige eksempler over.


1. Hvad du frembringer

En mappe med præcis to filer (plus valgfrie ekstrafiler):

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

Giv mappen tilbage som én kodeblok pr. fil. Brugeren lægger enten mappen i editorens udvidelsesmodulmappe eller pakker mappen som en zip, omdøber arkivet til <name>.mteplugin og installerer den via Indstillinger → Udvidelsesmoduler → Installér udvidelsesmodul.... Begge veje kræver en genstart for at blive indlæst.

De to filer er beskrevet udtømmende nedenfor. Følg reglerne bogstaveligt — værten foretager en statisk efterprøvning og afviser udvidelsesmoduler, der bryder dem.


2. plugin.json — den kanoniske skabelon

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

Ufravigelige regler

  1. "apiVersion" skal være 1 (værtens nuværende version).
  2. "entry" skal være "python" — intet andet går til denne backend.
  3. "module" skal være "main", medmindre der er en udtrykkelig grund til at give modulet et andet navn — og aldrig en sti med punktummer (fase 1 understøtter kun flade modulnavne).
  4. "id" skal være i omvendt DNS-stil (org.<vendor>.<name>) og globalt entydigt blandt brugerens installerede udvidelsesmoduler. Har brugeren ikke angivet en leverandør, så brug org.example..
  5. menus[] skal indeholde mindst én post, hvis title passer til det yderste venstre led i hver menusti, du bruger i main.py. Foretræk "Plugins" til alt, der vender mod brugeren, og som ikke tydeligvis hører hjemme i en af kernens menuer.

Valgfrie felter

  • "pyRequires": [] — en tabel af pip-kravstrenge (f.eks. ["requests>=2.31"]). Er den ikke tom, sætter editoren et virtuelt miljø op pr. udvidelsesmodul ved første start (pip install — kræver netadgang den ene gang) og kører modulet under det. Foretræk standardbiblioteket, når det kan gøre arbejdet (ingen forsinkelse ved opsætning, virker uden net); erklær pyRequires, når opgaven virkelig kræver en tredjepartspakke, og fortæl brugeren, at den første start holder pause for at installere den.
  • "permissions": [] — grove erklæringer om formåen (f.eks. ["network"]), der vises skrivebeskyttet på udvidelsesmodulsiden. Erklær "network", når modulet foretager udgående kald; det er ærlig mærkning over for brugeren og ikke en håndhævelse.
  • "requires": [] — id'er på ANDRE udvidelsesmoduler, som dette ikke kan fungere uden (ikke pip-pakker). De starter først; mangler et af dem, eller er det slået fra, starter dette modul ikke, og udvidelsesmodulsiden fortæller hvorfor. Lad den være tom, medmindre brugerens opgave virkelig afhænger af et andet modul.
  • "apiMinorVersion": 0 — den laveste minor-version af editorens API, som modulet har brug for. Lad den stå på 0, medmindre du bruger et kald, der er dokumenteret som nyere.

Værdier, du IKKE skal opfinde

  • Ethvert felt, der ikke er opregnet ovenfor. Værten ser bort fra ukendte nøgler; at tilføje dem er en vedligeholdelsesrisiko.

3. main.py — det kanoniske skelet

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

Ufravigelige regler

  1. register(ctx) SKAL være en funktion på øverste niveau. Den statiske efterprøvning kræver en linje, der begynder med def register( i kolonne 0. Indlejrer du den i en klasse, dekorerer den på modulniveau eller gør den til en async def, afvises udvidelsesmodulet i stilhed ved opdagelsen.
  2. Importér fra mte, når du abonnerer på hændelser — de typede dataklasser (mte.DocumentSaved og så videre) kommer fra den pakke. import mte øverst er trygt, også selv om du ikke bruger den direkte; # noqa: F401 ovenfor forhindrer støj fra kodekontrollen.
  3. Brug aldrig print(). Arbejderens stdout er RPC-kanalen; at skrive til den ødelægger protokollen. Brug ctx.log.info / warning / error.
  4. Importér aldrig noget, du ikke har erklæret. Ud over standardbiblioteket og mte skal hver tredjepartsimport stå i pyRequires i plugin.json (editoren installerer den med pip i modulets virtuelle miljø ved første start) eller ligge inde i modulmappen. En uerklæret import giver ModuleNotFoundError på brugerens maskine.
  5. Kommando- og hændelseshåndteringer må gerne spærre kortvarigt — de kører på arbejderens trådpulje, så synkrone kald ind i ctx.editor, ctx.settings og ctx.data_dir er lovlige. Men tilbagekald fra fuldførelsesleverandører (@ctx.completions.provider(...)) MÅ IKKE spærre: de kører på hændelsesløkken med en frist på cirka 30 ms. Ingen ind- og uddata, ingen kald ind i ctx.editor.*, kun mellemlager. Byg indekser andetsteds (f.eks. ved document.opened), og svar fra en ordbog.
  6. Importér ikke Qt, PySide, PyQt eller Scintilla. De er ikke tilgængelige i arbejderen.
  7. Kommando-id'er skal have et præfiks med dit udvidelsesmoduls navnerum for at undgå sammenstød med kernen og andre moduler. <id>.<verb> ovenfor; f.eks. wordcount.count eller hello.sayHi.
  8. Menustier skal begynde med en øverste menu, du også har erklæret i menus[] i plugin.json. Sædvane: "Plugins/<Plugin Name>/<Verb>".

4. Hele API'en — alt på ctx

Du har atten tjenester: commands, menus, events, log, status, editor, languages, settings, app_settings, document_settings, theme, diagnostics, compare, completions, docks, settings_page, ui og data_dir()/diagnostics_dir(). Alt, der ikke er opregnet her, findes ikke — opfind ikke tjenester. (Virtuelle miljøer pr. udvidelsesmodul erklæres via pyRequires og ikke med en ctx-tjeneste; protokolunderlaget til strømning og afbrydelse har endnu ingen API, der vender mod udvidelsesmoduler.)

ctx.commands

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

En dekorator. Registrerer en kommando og binder _handler (en funktion uden argumenter, der giver None tilbage). Undtagelser i _handler opfanges af arbejderen og skrives til stderr; de får ikke editoren til at gå ned.

Sæt requires_editor, når kommandoen læser det aktive dokument (bufferen, markeringen, markøren), og requires_writable_editor, når den ændrer teksten. Værten nedtoner så kommandoen der, hvor den ikke kunne virke — på et faneblad uden et dokument (en forskels- eller logvisning) og, for skriveflaget, på et skrivebeskyttet dokument — i stedet for at lade brugeren udløse noget, der i stilhed ikke gør noget. Skriveflaget indebærer det andet; lad begge stå slået fra for kommandoer, der bare åbner et vindue eller et panel.

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 er "Øverst/Under/…/Mærkat". Mellemliggende undermenuer oprettes efter behov.

set_item_checked gør punktet til et afkrydseligt menupunkt og sætter dets flueben. Brug den til vedvarende til/fra-kontakter. Det første kald gør punktet afkrydseligt; de følgende kald vender bare fluebenet. Gør i stilhed ingenting ved ukendte id'er.

set_item_enabled nedtoner punktet. Brug den, når kommandoen kræver en forudsætning, så en post, der ikke er tilgængelig, også ser sådan ud i stedet for ikke at gøre noget, når man klikker på den. Kan gentages; punkter begynder tilkoblede; gør i stilhed ingenting ved ukendte id'er.

ctx.events

Sytten emner, hvert med en let dekorator og en rå form.

Let form Rå emne Indhold
@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) — ved hver ændring, hold den ganske enkel
@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 gang pr. kørsel, efter det første billede
@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) — et klik i bogmærkekolonnen; bits i modifiers: mte.MODIFIER_*

Den rå form:

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

Opfind ikke nye emner — den rå form efterprøver dem og kaster ValueError ved alt, der ikke står i tabellen.

ctx.log

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

Send og glem. Linjer får præfikset [python:<plugin.id>] i værtens diagnostiklog.

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 skal være "info" / "warning" / "error". timeout_ms=0 betyder, at værten vælger en standard. show() er til forbigående meddelelser; alt, der bliver stående på skærmen (et levende tal, et tilstandsflag), er et afsnit — de to ødelægger aldrig hinanden.

ctx.editor (fase 2)

Hver metode er synkron: den spærrer den kaldende tråd. Det er fint fra et def-tilbagekald til en kommando eller en hændelse (arbejderen fordeler synkrone håndteringer på en trådpulje); ulovligt fra async def-håndteringer og fra ctx.completions-leverandører (som kører direkte på hændelsesløkken).

Dokumenter henvises der til med uigennemsigtige int-id'er (0 = intet 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 (fase 2)

Et typet nøgle/værdi-lager afgrænset pr. udvidelsesmodul (ctx.settings) plus en skrivebeskyttet visning af indstillingerne for hele programmet (ctx.app_settings). Typen udledes af standardværdiens Python-type.

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 kaster RuntimeError. Bool skal kontrolleres før int i din egen logik, når du læser (i Python er 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)]

Felter på req: document, prefix, position, language_id, manual. Typer: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Giv en list[CompletionItem], almindelige tekststrenge eller ordbøger tilbage. Tilbagekaldet SKAL vende tilbage inden for cirka 30 ms, ellers kasserer værten dine resultater for det tastetryk — ingen ind- og uddata, intet ctx.editor.text(...), kun mellemlager. Byg indekser andetsteds (f.eks. ved document.opened).

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

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

Brug data_dir() til mellemlagre og indekser. diagnostics_dir() er skrivebeskyttet (det er nedbrudsrapporteringen, der skriver der).

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

Byg formularbeskrivelsen med mte.ui-hjælperne. Ordforrådet af kontroller ligger fast: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List inde i Column / Row / Group.

  • binds="settings:<key>" — kontrollens værdi gemmes af sig selv gennem ctx.settings (samme lager som ctx.settings.set).
  • on_click="<cmd.id>" — kun knapper; kalder en registreret kommando.
  • on_change=<handler> — tildel den via ctx.docks.on_change(callback); udløses ved ændringer fra brugeren.

on_change-tilbagekald kører på arbejderens hændelsesløkke (IKKE på trådpuljen). Hold dem billige — ingen editorkald, intet langvarigt arbejde. Ændr en lille tilstand i hukommelsen, notér i loggen, eller udløs en kommando; lav det tunge arbejde fra kommandoen.

Giv initially_visible=False til et panel, der ikke må springe frem ved opstart, og vis det så fra dets kommando med ctx.docks.set_panel_visible(panel_id, True) (som også løfter det frem; kan gentages; ukendte id'er ses der bort fra). ctx.docks.is_panel_visible(panel_id) fortæller, om et panel er åbent (et faneblad i baggrunden tæller med). Flere paneler, der hører sammen: giv den samme tab_group="<plugin.id>/panels" og det samme area, så deler de én dok med faneblade.

ctx.line_markers — mærker på dokumentlinjer

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

Registrér typerne én gang i register(); editoren har kun få pladser. Gør kun krav på margenklikket, når modulet giver det en betydning — klikkene kommer under alle omstændigheder gennem @ctx.events.on_margin_clicked.

ctx.languages — syntaksfarvning

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 skal navngive en eksisterende backend-lexer; styles og keywords bruger lexerens egne navne (samme form som de medleverede colorizers/*.json), aldrig numeriske id'er.

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 — bølgelinjer

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

Send HELE sættet for din source hver gang; det erstatter det forrige. source er påkrævet.

ctx.theme — farver til din egen brugerflade, der kan indgå i temaer

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 — forskel side om side

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

ctx.events.publish

Kun workspace.rootChanged / workspace.filesChanged må udgives af et udvidelsesmodul; alt andet kaster ValueError. Der kan abonneres på hver værtshændelse undtagen document.aboutToSave (se listen over dekoratorer); den indbyggede hændelse DocumentAboutToClose har slet intet Python-emne — brug document.closed.

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

Samme byggere, samme betydning af binds. To kontroller, der deler et binds="settings:<key>", spejler hinanden med det samme — dokpanelet og indstillingssiden kan begge have et afkrydsningsfelt for den samme indstilling, og vender man det ene, vender det andet med.

Forbehold i fase 3

Værdier gemmes med det samme ved hver ændring fra brugeren — Annullér ruller ikke tilbage, hvad brugeren har ændret. Det bliver bedre i en senere forfining.

ctx.ui.get / ctx.ui.set — adgang til panelkontroller under kørslen (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 giver den primære værdi tilbage som en tekststreng:

  • Label / Button / LineEdit / ComboBox → teksten
  • Checkbox → "true" / "false"
  • NumberSpin → heltallet som decimaltal
  • List → teksten på den markerede post (tom tekststreng, hvis ingen)

Et set fra værten er tavst: det udløser IKKE kontrollens on_change igen, så du kan opdatere dine egne kontroller fra en kommandohåndtering uden uendelige løkker.


5. Et fuldstændigt gennemarbejdet eksempel — udvidelsesmodulet wordcount

Kopiér dette ordret, og ret id, name og titlerne, så de passer til dit tilfælde. Det bruger hver eneste tjeneste fra 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")

Det forventede forløb for brugeren: åbn editoren, gem en fil tre gange, og klik så på Plugins → Word Count → Report save count. Statuslinjen viser 3 save(s) this session. Loggen noterer hvert skridt.


6. Almindelige opgaver — opskrifter til at kopiere ind

Hver opskrift er et uddrag, der kun består af en krop: sæt det ind i def register(ctx):.

At tilføje en menukommando

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

At reagere på gemning

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

At advare ved afslutning

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

At notere på et bestemt niveau

ctx.log.trace  ("very fine")
ctx.log.debug  ("dev")
ctx.log.info   ("routine")
ctx.log.warning("odd")
ctx.log.error  ("wrong")

En skillelinje i menuen

ctx.menus.add_separator("Plugins/Mine")

Flere beslægtede kommandoer

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

En vedvarende til/fra-kontakt med et synligt flueben

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)

Et sidepanel med et tekstfelt, et afkrydsningsfelt og to knapper

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

En indstillingsside, der spejler et panels indstillinger

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. Antimønstre — gør IKKE disse ting

Forkert:

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

Rigtigt:

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

register inde i en klasse

Forkert:

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

Rigtigt:

def register(ctx):
    ...

async def register

Forkert:

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

Rigtigt:

def register(ctx):
    ...

Tredjepartsimport uden at levere hjulet med

Forkert:

import requests   # <-- ImportError on most user machines

Rigtigt (fase 1): hold dig til standardbiblioteket. Har du virkelig brug for noget fra tredjepart, så lever hjulet inde i modulmappen, og udvid sys.path i de første linjer af main.py; se Pakning.

Qt-import

Forkert:

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

Rigtigt: ingen Qt nogen steder. Al den brugerflade, du kan bidrage med, går gennem ctx.menus, ctx.status, ctx.diagnostics, ctx.compare og de deklarative panel- og indstillingssider. Aldrig en rå kontrol.

At spærre i en håndtering

Forkert:

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

Rigtigt (fase 1): overlad det til en tråd, og meld tilbage 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")

En menusti, hvis øverste led ikke står i plugin.json

Forkert:

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

Rigtigt: hold stiens øverste led på linje med menus[]. Brug "Plugins/Mine/Do" ovenfor.

At gemme ctx til senere brug

Forkert:

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

Rigtigt: fang de tjenester, du har brug for, i den lukning, der bruger dem. register er der, hvor tingene bindes sammen; alt andet er en håndtering, der allerede har adgang gennem sin lukning.


8. Fejlfindingsliste til brugeren

Tag denne med i slutningen af dit svar, når du frembringer et udvidelsesmodul, så brugeren ved, hvad der skal kontrolleres, hvis det ikke virker:

  1. Start editoren fra en skal, hvor en Python 3.9+-fortolker ligger i PATH (eller hvor MTE_PYTHON peger på en).
  2. Se efter PluginHost: Python plugin backend enabled; interpreter=… i loggen. Mangler den, så gå til Fejlfinding.
  3. Se efter [python:<your.id>] register() ran (hvis du fulgte wordcount-eksemplet) — det viser, at register blev kørt.
  4. Mangler menupunktet, så se i loggen efter forklaringer af typen PythonPluginBackend: skipping '<id>': ….
  5. Sker der ingenting, når du klikker på menuen, så se i loggen efter [worker stderr] …-linjer — Pythons kaldstakspor lander der.

9. Begrænsninger, der skal gentages i slutningen af hvert svar

Når du frembringer et udvidelsesmodul, så fortæl altid brugeren:

  • Installér det på en af to måder:
    • pak mappens indhold som en zip ved navn <name>.mteplugin, og brug Indstillinger → Udvidelsesmoduler → Installér udvidelsesmodul... (som efterprøver pakken, inden noget som helst røres), eller
    • læg mappen i editorens udvidelsesmodulmappe pr. bruger (%LOCALAPPDATA%/MTE/Plugins/ på Windows; ~/Library/Application Support/MTE/Plugins/ på macOS; ~/.local/share/MTE/Plugins/ på Linux).
  • Genstart editoren.
  • Sæt MTE_PYTHON, hvis loggen siger »no Python interpreter found«.
  • Meld [python:<id>] …-loglinjen tilbage, så vi kan bekræfte, at det kørte.

10. Kilden til sandhed

Er der noget på denne side, der strider mod det, Python-klientpakken faktisk gør, vinder pakken. Den leveres ved siden af editoren i <exe>/Python/mte/ — læs den kildekode, inden du afgør en adfærd. C++-siden af protokollen ligger i Components/PluginHost/Src/PyServiceBridge.cpp, og formatet på ledningen er beskrevet i Python/PROTOCOL.md.