Å 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):
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¶
"apiVersion"må være1(vertens gjeldende versjon)."entry"må være"python"— ingenting annet går til denne backenden."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)."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å brukorg.example..menus[]må inneholde minst én post dertitlepasser til det ytterste venstre leddet i hver menysti du bruker imain.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ærpyRequiresnå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å0med 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¶
register(ctx)MÅ være en funksjon på øverste nivå. Den statiske kontrollen krever en linje som begynner meddef register(i kolonne 0. Nøster du den inne i en klasse, dekorerer den på modulnivå eller gjør den til enasync def, avvises programtillegget i stillhet ved oppdagelsen.- Importer fra
mtenår du abonnerer på hendelser — de typede dataklassene (mte.DocumentSavedog så videre) kommer fra den pakken.import mteøverst er trygt også om du ikke bruker den direkte;# noqa: F401ovenfor hindrer støy fra kodekontrollen. - Bruk aldri
print(). Arbeiderens stdout er RPC-kanalen; å skrive til den ødelegger protokollen. Brukctx.log.info / warning / error. - Importer aldri noe du ikke har erklært. Utover standardbiblioteket og
mtemå hver tredjepartsimport stå ipyRequiresiplugin.json(redigereren installerer den med pip i programtilleggets virtuelle miljø ved første oppstart) eller ligge inne i programtilleggsmappen. En uerklært import girModuleNotFoundErrorpå brukerens maskin. - Kommando- og hendelsesbehandlere kan gjerne sperre kort — de kjører på arbeiderens
trådpulje, så synkrone kall inn i
ctx.editor,ctx.settingsogctx.data_direr 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 ictx.editor.*, bare mellomlager. Bygg indekser andre steder (for eksempel veddocument.opened), og svar fra en ordbok. - Importer ikke Qt, PySide, PyQt eller Scintilla. De er ikke tilgjengelige i arbeideren.
- Kommando-id-er må ha et prefiks med programtilleggets navnerom for å unngå
kollisjoner med kjernen og andre programtillegg.
<id>.<verb>ovenfor; for eksempelwordcount.countellerhello.sayHi. - Menystier må begynne med en øverste meny du også har erklært i
menus[]iplugin.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¶
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)¶
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 gjennomctx.settings(samme lager somctx.settings.set).on_click="<cmd.id>"— bare knapper; kaller en registrert kommando.on_change=<handler>— tildel den gjennomctx.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.
{
"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ø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¶
Å 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¶
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¶
print() til diagnostikk¶
Galt:
Riktig:
register inne i en klasse¶
Galt:
Riktig:
async def register¶
Galt:
Riktig:
Tredjepartsimport uten å levere hjulet med¶
Galt:
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:
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:
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:
- Start redigereren fra et skall der en Python 3.9+-tolk ligger i
PATH(eller derMTE_PYTHONpeker på en). - Se etter
PluginHost: Python plugin backend enabled; interpreter=…i loggen. Mangler den, så gå til Feilsøking. - Se etter
[python:<your.id>] register() ran(om du fulgtewordcount-eksempelet) — det viser atregisterble kjørt. - Mangler menyposten, så se i loggen etter forklaringer av typen
PythonPluginBackend: skipping '<id>': …. - 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).
- pakk mappens innhold som en zip ved navn
- Start redigereren på nytt.
- Sett
MTE_PYTHONom 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.