Gå til innhold

Å skrive et Python-programtillegg til Modular Text Editor med Claude

Målgruppe: en KI-assistent (Claude, ChatGPT og lignende). Denne siden er et selvstendig sett instruksjoner: gi den til assistenten sammen med oppgaven («skriv meg et programtillegg som gjør X»), så bør assistenten gi en fullstendig, riktig og fungerende programtilleggsmappe uten å måtte lese resten av dokumentasjonen.

Er du et menneske som prøver å skrive et programtillegg for hånd, så begynn her i stedet — denne siden er lagt til rette for å bli lest av en KI og hopper over de vennlige eksemplene.


1. Hva du lager

En mappe med nøyaktig to filer (pluss valgfrie ekstrafiler):

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

Gi mappen tilbake som én kodeblokk per fil. Brukeren legger enten mappen i redigererens programtilleggsmappe, eller pakker mappen som en zip, gir arkivet navnet <name>.mteplugin og installerer den gjennom Innstillinger → Programtillegg → Installer programtillegg.... Begge veiene krever en omstart for å bli lastet inn.

De to filene er beskrevet uttømmende nedenfor. Følg reglene bokstavelig — verten gjør en statisk kontroll og avviser programtillegg som bryter dem.


2. plugin.json — den kanoniske malen

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

Ufravikelige regler

  1. "apiVersion" må være 1 (vertens gjeldende versjon).
  2. "entry" må være "python" — ingenting annet går til denne backenden.
  3. "module" må være "main", med mindre det er en uttrykkelig grunn til å gi modulet et annet navn — og aldri en sti med punktum (fase 1 støtter bare flate modulnavn).
  4. "id" må være i omvendt DNS-stil (org.<vendor>.<name>) og globalt entydig blant brukerens installerte programtillegg. Har brukeren ikke oppgitt en leverandør, så bruk org.example..
  5. menus[] må inneholde minst én post der title passer til det ytterste venstre leddet i hver menysti du bruker i main.py. Foretrekk "Plugins" for alt som vender mot brukeren, og som ikke tydelig hører hjemme i en av kjernens menyer.

Valgfrie felter

  • "pyRequires": [] — en tabell av pip-kravstrenger (for eksempel ["requests>=2.31"]). Er den ikke tom, setter redigereren opp et virtuelt miljø per programtillegg ved første oppstart (pip install — krever nettilgang den ene gangen) og kjører programtillegget under det. Foretrekk standardbiblioteket når det gjør jobben (ingen forsinkelse ved oppsett, virker uten nett); erklær pyRequires når oppgaven virkelig trenger en tredjepartspakke, og si fra til brukeren om at den første oppstarten vil ta en pause for å installere den.
  • "permissions": [] — grove erklæringer om evner (for eksempel ["network"]), som vises skrivebeskyttet på programtilleggssiden. Erklær "network" når programtillegget gjør utgående kall; det er ærlig merking overfor brukeren og ikke en håndhevelse.
  • "requires": [] — ID-er for ANDRE programtillegg som dette ikke kan virke uten (ikke pip-pakker). De starter først; mangler ett av dem eller er det slått av, starter ikke dette programtillegget, og programtilleggssiden sier hvorfor. La den stå tom med mindre brukerens oppgave virkelig avhenger av et annet programtillegg.
  • "apiMinorVersion": 0 — den laveste minor-versjonen av redigererens API som programtillegget trenger. La den stå på 0 med mindre du bruker et kall som er dokumentert som nyere.

Verdier du IKKE skal finne på

  • Ethvert felt som ikke er listet ovenfor. Verten ser bort fra ukjente nøkler; å legge dem til er en vedlikeholdsrisiko.

3. main.py — det kanoniske skjelettet

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

Ufravikelige regler

  1. register(ctx) MÅ være en funksjon på øverste nivå. Den statiske kontrollen krever en linje som begynner med def register( i kolonne 0. Nøster du den inne i en klasse, dekorerer den på modulnivå eller gjør den til en async def, avvises programtillegget i stillhet ved oppdagelsen.
  2. Importer fra mte når du abonnerer på hendelser — de typede dataklassene (mte.DocumentSaved og så videre) kommer fra den pakken. import mte øverst er trygt også om du ikke bruker den direkte; # noqa: F401 ovenfor hindrer støy fra kodekontrollen.
  3. Bruk aldri print(). Arbeiderens stdout er RPC-kanalen; å skrive til den ødelegger protokollen. Bruk ctx.log.info / warning / error.
  4. Importer aldri noe du ikke har erklært. Utover standardbiblioteket og mte må hver tredjepartsimport stå i pyRequires i plugin.json (redigereren installerer den med pip i programtilleggets virtuelle miljø ved første oppstart) eller ligge inne i programtilleggsmappen. En uerklært import gir ModuleNotFoundError på brukerens maskin.
  5. Kommando- og hendelsesbehandlere kan gjerne sperre kort — de kjører på arbeiderens trådpulje, så synkrone kall inn i ctx.editor, ctx.settings og ctx.data_dir er lovlige. Men tilbakekall fra fullføringsleverandører (@ctx.completions.provider(...)) MÅ IKKE sperre: de kjører på hendelsesløkken med en frist på omtrent 30 ms. Ingen inn- og utdata, ingen kall inn i ctx.editor.*, bare mellomlager. Bygg indekser andre steder (for eksempel ved document.opened), og svar fra en ordbok.
  6. Importer ikke Qt, PySide, PyQt eller Scintilla. De er ikke tilgjengelige i arbeideren.
  7. Kommando-id-er må ha et prefiks med programtilleggets navnerom for å unngå kollisjoner med kjernen og andre programtillegg. <id>.<verb> ovenfor; for eksempel wordcount.count eller hello.sayHi.
  8. Menystier må begynne med en øverste meny du også har erklært i menus[] i plugin.json. Sedvane: "Plugins/<Plugin Name>/<Verb>".

4. Hele API-et — 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 som ikke er listet her, finnes ikke — finn ikke på tjenester. (Virtuelle miljøer per programtillegg erklæres gjennom pyRequires og ikke med en ctx-tjeneste; protokollunderlaget for strømming og avbrytelse har ennå ingen API som vender mot programtillegg.)

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 funksjon uten argumenter som gir None tilbake). Unntak i _handler fanges av arbeideren og skrives til stderr; de får ikke redigereren til å krasje.

Sett requires_editor når kommandoen leser det aktive dokumentet (bufferen, merkingen, markøren), og requires_writable_editor når den endrer teksten. Verten nedtoner da kommandoen der den ikke kunne virke — på en fane uten et dokument (en sammenlignings- eller loggvisning) og, for skriveflagget, på et skrivebeskyttet dokument — i stedet for å la brukeren utløse noe som i stillhet ikke gjør noe. Skriveflagget innebærer det andre; la begge være av for kommandoer som bare åpner et vindu 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/…/Merkelapp". Mellomliggende undermenyer lages ved behov.

set_item_checked gjør posten om til en avkryssbar menypost og setter krysset dens. Bruk den til varige av/på-brytere. Det første kallet gjør posten avkryssbar; de følgende kallene snur bare krysset. Gjør i stillhet ingenting ved ukjente id-er.

set_item_enabled nedtoner posten. Bruk den når kommandoen trenger en forutsetning, så en post som ikke er tilgjengelig, også ser slik ut, i stedet for ikke å gjøre noe når man klikker på den. Kan gjentas; poster begynner påslått; gjør i stillhet ingenting ved ukjente id-er.

ctx.events

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

Lett form Rått emne Innhold
@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 endring, hold den helt 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 per kjøring, etter det første bildet
@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 klikk i bokmerkekolonnen; biter i modifiers: mte.MODIFIER_*

Den rå formen:

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

Finn ikke på nye emner — den rå formen kontrollerer dem og kaster ValueError ved alt som 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 prefikset [python:<plugin.id>] i vertens diagnostikklogg.

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 må være "info" / "warning" / "error". timeout_ms=0 betyr at verten velger en standard. show() er til forbigående meldinger; alt som blir stående på skjermen (en levende telling, et tilstandsflagg), er et avsnitt — de to ødelegger aldri hverandre.

ctx.editor (fase 2)

Hver metode er synkron: den sperrer den kallende tråden. Det er greit fra et def-tilbakekall for en kommando eller en hendelse (arbeideren fordeler synkrone behandlere på en trådpulje); ulovlig fra async def-behandlere og fra ctx.completions-leverandører (som kjører direkte på hendelsesløkken).

Dokumenter vises det til med ugjennomsiktige 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økkel/verdi-lager avgrenset per programtillegg (ctx.settings) pluss en skrivebeskyttet visning av innstillingene for hele programmet (ctx.app_settings). Typen utledes av standardverdiens 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 må kontrolleres før int i din egen logikk når du leser (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. Gi en list[CompletionItem], alminnelige tekststrenger eller ordbøker tilbake. Tilbakekallet MÅ komme tilbake innen omtrent 30 ms, ellers kaster verten resultatene dine for det tastetrykket — ingen inn- og utdata, ingen ctx.editor.text(...), bare mellomlager. Bygg indekser andre steder (for eksempel ved document.opened).

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

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

Bruk data_dir() til mellomlagre og indekser. diagnostics_dir() er skrivebeskyttet (det er krasjrapporteringen som skriver der).

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

Bygg skjemabeskrivelsen med mte.ui-hjelperne. Ordforrådet av kontroller ligger fast: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List inne i Column / Row / Group.

  • binds="settings:<key>" — kontrollens verdi lagres av seg selv gjennom ctx.settings (samme lager som ctx.settings.set).
  • on_click="<cmd.id>" — bare knapper; kaller en registrert kommando.
  • on_change=<handler> — tildel den gjennom ctx.docks.on_change(callback); utløses ved endringer fra brukeren.

on_change-tilbakekall kjører på arbeiderens hendelsesløkke (IKKE på trådpuljen). Hold dem billige — ingen redigererkall, intet langvarig arbeid. Endre en liten tilstand i minnet, noter i loggen, eller utløs en kommando; gjør det tunge arbeidet fra kommandoen.

Gi initially_visible=False til et panel som ikke må sprette fram ved oppstart, og vis det så fra kommandoen dens med ctx.docks.set_panel_visible(panel_id, True) (som også løfter det fram; kan gjentas; ukjente id-er ses det bort fra). ctx.docks.is_panel_visible(panel_id) sier om et panel er åpent (en fane i bakgrunnen teller). Flere paneler som hører sammen: gi den samme tab_group="<plugin.id>/panels" og det samme area, så deler de én dokk med faner.

ctx.line_markers — merker 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

Registrer typene én gang i register(); redigereren har bare noen få plasser. Gjør bare krav på margklikket når programtillegget gir det en betydning — klikkene kommer uansett gjennom @ctx.events.on_margin_clicked.

ctx.languages — syntaksfarging

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 må navngi en leksikograf som alt finnes i backenden; styles og keywords bruker leksikografens egne navn (samme form som de medfølgende colorizers/*.json), aldri 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 settet for source-en din hver gang; det erstatter det forrige. source er påkrevd.

ctx.theme — farger for ditt eget grensesnitt som kan inngå 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 — sammenligning side ved side

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

ctx.events.publish

Bare workspace.rootChanged / workspace.filesChanged kan publiseres av et programtillegg; alt annet kaster ValueError. Det kan abonneres på hver vertshendelse unntatt document.aboutToSave (se listen over dekoratorer); den innebygde hendelsen DocumentAboutToClose har ikke noe Python-emne i det hele tatt — bruk document.closed.

ctx.settings_page — deklarative innstillingssider (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 av binds. To kontroller som deler et binds="settings:<key>", speiler hverandre med det samme — dokkpanelet og innstillingssiden kan begge ha en avkrysningsboks for den samme innstillingen, og snur du den ene, snur den andre med.

Forbehold i fase 3

Verdier lagres med det samme ved hver endring fra brukeren — Avbryt ruller ikke tilbake det brukeren har endret. Dette blir bedre i en senere forfining.

ctx.ui.get / ctx.ui.set — tilgang til panelkontroller under kjøringen (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 gir den viktigste verdien tilbake som en tekststreng:

  • Label / Button / LineEdit / ComboBox → teksten
  • Checkbox → "true" / "false"
  • NumberSpin → heltallet som desimaltall
  • List → teksten på den merkede posten (tom tekststreng om ingen)

Et set fra verten er taust: det utløser IKKE kontrollens on_change på nytt, så du kan oppdatere dine egne kontroller fra en kommandobehandler uten uendelige løkker.


5. Et fullstendig gjennomarbeidet eksempel — programtillegget wordcount

Kopier dette ordrett, og endre id, name og titlene så de passer til ditt tilfelle. Det bruker 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øpet for brukeren: åpne redigereren, lagre en fil tre ganger, og klikk så på Plugins → Word Count → Report save count. Statuslinjen viser 3 save(s) this session. Loggen noterer hvert steg.


6. Vanlige oppgaver — oppskrifter til å kopiere inn

Hver oppskrift er et utdrag som bare består av en kropp: lim det inn i def register(ctx):.

Å legge til en menykommando

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

Å reagere på lagring

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

Å advare ved avslutning

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

Å notere på et bestemt nivå

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

En skillelinje i menyen

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

Flere beslektede 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 varig av/på-bryter med et synlig kryss

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, en avkrysningsboks 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 innstillingsside som speiler et panels innstillinger

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 — gjør IKKE disse tingene

Galt:

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

Riktig:

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

register inne i en klasse

Galt:

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

Riktig:

def register(ctx):
    ...

async def register

Galt:

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

Riktig:

def register(ctx):
    ...

Tredjepartsimport uten å levere hjulet med

Galt:

import requests   # <-- ImportError on most user machines

Riktig (fase 1): hold deg til standardbiblioteket. Trenger du virkelig noe fra tredjepart, så lever hjulet inne i programtilleggsmappen, og utvid sys.path i de første linjene av main.py; se Pakking.

Qt-import

Galt:

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

Riktig: ingen Qt noe sted. Alt grensesnittet du kan bidra med, går gjennom ctx.menus, ctx.status, ctx.diagnostics, ctx.compare og de deklarative panel- og innstillingssidene. Aldri en rå kontroll.

Å sperre i en behandler

Galt:

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

Riktig (fase 1): overlat det til en tråd, og meld tilbake gjennom 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 menysti der det øverste leddet ikke står i plugin.json

Galt:

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

Riktig: hold stiens øverste ledd på linje med menus[]. Bruk "Plugins/Mine/Do" ovenfor.

Å lagre ctx til senere bruk

Galt:

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

Riktig: fang de tjenestene du trenger, i den lukningen som bruker dem. register er der ting bindes sammen; alt annet er en behandler som alt har tilgang gjennom lukningen sin.


8. Feilsøkingsliste for brukeren

Ta denne med til slutt i svaret ditt når du lager et programtillegg, så brukeren vet hva som bør kontrolleres om det ikke virker:

  1. Start redigereren fra et skall der en Python 3.9+-tolk ligger i PATH (eller der MTE_PYTHON peker på en).
  2. Se etter PluginHost: Python plugin backend enabled; interpreter=… i loggen. Mangler den, så gå til Feilsøking.
  3. Se etter [python:<your.id>] register() ran (om du fulgte wordcount-eksempelet) — det viser at register ble kjørt.
  4. Mangler menyposten, så se i loggen etter forklaringer av typen PythonPluginBackend: skipping '<id>': ….
  5. Skjer det ingenting når du klikker på menyen, så se i loggen etter [worker stderr] …-linjer — Pythons kallstakspor lander der.

9. Begrensninger som skal gjentas til slutt i hvert svar

Når du lager et programtillegg, så fortell alltid brukeren:

  • Installer det på en av to måter:
    • pakk mappens innhold som en zip ved navn <name>.mteplugin, og bruk Innstillinger → Programtillegg → Installer programtillegg... (som kontrollerer pakken før noe som helst røres), eller
    • legg mappen i redigererens programtilleggsmappe per bruker (%LOCALAPPDATA%/MTE/Plugins/ på Windows; ~/Library/Application Support/MTE/Plugins/ på macOS; ~/.local/share/MTE/Plugins/ på Linux).
  • Start redigereren på nytt.
  • Sett MTE_PYTHON om loggen sier «no Python interpreter found».
  • Meld [python:<id>] …-loggelinjen tilbake, så vi kan bekrefte at det kjørte.

10. Kilden til sannhet

Er det noe på denne siden som strider mot det Python-klientpakken faktisk gjør, vinner pakken. Den leveres ved siden av redigereren i <exe>/Python/mte/ — les den kildekoden før du avgjør en oppførsel. C++-siden av protokollen ligger i Components/PluginHost/Src/PyServiceBridge.cpp, og formatet på ledningen er beskrevet i Python/PROTOCOL.md.