Tvorba zásuvného modulu Modular Text Editor v Pythone s Claudom¶
Publikum: asistent AI (Claude, ChatGPT a podobne). Táto stránka je samostatnou sadou pokynov: odovzdajte ju asistentovi spolu s úlohou („napíš mi zásuvný modul, ktorý robí X“) a asistent by mal vytvoriť úplný, správny a fungujúci priečinok zásuvného modulu bez toho, aby musel čítať zvyšok dokumentácie.
Ak ste človek, ktorý sa pokúša napísať zásuvný modul ručne, začnite tu — táto stránka je optimalizovaná pre AI a vynecháva priateľské príklady.
1. Čo vytvárate¶
Priečinok s presne dvoma súbormi (plus voliteľné doplnky):
Priečinok vráťte ako blok kódu na každý súbor. Používateľ buď priečinok vloží do priečinka
zásuvných modulov editora, alebo priečinok zabalí do zipu, archív premenuje na
<name>.mteplugin a nainštaluje ho cez Predvoľby → Zásuvné moduly → Nainštalovať zásuvný modul.... Obidve
cesty vyžadujú na načítanie reštart.
Obidva súbory sú vyčerpávajúco opísané nižšie. Pravidlá dodržte doslovne — hostiteľ vykonáva statickú kontrolu a zásuvné moduly, ktoré ich porušujú, odmieta.
2. plugin.json — kanonická šablóna¶
{
"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 }
]
}
Tvrdé pravidlá¶
"apiVersion"musí byť1(aktuálna verzia hostiteľa)."entry"musí byť"python"— nič iné na tento backend nesmeruje."module"musí byť"main", pokiaľ neexistuje výslovný dôvod pomenovať modul inak — a nikdy to nesmie byť bodková cesta (fáza 1 podporuje iba ploché názvy modulov)."id"musí byť v štýle reverzného DNS (org.<vendor>.<name>) a musí byť globálne jedinečné naprieč nainštalovanými modulmi používateľa. Ak používateľ neuviedol dodávateľa, použiteorg.example..menus[]musí obsahovať aspoň jednu položku, ktorejtitlezodpovedá najľavejšiemu segmentu každej cesty v ponuke, ktorú použijete vmain.py. Pri čomkoľvek určenom používateľovi, čo zjavne nepatrí do ponuky jadra, uprednostnite"Plugins".
Voliteľné polia¶
"pyRequires": []— pole reťazcov s požiadavkami pip (napr.["requests>=2.31"]). Keď nie je prázdne, editor pri prvom spustení pripraví pre daný modul virtuálne prostredie (pip install— ten jeden raz potrebuje sieť) a modul pod ním spustí. Keď úlohu zvládne štandardná knižnica, uprednostnite ju (žiadne zdržanie prípravou, funguje bez siete);pyRequiresdeklarujte, keď úloha skutočne potrebuje balík tretej strany, a používateľovi povedzte, že prvé spustenie sa pozastaví kvôli inštalácii."permissions": []— hrubé deklarácie schopností (napr.["network"]), zobrazené iba na čítanie na stránke Zásuvné moduly. Deklarujte"network", keď modul robí volania von; je to poctivé označenie pre používateľa, nie vynucovanie."requires": []— ID INÝCH zásuvných modulov, bez ktorých tento nemôže fungovať (nie balíky pip). Spúšťajú sa prvé; keď niektorý chýba alebo je vypnutý, tento modul sa nespustí a stránka Zásuvné moduly povie prečo. Nechajte prázdne, pokiaľ úloha používateľa naozaj nezávisí od iného modulu."apiMinorVersion": 0— najnižšia minor verzia API editora, ktorú modul potrebuje. Nechajte0, pokiaľ nepoužívate volanie zdokumentované ako novšie.
Hodnoty, ktoré si NEMÁTE vymýšľať¶
- Akékoľvek pole, ktoré nie je uvedené vyššie. Hostiteľ neznáme kľúče ignoruje; ich pridávanie je rizikom pri údržbe.
3. main.py — kanonická kostra¶
"""<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>")
Tvrdé pravidlá¶
register(ctx)MUSÍ byť funkciou na najvyššej úrovni. Statická kontrola vyžaduje riadok začínajúci sadef register(v stĺpci 0. Ak ju vnoríte do triedy, dekorujete na úrovni modulu alebo z nej urobíteasync def, modul sa pri vyhľadávaní potichu odmietne.- Keď sa prihlasujete na odber udalostí, importujte z
mte— typované dátové triedy (mte.DocumentSaveda podobne) pochádzajú z tohto balíka.import mtenavrchu je bezpečný, aj keď ho priamo nepoužívate;# noqa: F401vyššie zabráni šumu z lintera. - Nikdy nepoužívajte
print(). Stdout pracovného procesu je kanálom RPC; zápis doň protokol poškodí. Použitectx.log.info / warning / error. - Nikdy neimportujte nič, čo ste nedeklarovali. Okrem štandardnej knižnice a
mtesa musí každý import tretej strany objaviť v polipyRequiressúboruplugin.json(editor ho pri prvom spustení nainštaluje cez pip do virtuálneho prostredia modulu) alebo musí byť vložený vnútri priečinka modulu. Nedeklarovaný import vyvolá na počítači používateľaModuleNotFoundError. - Obslužný kód príkazov a udalostí môže krátko blokovať — beží v zásobe vlákien pracovného
procesu, takže synchrónne volania do
ctx.editor,ctx.settingsčictx.data_dirsú v poriadku. Ale zavolania poskytovateľa dopĺňania (@ctx.completions.provider(...)) NESMÚ blokovať: bežia v slučke udalostí s termínom ~30 ms. Žiadne vstupno-výstupné operácie, žiadne volania doctx.editor.*, iba z vyrovnávacej pamäte. Indexy budujte inde (napríklad pridocument.opened) a odpovedajte zo slovníka. - Neimportujte Qt, PySide, PyQt ani Scintillu. V pracovnom procese nie sú k dispozícii.
- Id príkazov musia mať predponu s menným priestorom vášho modulu, aby nekolidovali s jadrom
ani s inými modulmi. Vyššie
<id>.<verb>; napríkladwordcount.count,hello.sayHi. - Cesty v ponukách sa musia začínať hlavnou ponukou, ktorú ste zároveň deklarovali
v
menus[]súboruplugin.json. Zvyklosť:"Plugins/<Plugin Name>/<Verb>".
4. Úplné API — všetko na ctx¶
Máte k dispozícii osemnásť služieb: commands, menus, events, log, status, editor, languages,
settings, app_settings, document_settings, theme, diagnostics, compare, completions, docks,
settings_page, ui a data_dir()/diagnostics_dir(). Čokoľvek, čo tu nie je uvedené, neexistuje —
nevymýšľajte si služby. (Virtuálne prostredia pre jednotlivé moduly sa deklarujú cez
pyRequires, nie ako služba ctx; podklad protokolu na prúdenie a rušenie zatiaľ nemá žiadne
API smerom k modulom.)
ctx.commands¶
@ctx.commands.command(*, id, title, shortcut="", category="",
requires_editor=False, requires_writable_editor=False)
def _handler(): ...
Dekorátor. Zaregistruje príkaz a naviaže _handler (funkciu bez argumentov vracajúcu None).
Výnimky v _handler zachytí pracovný proces a zapíše ich na stderr; editor nezhavarujú.
Nastavte requires_editor, keď príkaz číta aktívny dokument (buffer, výber, kurzor),
a requires_writable_editor, keď mení text. Hostiteľ potom príkaz zosivie tam, kde by nemohol
fungovať — na karte bez dokumentu (v zobrazení porovnania alebo logu) a pri príznaku
zapisovateľnosti aj na dokumente iba na čítanie — namiesto toho, aby používateľ vyvolal tichú
prázdnu operáciu. Príznak zapisovateľnosti zahŕňa ten druhý; pri príkazoch, ktoré iba otvárajú
dialóg alebo panel, nechajte obidva vypnuté.
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 je "Top/Sub/…/Label". Medziľahlé podponuky sa vytvárajú na požiadanie.
set_item_checked premení položku na začiarkavaciu a nastaví jej značku. Použite to pri trvalých
prepínačoch zapnuté/vypnuté. Prvé volanie povyšuje; ďalšie už iba prepínajú. Pri neznámych id ide
o tichú prázdnu operáciu.
set_item_enabled položku zosivie. Použite to, keď príkaz potrebuje splnenú podmienku, aby
nedostupná položka aj vyzerala nedostupne, namiesto aby po kliknutí neurobila nič. Je
idempotentné; položky sú na začiatku zapnuté; pri neznámych id ide o tichú prázdnu operáciu.
ctx.events¶
Sedemnásť tém, každá s pohodlným dekorátorom a surovým tvarom.
| Pohodlný tvar | Surová téma | Obsah |
|---|---|---|
@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) — každá úprava, držte to triviálne |
@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() — raz za spustenie, po prvej snímke |
@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) — kliknutie do stĺpca záložiek; bity modifiers: mte.MODIFIER_* |
Surový tvar:
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
Nevymýšľajte si nové témy — surový tvar ich overuje a pri čomkoľvek mimo tabuľky vyvolá
ValueError.
ctx.log¶
Odošlete a ďalej sa nestaráte. Riadky majú v diagnostickom logu hostiteľa predponu
[python:<plugin.id>].
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 musí byť "info" / "warning" / "error". timeout_ms=0 znamená, že predvoľbu zvolí
hostiteľ. show() je určené na dočasné upozornenia; čokoľvek, čo ostáva na obrazovke (živý počet,
príznak režimu), je segment — tieto dva sa navzájom nikdy neprebíjajú.
ctx.editor (fáza 2)¶
Každá metóda je synchrónna: blokuje volajúce vlákno. Z obslužného kódu príkazu či udalosti
typu def je to v poriadku (pracovný proces posiela synchrónny obslužný kód do zásoby vlákien);
je to neprípustné z obslužného kódu async def a z poskytovateľov ctx.completions (ktorí
bežia priamo v slučke udalostí).
Dokumenty sa adresujú nepriehľadnými celočíselnými id (0 = žiadny 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 (fáza 2)¶
Typované úložisko kľúč/hodnota s rozsahom modulu (ctx.settings) plus pohľad na nastavenia celej
aplikácie iba na čítanie (ctx.app_settings). Typ sa odvodí z typu predvolenej hodnoty
v Pythone.
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 vyvolajú RuntimeError. Vo vlastnej logike musíte pri čítaní
kontrolovať bool skôr než int (v Pythone platí isinstance(True, int) == True).
ctx.completions (fáza 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)]
Polia req: document, prefix, position, language_id, manual. Druhy:
KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Vráťte
list[CompletionItem], obyčajné reťazce alebo slovníky. Zavolanie sa MUSÍ vrátiť do ~30 ms,
inak hostiteľ vaše výsledky pri tomto stlačení klávesu zahodí — žiadne vstupno-výstupné operácie,
žiadne ctx.editor.text(...), iba z vyrovnávacej pamäte. Indexy budujte inde (napríklad pri
document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (fáza 2)¶
data_dir() používajte na vyrovnávacie pamäte a indexy. diagnostics_dir() je iba na čítanie
(zapisuje doň nástroj na hlásenie pádov).
ctx.docks — deklaratívne ukotviteľné panely (fáza 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")]),
]),
)
Špecifikáciu formulára zostavte pomocníkmi z mte.ui. Slovník ovládacích prvkov je
pevne daný: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List vnútri
Column / Row / Group.
binds="settings:<key>"— hodnota ovládacieho prvku sa automaticky uchová cezctx.settings(to isté úložisko akoctx.settings.set).on_click="<cmd.id>"— iba pri tlačidlách; vyvolá zaregistrovaný príkaz.on_change=<handler>— pridelíte ho cezctx.docks.on_change(callback); vyvolá sa pri úpravách používateľom.
Zavolania on_change bežia v slučke udalostí pracovného procesu (NIE v zásobe vlákien). Držte ich
lacné — žiadne volania do editora, žiadna dlhá práca. Zmeňte malý stav v pamäti, zaznamenajte do
logu alebo spustite príkaz; ťažkú prácu robte z príkazu.
Pri paneli, ktorý pri štarte nemá vyskočiť, odovzdajte initially_visible=False a potom ho
z jeho príkazu zobrazte cez ctx.docks.set_panel_visible(panel_id, True) (zároveň ho vynesie
dopredu; je to idempotentné; neznáme id sa ignorujú). ctx.docks.is_panel_visible(panel_id)
povie, či je panel otvorený (karta v pozadí sa počíta). Viac panelov, ktoré patria k sebe:
odovzdajte rovnakú tab_group="<plugin.id>/panels" a rovnakú area a budú zdieľať jeden dok
s kartami.
ctx.line_markers — značky na riadkoch dokumentu¶
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
Druhy registrujte raz v register(); editor má iba niekoľko slotov. Kliknutie na okraj si
nárokujte iba vtedy, keď mu modul dáva význam — kliknutia prichádzajú cez
@ctx.events.on_margin_clicked tak či tak.
ctx.languages — farebné zvýrazňovanie syntaxe¶
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 musí pomenovať existujúci lexer backendu; styles a keywords používajú vlastné názvy
daného lexera (rovnaký tvar ako dodávané colorizers/*.json), nikdy nie číselné id.
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 — vlnovky¶
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
Zakaždým posielajte ÚPLNÚ množinu pre svoj source; nahradí predchádzajúcu. source je povinný.
ctx.theme — farby z motívu pre vaše vlastné rozhranie¶
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 — porovnanie vedľa seba¶
ctx.compare.open(mte.CompareSource(title, path=..., content=...),
mte.CompareSource(...), ignore_whitespace=False,
ignore_case=False, ignore_eol=False) # -> bool
ctx.events.publish¶
Zásuvný modul môže zverejňovať iba workspace.rootChanged a workspace.filesChanged; čokoľvek
iné vyvolá ValueError. Odoberať sa dá každá udalosť hostiteľa okrem document.aboutToSave
(pozri zoznam dekorátorov); natívna udalosť DocumentAboutToClose nemá v Pythone vôbec
žiadnu tému — použite document.closed.
ctx.settings_page — deklaratívne stránky Predvolieb (fáza 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"),
]),
)
Tí istí tvorcovia, tá istá sémantika binds. Dva ovládacie prvky zdieľajúce
binds="settings:<key>" sa navzájom zrkadlia v reálnom čase — ukotviteľný panel aj stránka
Predvolieb môžu mať začiarkavacie políčko pre to isté nastavenie a prepnutím jedného sa prepne
druhé.
Výhrada k fáze 3
Hodnoty sa uchovávajú priebežne pri každej úprave používateľom — Zrušiť zmeny nevráti. Zlepší sa to v budúcom spresnení.
ctx.ui.get / ctx.ui.set — prístup k ovládacím prvkom panela za behu (fáza 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 vracia hlavnú hodnotu ako reťazec:
- Label / Button / LineEdit / ComboBox → text
- Checkbox →
"true"/"false" - NumberSpin → celé číslo v desiatkovej sústave
- List → text práve vybranej položky (prázdny reťazec, keď nie je vybraná žiadna)
Zápis cez set zo strany hostiteľa je tichý: NEVYVOLÁ znova on_change daného prvku, takže
môžete svoje widgety aktualizovať z obslužného kódu príkazu bez nekonečných slučiek.
5. Úplný rozpracovaný príklad — zásuvný modul wordcount¶
Skopírujte to doslovne a zmeňte id, name a hodnoty title podľa svojho prípadu. Prechádza
každou službou z fázy 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")
Očakávaný priebeh u používateľa: otvorte editor, trikrát uložte súbor a potom kliknite na
Plugins → Word Count → Report save count. Stavový riadok zobrazí 3 save(s) this session.
Log zaznamená každý krok.
6. Bežné úlohy — recepty na skopírovanie¶
Každý recept je útržkom tela: vložte ho do def register(ctx):.
Pridanie príkazu do ponuky¶
@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")
Reakcia na uloženie¶
Upozornenie pri ukončení¶
@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
ctx.log.warning("mine: last chance to persist state")
Zápis do logu na konkrétnej úrovni¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Oddeľovač v ponuke¶
Viacero súvisiacich príkazov¶
@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")
Trvalý prepínač zapnuté/vypnuté s viditeľnou značkou¶
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)
Bočný panel s textovým poľom, začiarkavacím políčkom a dvoma tlačidlami¶
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", "")
Stránka Predvolieb zrkadliaca nastavenia panela¶
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. Antivzory — TOTO nerobte¶
print() na diagnostiku¶
Zle:
Správne:
register vnútri triedy¶
Zle:
Správne:
async def register¶
Zle:
Správne:
Importy tretích strán bez dodania balíka¶
Zle:
Správne (fáza 1): držte sa štandardnej knižnice. Ak naozaj potrebujete niečo od tretej strany,
dodajte balík wheel vnútri priečinka zásuvného modulu a v prvých riadkoch main.py rozšírte
sys.path; pozri Balenie.
Importy Qt¶
Zle:
Správne: nikde žiadne Qt. Celé rozhranie, ktorým môžete prispieť, vedie cez ctx.menus,
ctx.status, ctx.diagnostics, ctx.compare a deklaratívne panely a stránky nastavení. Nikdy
nie cez surový widget.
Blokovanie v obslužnom kóde¶
Zle:
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Správne (fáza 1): odložte to do vlákna a komunikujte cez 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")
Cesta v ponuke, ktorej horný segment nie je v plugin.json¶
Zle:
Správne: horný segment cesty držte v súlade s menus[]. Vyššie použite "Plugins/Mine/Do".
Uchovávanie ctx na neskôr¶
Zle:
_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?
Správne: zachyťte si služby, ktoré potrebujete, v uzávere, ktorý ich používa. register je
miestom, kde sa všetko prepája; všetko ostatné je obslužný kód, ktorý k nim už má prístup cez
uzáver.
8. Kontrolný zoznam pri ladení pre používateľa¶
Keď vytvoríte zásuvný modul, pripojte toto na koniec svojej odpovede, aby používateľ vedel, čo si overiť, keď to nebude fungovať:
- Spustite editor zo shellu, v ktorom je v
PATHinterpret Pythonu 3.9+ (prípadne naň smerujeMTE_PYTHON). - Hľadajte v logu
PluginHost: Python plugin backend enabled; interpreter=…. Ak chýba, prejdite na Riešenie problémov. - Hľadajte
[python:<your.id>] register() ran(ak ste sa držali príkladuwordcount) — dokazuje to, žeregisterprebehol. - Ak položka ponuky chýba, hľadajte v logu vysvetlenia
PythonPluginBackend: skipping '<id>': …. - Ak kliknutie na ponuku neurobí nič, hľadajte v logu riadky
[worker stderr] …— pristávajú tam výpisy zásobníka z Pythonu.
9. Obmedzenia, ktoré treba zopakovať na konci každej odpovede¶
Keď vytvoríte zásuvný modul, vždy používateľovi povedzte:
- Nainštalovať ho môže dvoma spôsobmi:
- zabaliť obsah priečinka do zipu ako
<name>.mteplugina použiť Predvoľby → Zásuvné moduly → Nainštalovať zásuvný modul... (balík sa pred akýmkoľvek zásahom overí), alebo - vložiť priečinok do používateľského priečinka zásuvných modulov editora
(
%LOCALAPPDATA%/MTE/Plugins/vo Windows;~/Library/Application Support/MTE/Plugins/na macOS;~/.local/share/MTE/Plugins/na Linuxe).
- zabaliť obsah priečinka do zipu ako
- Reštartovať editor.
- Nastaviť
MTE_PYTHON, ak log uvádza „no Python interpreter found“. - Ohlásiť späť riadok logu
[python:<id>] …, aby sme si potvrdili, že to prebehlo.
10. Zdroj pravdy¶
Ak si ktorékoľvek tvrdenie na tejto stránke protirečí s tým, čo klientský balík pre Python
skutočne implementuje, vyhráva balík. Dodáva sa vedľa editora v <exe>/Python/mte/ — skôr než
rozhodnete o nejakom správaní, prečítajte si tento zdrojový kód. Strana C++ protokolu je
v Components/PluginHost/Src/PyServiceBridge.cpp a prenosová špecifikácia je
v Python/PROTOCOL.md.