Gå till innehållet

Skriva en Python-insticksmodul för Modular Text Editor med Claude

Målgrupp: en AI-assistent (Claude / ChatGPT / med flera). Den här sidan är en självförsörjande instruktionsuppsättning: lämna den till assistenten tillsammans med uppgiften («skriv en insticksmodul åt mig som gör X») så ska assistenten producera en komplett, korrekt, fungerande insticksmodulsmapp utan att behöva läsa resten av dokumentationen.

Är du en människa som försöker skriva en insticksmodul för hand, börja här i stället — den här sidan är optimerad för AI-konsumtion och hoppar över de vänliga exemplen.


1. Vad du producerar

En mapp med exakt två filer (plus valfria extrafiler):

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

Leverera mappen som ett kodblock per fil. Användaren lägger antingen mappen i redigerarens insticksmodulskatalog, eller komprimerar mappen och döper om arkivet till <name>.mteplugin och installerar det via Inställningar → Insticksmoduler → Installera insticksmodul.... Båda vägarna kräver en omstart för att läsas in.

De två filerna beskrivs uttömmande nedan. Följ reglerna bokstavligt — värden kör en statisk sond och avvisar insticksmoduler som bryter mot dem.

2. plugin.json — kanonisk mall

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

Hårda regler

  1. "apiVersion" måste vara 1 (värdens aktuella version).
  2. "entry" måste vara "python" — inget annat leder till den här backenden.
  3. "module" måste vara "main" om det inte finns ett uttryckligt skäl att namnge modulen annorlunda — och aldrig en sökväg med punkter (fas 1 stöder bara platta modulnamn).
  4. "id" måste vara i omvänd DNS-stil (org.<vendor>.<name>) och unik globalt bland användarens installerade insticksmoduler. Har användaren inte angett någon leverantör, använd org.example..
  5. menus[] måste innehålla minst en post vars title stämmer med det vänstraste segmentet i varje menysökväg du använder i main.py. Föredra "Plugins" för allt som vetter mot användaren och inte uppenbart hör hemma i en kärnmeny.

Valfria fält

  • "pyRequires": [] — en vektor med pip-kravsträngar (till exempel ["requests>=2.31"]). När den inte är tom ställer redigeraren i ordning en venv per insticksmodul vid första starten (pip install — kräver nätverk den enda gången) och kör modulen under den. Föredra standardbiblioteket när det räcker (ingen fördröjning för iordningställandet, fungerar utan nätverk); deklarera pyRequires när uppgiften verkligen behöver ett tredjepartspaket, och säg till användaren att första starten kommer att pausa för att installera det.
  • "permissions": [] — grova deklarationer av förmågor (till exempel ["network"]), visade skrivskyddade på sidan Insticksmoduler. Deklarera "network" när modulen gör utgående anrop; det är en ärlig märkning för användaren, inte ett upprätthållande.
  • "requires": [] — id:n för ANDRA insticksmoduler som den här inte kan fungera utan (inte pip-paket). De startas först; om någon saknas eller är avstängd startar den här modulen inte, och sidan Insticksmoduler talar om varför. Lämna den tom om inte användarens uppgift verkligen beror på en annan modul.
  • "apiMinorVersion": 0 — den lägsta minorversion av redigerarens API som modulen behöver. Låt den vara 0 om du inte använder ett anrop som är dokumenterat som nyare.

Värden du INTE ska hitta på

  • Varje fält som inte listas ovan. Värden ignorerar okända nycklar; att lägga till dem är en underhållsrisk.

3. main.py — kanoniskt skelett

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

Hårda regler

  1. register(ctx) MÅSTE vara en funktion på toppnivå. Den statiska sonden kräver en rad som börjar med def register( på kolumn 0. Nästlar du den i en klass, dekorerar den på modulnivå, eller gör den till en async def, avvisas modulen tyst vid upptäckten.
  2. Importera från mte när du prenumererar på händelser — de typade dataklasserna (mte.DocumentSaved, med flera) kommer från det paketet. import mte överst är säkert även om du inte använder det direkt; # noqa: F401 ovan undviker brus från lintern.
  3. Använd aldrig print(). Arbetarens stdout är RPC-kanalen; att skriva till den förstör protokollet. Använd ctx.log.info / warning / error.
  4. Importera aldrig något du inte deklarerat. Utöver standardbiblioteket och mte måste varje tredjepartsimport finnas i pyRequires i plugin.json (redigeraren pip-installerar den i modulens venv vid första starten) eller levereras inuti modulmappen. En odeklarerad import kastar ModuleNotFoundError på användarens maskin.
  5. Hanterare för kommandon och händelser får blockera kort — de körs på arbetarens trådpool, så synkrona anrop till ctx.editor / ctx.settings / ctx.data_dir är tillåtna. Men återanrop från kompletteringsleverantörer (@ctx.completions.provider(...)) FÅR INTE blockera: de körs på händelseloopen med en deadline på ~30 ms. Ingen I/O, inga anrop till ctx.editor.*, bara cache. Bygg index någon annanstans (till exempel vid document.opened) och svara från en ordbok.
  6. Importera inte Qt, PySide, PyQt eller Scintilla. De finns inte i arbetaren.
  7. Kommando-id:n måste föregås av din insticksmoduls namnrymd för att undvika krockar med kärnan eller andra moduler. <id>.<verb> ovan; till exempel wordcount.count, hello.sayHi.
  8. Menysökvägar måste börja med en toppnivåmeny som du också deklarerat i menus[] i plugin.json. Konvention: "Plugins/<Plugin Name>/<Verb>".

4. Fullständigt API — allt på ctx

Du har arton tjänster: commands, menus, events, log, status, editor, languages, settings, app_settings, document_settings, theme, diagnostics, compare, completions, docks, settings_page, ui, och data_dir()/diagnostics_dir(). Allt som inte listas här finns inte — hitta inte på tjänster. (Venvar per insticksmodul deklareras genom pyRequires, inte med en tjänst i ctx; protokollgrunden med strömning och avbrott har ännu inget API som vetter mot insticksmoduler.)

ctx.commands

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

Dekorator. Registrerar ett kommando och binder _handler (en funktion utan argument som returnerar None). Undantag i _handler fångas av arbetaren och skrivs till stderr; de kraschar inte redigeraren.

Sätt requires_editor när kommandot läser det aktiva dokumentet (buffert, markering, markör) och requires_writable_editor när det ändrar texten. Värden gråtonar då kommandot där det inte skulle kunna fungera — i en flik utan dokument (en jämförelse eller en loggvy) och, för skrivflaggan, i ett skrivskyddat dokument — i stället för att låta användaren anropa en tyst nulloperation. Skrivflaggan medför den andra; lämna båda av för kommandon som bara öppnar en dialogruta eller en 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 är "Top/Sub/…/Label". Mellanliggande undermenyer skapas på begäran.

set_item_checked gör posten till en kryssbar menypost och sätter dess kryss. Använd det för bestående på/av-växlar. Första anropet befordrar; senare anrop vänder bara. Tyst nulloperation vid okända id:n.

set_item_enabled gråtonar posten. Använd det när kommandot behöver ett villkor, så att en otillgänglig post ser otillgänglig ut i stället för att inte göra något vid klick. Idempotent; poster börjar aktiverade; tyst nulloperation vid okända id:n.

ctx.events

Sjutton ämnen, vart och ett med en sockerdekorator och en rå form.

Socker Rått ämne Innehåll
@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) — vid varje ändring, håll det trivialt
@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() — en gång per körning, efter den första bilden
@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) — ett klick i bokmärkeskolumnen; bitar i modifiers: mte.MODIFIER_*

Rå form:

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

Hitta inte på nya ämnen — den råa formen validerar dem och kastar ValueError för allt som inte 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)

Avfyra-och-glöm. Rader föregås av [python:<plugin.id>] i värdens diagnostiklogg.

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åste vara "info" / "warning" / "error". timeout_ms=0 betyder att värden väljer ett standardvärde. show() är för flyktiga aviseringar; allt som stannar på skärmen (en levande räkning, en lägesflagga) är ett segment — de två skriver aldrig över varandra.

ctx.editor (fas 2)

Varje metod är synkron: den blockerar den anropande tråden. Går bra från ett återanrop med def för ett kommando eller en händelse (arbetaren fördelar synkrona hanterare på en trådpool); otillåtet från hanterare med async def och från leverantörer i ctx.completions (som körs direkt på händelseloopen).

Dokument refereras med ogenomskinliga int-id:n (0 = inget 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 (fas 2)

Typad nyckel/värde-lagring begränsad per insticksmodul (ctx.settings) plus en skrivskyddad vy av programövergripande inställningar (ctx.app_settings). Typen härleds från standardvärdets Python-typ.

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 kastar RuntimeError. I din egen logik måste du kontrollera booleanen före heltalet vid läsning (i Python är isinstance(True, int) == True).

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

Fält på req: document, prefix, position, language_id, manual. Sorter: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Returnera list[CompletionItem], vanliga strängar eller ordböcker. Återanropet MÅSTE återvända inom ~30 ms, annars släpper värden dina resultat för den tangenttryckningen — ingen I/O, ingen ctx.editor.text(...), bara cache. Bygg index någon annanstans (till exempel vid document.opened).

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

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

Använd data_dir() för cachar och index. diagnostics_dir() är skrivskyddad (kraschrapportören skriver dit).

ctx.docks — deklarativa dockpaneler (fas 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 formulärspecifikationen med hjälparna i mte.ui. Kontrollvokabulären är fast: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List inuti Column / Row / Group.

  • binds="settings:<key>" — kontrollens värde sparas automatiskt genom ctx.settings (samma lager som ctx.settings.set).
  • on_click="<cmd.id>" — endast för Button; anropar ett registrerat kommando.
  • on_change=<handler> — tilldela ett genom ctx.docks.on_change(callback); utlöses vid användarändringar.

Återanrop för on_change körs på arbetarens händelseloop (INTE på trådpoolen). Håll dem billiga — inga redigeraranrop, inget långt arbete. Ändra ett litet tillstånd i minnet, notera något, eller sätt i gång ett kommando; gör tungt arbete från kommandot.

Skicka initially_visible=False för en panel som inte ska dyka upp vid start, och visa den sedan från dess kommando med ctx.docks.set_panel_visible(panel_id, True) (det lyfter också fram den; är idempotent; okända id:n ignoreras). ctx.docks.is_panel_visible(panel_id) talar om ifall en panel är öppen (en flik i bakgrunden räknas). Flera paneler som hör ihop: skicka samma tab_group="<plugin.id>/panels" och samma area, så delar de en docka med flikar.

ctx.line_markers — märken på dokumentrader

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

Registrera typerna en gång i register(); redigeraren har bara några få platser. Gör bara anspråk på marginalklicket när modulen ger det en betydelse — klicken kommer via @ctx.events.on_margin_clicked i vilket fall som helst.

ctx.languages — syntaxfärgning

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åste namnge en befintlig lexer i backenden; styles och keywords använder lexerns egna namn (samma form som de medföljande colorizers/*.json), aldrig numeriska id:n.

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 — våglinjer

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

Skicka HELA mängden för din source varje gång; den ersätter den föregående. source är obligatorisk.

ctx.theme — temabara färger för ditt eget gränssnitt

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 sida vid sida

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

ctx.events.publish

Bara workspace.rootChanged / workspace.filesChanged får publiceras av en insticksmodul; allt annat kastar ValueError. Varje värdhändelse utom document.aboutToSave går att prenumerera på (se dekoratorlistan); den inbyggda händelsen DocumentAboutToClose har inget Python-ämne alls — använd document.closed.

ctx.settings_page — deklarativa Inställningssidor (fas 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"),
    ]),
)

Samma byggare, samma semantik för binds. Två kontroller som delar ett binds="settings:<key>" speglar varandra i realtid — dockpanelen och Inställningssidan kan båda bära en kryssruta för samma inställning, och att växla den ena vänder den andra.

Förbehåll i fas 3

Värden sparas ivrigt vid varje användarändring — Avbryt återställer inte det användaren ändrat. Det förbättras i en framtida förfining.

ctx.ui.get / ctx.ui.set — åtkomst till panelkontroller under körning (fas 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 returnerar det primära värdet som en sträng:

  • Label / Button / LineEdit / ComboBox → texten
  • Checkbox → "true" / "false"
  • NumberSpin → heltalet som decimal
  • List → texten i den just nu markerade posten (tom sträng om ingen finns)

En set som drivs av värden är tyst: den utlöser INTE kontrollens on_change på nytt, så du kan uppdatera dina egna widgetar från en kommandohanterare utan oändliga loopar.


5. Fullständigt utarbetat exempel — insticksmodulen wordcount

Kopiera detta ordagrant och ändra id, name och title så att de passar ditt fall. Det använder varje tjänst från fas 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")

Förväntat användarflöde: öppna redigeraren, spara en fil tre gånger, och klicka sedan på Plugins → Word Count → Report save count. Statusraden visar 3 save(s) this session. Loggen noterar varje steg.


6. Vanliga uppgifter — recept att kopiera och klistra in

Varje recept är ett utdrag med bara kroppen: klistra in det inuti def register(ctx):.

Lägga till ett 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")

Reagera på sparande

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

Varna vid avslut

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

Notera på en viss nivå

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

Menyavskiljare

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

Flera besläktade kommandon

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

Bestående på/av-växel med ett synligt 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)

Sidopanel med en textinmatning, en kryssruta och två knappar

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

Inställningssida som speglar en dockas inställningar

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önster — gör INTE det här

Fel:

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

Rätt:

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

register inuti en klass

Fel:

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

Rätt:

def register(ctx):
    ...

async def register

Fel:

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

Rätt:

def register(ctx):
    ...

Tredjepartsimporter utan att leverera hjulet

Fel:

import requests   # <-- ImportError on most user machines

Rätt (fas 1): håll dig till standardbiblioteket. Behöver du verkligen något från tredje part, leverera hjulet inuti modulmappen och utöka sys.path på de första raderna i main.py; se Paketering.

Qt-importer

Fel:

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

Rätt: ingen Qt någonstans. Allt gränssnitt du kan bidra med går genom ctx.menus, ctx.status, ctx.diagnostics, ctx.compare och de deklarativa dock- och inställningspanelerna. Aldrig en naken widget.

Att blockera i en hanterare

Fel:

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

Rätt (fas 1): lämna över till en tråd och kommunicera genom 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")

Menysökväg vars översta segment inte finns i plugin.json

Fel:

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

Rätt: håll toppnivåsegmentet i sökvägen i linje med menus[]. Använd "Plugins/Mine/Do" ovan.

Att spara ctx för senare bruk

Fel:

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

Rätt: fånga de tjänster du behöver i den closure som använder dem. register är där kopplingen sker; allt annat är en hanterare som redan har åtkomst genom sin closure.


8. Felsökningschecklista för användaren

Ta med den här i slutet av ditt svar när du producerar en insticksmodul, så att användaren vet vad som ska kontrolleras om den inte fungerar:

  1. Starta redigeraren från ett skal där en Python-tolk 3.9+ finns i PATH (eller där MTE_PYTHON pekar på en).
  2. Leta efter PluginHost: Python plugin backend enabled; interpreter=… i loggen. Saknas den, hoppa till Felsökning.
  3. Leta efter [python:<your.id>] register() ran (om du följde exemplet wordcount) — det visar att register kördes.
  4. Saknas menyposten, leta i loggen efter förklaringarna PythonPluginBackend: skipping '<id>': ….
  5. Gör ett klick i menyn inget, leta i loggen efter rader [worker stderr] … — Python-spårningar hamnar där.

9. Villkor att upprepa i slutet av varje svar

När du producerar en insticksmodul, säg alltid till användaren:

  • Installera den på endera sättet:
    • komprimera mappens innehåll som <name>.mteplugin och använd Inställningar → Insticksmoduler → Installera insticksmodul... (det validerar paketet innan något rörs), eller
    • lägg mappen i redigerarens insticksmodulskatalog per användare (%LOCALAPPDATA%/MTE/Plugins/ på Windows; ~/Library/Application Support/MTE/Plugins/ på macOS; ~/.local/share/MTE/Plugins/ på Linux).
  • Starta om redigeraren.
  • Sätt MTE_PYTHON om loggen säger «no Python interpreter found».
  • Rapportera tillbaka loggraden [python:<id>] … så att vi kan bekräfta att den kördes.

10. Sanningskälla

Står något påstående på den här sidan i strid med vad Python-klientpaketet faktiskt implementerar, vinner paketet. Det levereras bredvid redigeraren i <exe>/Python/mte/ — läs den källkoden innan du avgör ett beteende. C++-sidan av protokollet finns i Components/PluginHost/Src/PyServiceBridge.cpp och protokollspecifikationen i Python/PROTOCOL.md.