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):
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¶
"apiVersion"måste vara1(värdens aktuella version)."entry"måste vara"python"— inget annat leder till den här backenden."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)."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ändorg.example..menus[]måste innehålla minst en post varstitlestämmer med det vänstraste segmentet i varje menysökväg du använder imain.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); deklarerapyRequiresnä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 vara0om 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¶
register(ctx)MÅSTE vara en funktion på toppnivå. Den statiska sonden kräver en rad som börjar meddef register(på kolumn 0. Nästlar du den i en klass, dekorerar den på modulnivå, eller gör den till enasync def, avvisas modulen tyst vid upptäckten.- Importera från
mtenä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: F401ovan undviker brus från lintern. - Använd aldrig
print(). Arbetarens stdout är RPC-kanalen; att skriva till den förstör protokollet. Användctx.log.info / warning / error. - Importera aldrig något du inte deklarerat. Utöver standardbiblioteket och
mtemåste varje tredjepartsimport finnas ipyRequiresiplugin.json(redigeraren pip-installerar den i modulens venv vid första starten) eller levereras inuti modulmappen. En odeklarerad import kastarModuleNotFoundErrorpå användarens maskin. - 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 tillctx.editor.*, bara cache. Bygg index någon annanstans (till exempel viddocument.opened) och svara från en ordbok. - Importera inte Qt, PySide, PyQt eller Scintilla. De finns inte i arbetaren.
- 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 exempelwordcount.count,hello.sayHi. - Menysökvägar måste börja med en toppnivåmeny som du också deklarerat i
menus[]iplugin.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¶
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)¶
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 genomctx.settings(samma lager somctx.settings.set).on_click="<cmd.id>"— endast för Button; anropar ett registrerat kommando.on_change=<handler>— tilldela ett genomctx.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.
{
"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 }
]
}
"""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¶
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¶
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¶
print() för diagnostik¶
Fel:
Rätt:
register inuti en klass¶
Fel:
Rätt:
async def register¶
Fel:
Rätt:
Tredjepartsimporter utan att leverera hjulet¶
Fel:
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:
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:
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:
- Starta redigeraren från ett skal där en Python-tolk 3.9+ finns i
PATH(eller därMTE_PYTHONpekar på en). - Leta efter
PluginHost: Python plugin backend enabled; interpreter=…i loggen. Saknas den, hoppa till Felsökning. - Leta efter
[python:<your.id>] register() ran(om du följde exempletwordcount) — det visar attregisterkördes. - Saknas menyposten, leta i loggen efter förklaringarna
PythonPluginBackend: skipping '<id>': …. - 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>.mtepluginoch 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).
- komprimera mappens innehåll som
- Starta om redigeraren.
- Sätt
MTE_PYTHONom 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.