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):
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¶
"apiVersion"skal være1(værtens nuværende version)."entry"skal være"python"— intet andet går til denne backend."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)."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å brugorg.example..menus[]skal indeholde mindst én post, hvistitlepasser til det yderste venstre led i hver menusti, du bruger imain.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ærpyRequires, 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¶
register(ctx)SKAL være en funktion på øverste niveau. Den statiske efterprøvning kræver en linje, der begynder meddef register(i kolonne 0. Indlejrer du den i en klasse, dekorerer den på modulniveau eller gør den til enasync def, afvises udvidelsesmodulet i stilhed ved opdagelsen.- Importér fra
mte, når du abonnerer på hændelser — de typede dataklasser (mte.DocumentSavedog så videre) kommer fra den pakke.import mteøverst er trygt, også selv om du ikke bruger den direkte;# noqa: F401ovenfor forhindrer støj fra kodekontrollen. - Brug aldrig
print(). Arbejderens stdout er RPC-kanalen; at skrive til den ødelægger protokollen. Brugctx.log.info / warning / error. - Importér aldrig noget, du ikke har erklæret. Ud over standardbiblioteket og
mteskal hver tredjepartsimport stå ipyRequiresiplugin.json(editoren installerer den med pip i modulets virtuelle miljø ved første start) eller ligge inde i modulmappen. En uerklæret import giverModuleNotFoundErrorpå brugerens maskine. - 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.settingsogctx.data_direr 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 ictx.editor.*, kun mellemlager. Byg indekser andetsteds (f.eks. veddocument.opened), og svar fra en ordbog. - Importér ikke Qt, PySide, PyQt eller Scintilla. De er ikke tilgængelige i arbejderen.
- 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.countellerhello.sayHi. - Menustier skal begynde med en øverste menu, du også har erklæret i
menus[]iplugin.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¶
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)¶
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 gennemctx.settings(samme lager somctx.settings.set).on_click="<cmd.id>"— kun knapper; kalder en registreret kommando.on_change=<handler>— tildel den viactx.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.
{
"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")
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¶
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¶
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¶
print() til diagnostik¶
Forkert:
Rigtigt:
register inde i en klasse¶
Forkert:
Rigtigt:
async def register¶
Forkert:
Rigtigt:
Tredjepartsimport uden at levere hjulet med¶
Forkert:
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:
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:
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:
- Start editoren fra en skal, hvor en Python 3.9+-fortolker ligger i
PATH(eller hvorMTE_PYTHONpeger på en). - Se efter
PluginHost: Python plugin backend enabled; interpreter=…i loggen. Mangler den, så gå til Fejlfinding. - Se efter
[python:<your.id>] register() ran(hvis du fulgtewordcount-eksemplet) — det viser, atregisterblev kørt. - Mangler menupunktet, så se i loggen efter forklaringer af typen
PythonPluginBackend: skipping '<id>': …. - 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).
- pak mappens indhold som en zip ved navn
- 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.