Psaní zásuvného modulu Modular Text Editoru v Pythonu s Claudem¶
Publikum: asistent AI (Claude / ChatGPT a podobní). Tato stránka je soběstačnou sadou pokynů: předejte ji asistentovi spolu se zadáním (»napiš mi prosím zásuvný modul, který dělá X«) a asistent by měl vytvořit úplnou, správnou, funkční složku modulu, aniž by musel číst zbytek dokumentace.
Jste-li člověk, který chce zásuvný modul napsat ručně, začněte raději zde — tato stránka je optimalizovaná pro konzumaci AI a přeskakuje přívětivé příklady.
1. Co vytváříte¶
Složku s právě dvěma soubory (plus volitelnými navíc):
Vraťte složku jako blok kódu na každý soubor. Uživatel složku buď vloží do adresáře editoru se
zásuvnými moduly, nebo ji zazipuje a archiv přejmenuje na <name>.mteplugin a nainstaluje přes
Předvolby → Zásuvné moduly → Instalovat zásuvný modul.... Obě cesty vyžadují k načtení restart.
Oba soubory jsou vyčerpávajícím způsobem popsány níže. Pravidla dodržujte doslova — hostitel provádí statickou sondu a moduly, které je porušují, odmítá.
2. plugin.json — kanonická šablona¶
{
"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á pravidla¶
"apiVersion"musí být1(aktuální verze hostitele)."entry"musí být"python"— nic jiného na tento backend nesměruje."module"musí být"main", není-li výslovný důvod modul pojmenovat jinak — a nikdy to nesmí být tečková cesta (fáze 1 podporuje pouze ploché názvy modulů)."id"musí být ve stylu obráceného DNS (org.<vendor>.<name>) a globálně jedinečné napříč nainstalovanými moduly uživatele. Pokud uživatel neurčil dodavatele, použijteorg.example..menus[]musí obsahovat alespoň jednu položku, jejížtitleodpovídá nejlevějšímu segmentu každé cesty v nabídkách, kterou vmain.pypoužíváte. U čehokoli obráceného k uživateli, co zjevně nepatří do nabídky jádra, dávejte přednost"Plugins".
Volitelná pole¶
"pyRequires": []— pole řetězců s požadavky pip (například["requests>=2.31"]). Je-li neprázdné, editor při prvním spuštění připraví virtuální prostředí pro daný modul (pip install— jednorázově potřebuje síť) a modul pod ním spustí. Dávejte přednost standardní knihovně, když stačí (žádná prodleva na přípravu, funguje bez sítě);pyRequiresdeklarujte, když úloha balíček třetí strany skutečně potřebuje, a uživateli řekněte, že se první spuštění na jeho instalaci pozastaví."permissions": []— hrubé deklarace schopností (například["network"]), zobrazené jen ke čtení na stránce Zásuvné moduly."network"deklarujte, když modul volá ven; je to poctivé označení pro uživatele, nikoli vynucení."requires": []— ID JINÝCH modulů, bez kterých tento nemůže fungovat (ne balíčky pip). Spouštějí se první; když některý chybí nebo je vypnutý, tento modul se nespustí a stránka Zásuvné moduly řekne proč. Nechte prázdné, pokud úloha uživatele opravdu nezávisí na jiném modulu."apiMinorVersion": 0— nejnižší minor verze API editoru, kterou modul potřebuje. Nechte0, pokud nepoužíváte volání zdokumentované jako novější.
Hodnoty, které byste si NEMĚLI vymýšlet¶
- Jakékoli pole neuvedené výše. Hostitel neznámé klíče ignoruje; jejich přidávání je rizikem pro údržbu.
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á pravidla¶
register(ctx)MUSÍ být funkcí na nejvyšší úrovni. Statická sonda vyžaduje řádek začínajícídef register(ve sloupci 0. Vnoříte-li ji do třídy, dekorujete v rozsahu modulu nebo z ní uděláteasync def, modul se při hledání tiše odmítne.- Když odebíráte události, importujte z
mte— typované dataclass (mte.DocumentSaveda podobně) pocházejí z onoho balíku.import mtenahoře je bezpečný, i když jej přímo nepoužíváte;# noqa: F401výše brání šumu v linteru. - Nikdy
print(). Stdout pracovníka je kanálem RPC; zápis do něj protokol poškozuje. Používejtectx.log.info / warning / error. - Nikdy neimportujte nic, co jste nedeklarovali. Nad rámec standardní knihovny a
mtemusí každý import třetí strany být uveden vpyRequiresvplugin.json(editor jej při prvním spuštění pipem nainstaluje do virtuálního prostředí modulu), nebo být vendorován uvnitř složky modulu. Nedeklarovaný import na stroji uživatele vyhodíModuleNotFoundError. - Obsluhy příkazů a událostí smějí krátce blokovat — běží ve fondu vláken pracovníka, takže
synchronní volání do
ctx.editor/ctx.settings/ctx.data_dirjsou legální. Ale zavolání poskytovatelů dokončování (@ctx.completions.provider(...)) blokovat NESMÍ: běží ve smyčce událostí s termínem asi 30 ms. Žádné vstupy a výstupy, žádná volání doctx.editor.*, pouze mezipaměť. Indexy budujte jinde (například přidocument.opened) a odpovídejte ze slovníku. - Neimportujte Qt, PySide, PyQt ani Scintillu. V pracovníkovi nejsou k dispozici.
- Id příkazů musí mít předponu jmenného prostoru vašeho modulu, aby nedošlo ke srážkám s
jádrem či jinými moduly. Výše
<id>.<verb>; napříkladwordcount.count,hello.sayHi. - Cesty v nabídkách musí začínat nabídkou nejvyšší úrovně, kterou jste rovněž deklarovali v
menus[]vplugin.json. Zvyklost:"Plugins/<Plugin Name>/<Verb>".
4. Úplné rozhraní — vše na ctx¶
Máte osmnáct služeb: 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(). Cokoli zde neuvedeného neexistuje — služby si nevymýšlejte.
(Virtuální prostředí pro jednotlivé moduly se deklarují přes pyRequires, nikoli službou na ctx;
podklad protokolu s proudy a rušením zatím žádné rozhraní obrácené k modulům nemá.)
ctx.commands¶
@ctx.commands.command(*, id, title, shortcut="", category="",
requires_editor=False, requires_writable_editor=False)
def _handler(): ...
Dekorátor. Registruje příkaz a naváže _handler (funkci bez argumentů vracející None). Výjimky v
_handler pracovník zachytí a zapíše na stderr; editor neshodí.
requires_editor nastavte, když příkaz čte aktivní dokument (buffer, výběr, kurzor), a
requires_writable_editor, když mění text. Hostitel pak příkaz zašedne tam, kde by fungovat
nemohl — na kartě bez dokumentu (zobrazení porovnání či protokolu) a u příznaku zapisovatelnosti i
na dokumentu jen ke čtení — místo aby uživateli dovolil vyvolat tichou prázdnou operaci. Příznak
zapisovatelnosti ten druhý implikuje; u příkazů, které jen otevírají okno či panel, nechte oba
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 "Nejvyšší/Pod/…/Popisek". Mezilehlé podnabídky se vytvářejí na vyžádání.
set_item_checked promění položku v zaškrtávatelnou položku nabídky a nastaví její značku. Použijte
to pro trvalé přepínače zapnuto/vypnuto. První volání položku povýší; další volání jen přehazují.
U neznámých id tichá prázdná operace.
set_item_enabled položku zašedne. Použijte to, když příkaz potřebuje splněnou podmínku, aby
nedostupná položka vypadala nedostupně, místo aby po kliknutí nic neudělala. Idempotentní; položky
začínají zapnuté; u neznámých id tichá prázdná operace.
ctx.events¶
Sedmnáct témat, každé se sladkým dekorátorem a se surovou podobou.
| Sladké | Surové téma | Data |
|---|---|---|
@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 ji triviální |
@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() — jednou za běh, po prvním snímku |
@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) — kliknutí do sloupce záložek; bity modifiers: mte.MODIFIER_* |
Surová podoba:
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
Nevymýšlejte si nová témata — surová podoba je ověřuje a u čehokoli mimo tabulku vyhodí
ValueError.
ctx.log¶
Spusť a zapomeň. Řádky mají v diagnostickém protokolu hostitele předponu [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í být "info" / "warning" / "error". timeout_ms=0 znamená, že hostitel zvolí
výchozí hodnotu. show() je pro pomíjivá oznámení; cokoli, co zůstává na obrazovce (živý počet,
příznak režimu), je část — ty dvě se navzájem nikdy nepřebíjejí.
ctx.editor (fáze 2)¶
Každá metoda je synchronní: blokuje volající vlákno. Z obsluhy příkazu či události typu def
je to v pořádku (pracovník synchronní obsluhy rozděluje do fondu vláken); nelegální z obsluh
async def a z poskytovatelů ctx.completions (kteří běží přímo ve smyčce událostí).
Dokumenty se odkazují neprůhlednými id typu int (0 = žádný 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áze 2)¶
Typované úložiště klíč/hodnota omezené na daný modul (ctx.settings) plus pohled na nastavení celé
aplikace jen ke čtení (ctx.app_settings). Typ se odvozuje z typu výchozí hodnoty v Pythonu.
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 vyhazují RuntimeError. Ve vlastní logice musíte při čtení
kontrolovat bool dřív než int (v Pythonu platí isinstance(True, int) == True).
ctx.completions (fáze 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)]
Pole req: document, prefix, position, language_id, manual. Druhy: KIND_KEYWORD /
KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Vraťte list[CompletionItem], prosté
řetězce, nebo slovníky. Zavolání se MUSÍ vrátit do asi 30 ms, jinak hostitel vaše výsledky pro
daný stisk klávesy zahodí — žádné vstupy a výstupy, žádné ctx.editor.text(...), pouze mezipaměť.
Indexy budujte jinde (například při document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (fáze 2)¶
data_dir() používejte pro mezipaměti a indexy. diagnostics_dir() je jen ke čtení (zapisuje tam
hlášení o pádech).
ctx.docks — deklarativní ukotvitelné panely (fáze 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")]),
]),
)
Specifikaci formuláře stavte pomocníky mte.ui. Slovník ovládacích prvků je pevný:
Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List uvnitř Column / Row / Group.
binds="settings:<key>"— hodnota prvku se automaticky uchovává přesctx.settings(totéž úložiště jakoctx.settings.set).on_click="<cmd.id>"— pouze tlačítka; vyvolá zaregistrovaný příkaz.on_change=<handler>— přidělte přesctx.docks.on_change(callback); spouští se při úpravách uživatelem.
Zavolání on_change běží ve smyčce událostí pracovníka (NIKOLI ve fondu vláken). Držte je levná —
žádná volání do editoru, žádná dlouhá práce. Změňte malý stav v paměti, zapište do protokolu, nebo
spusťte příkaz; těžkou práci dělejte z příkazu.
Pro panel, který při spuštění nesmí vyskočit, předejte initially_visible=False a pak jej z jeho
příkazu odkryjte pomocí ctx.docks.set_panel_visible(panel_id, True) (zároveň jej zvedne dopředu;
idempotentní; neznámá id se ignorují). ctx.docks.is_panel_visible(panel_id) řekne, zda je
panel otevřený (karta v pozadí se počítá). Více panelů, které patří k sobě: předejte stejnou
tab_group="<plugin.id>/panels" a stejnou area a budou sdílet jeden dok s kartami.
ctx.line_markers — značky na řádcích 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 jednou v register(); editor má jen několik slotů. Kliknutí na okraj si
nárokujte jen tehdy, když mu modul dává význam — kliknutí přicházejí přes
@ctx.events.on_margin_clicked tak jako tak.
ctx.languages — obarvování 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í pojmenovat existující lexer backendu; styles a keywords používají vlastní názvy
daného lexeru (týž tvar jako dodávané colorizers/*.json), nikdy čí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
Pro svůj source posílejte pokaždé ÚPLNOU sadu; nahrazuje tu předchozí. source je povinný.
ctx.theme — barvy z motivu pro vaše vlastní rozhraní¶
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 — rozdíl vedle sebe¶
ctx.compare.open(mte.CompareSource(title, path=..., content=...),
mte.CompareSource(...), ignore_whitespace=False,
ignore_case=False, ignore_eol=False) # -> bool
ctx.events.publish¶
Modul smí publikovat pouze workspace.rootChanged / workspace.filesChanged; cokoli jiného vyhodí
ValueError. Odebírat lze každou událost hostitele kromě document.aboutToSave (viz seznam
dekorátorů); nativní událost DocumentAboutToClose nemá v Pythonu žádné téma — použijte
document.closed.
ctx.settings_page — deklarativní stránky Předvoleb (fáze 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"),
]),
)
Titíž stavitelé, tatáž sémantika binds. Dva ovládací prvky sdílející binds="settings:<key>" se
navzájem v reálném čase zrcadlí — ukotvitelný panel i stránka Předvoleb mohou oba držet
zaškrtávací pole pro totéž nastavení a přehození jednoho překlopí druhé.
Výhrada fáze 3
Hodnoty se ukládají dychtivě při každé úpravě uživatelem — Zrušit nevrací to, co uživatel změnil. V budoucím upřesnění se to zlepší.
ctx.ui.get / ctx.ui.set — přístup k ovládacím prvkům panelu za běhu (fáze 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 vrací hlavní hodnotu jako řetězec:
- Label / Button / LineEdit / ComboBox → text
- Checkbox →
"true"/"false" - NumberSpin → celé číslo desítkově
- List → text právě vybrané položky (prázdný řetězec, když žádná)
set řízený hostitelem je tichý: znovu NESPOUŠTÍ on_change daného prvku, takže můžete své widgety
aktualizovat z obsluhy příkazu bez nekonečných smyček.
5. Úplný rozpracovaný příklad — zásuvný modul wordcount¶
Zkopírujte jej doslova a změňte id, name a title podle svého případu. Prochází každou službou
z fáze 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čekávaný postup uživatele: otevřít editor, třikrát uložit soubor a pak kliknout na
Plugins → Word Count → Report save count. Stavový řádek zobrazí 3 save(s) this session.
Protokol zaznamená každý krok.
6. Běžné úlohy — recepty ke zkopírování¶
Každý recept je úryvek jen s tělem: vložte jej dovnitř def register(ctx):.
Přidání příkazu do nabídky¶
@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")
Reakce na uložení¶
Upozornění při 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 protokolu na konkrétní úrovni¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Oddělovač v nabídce¶
Několik souvisejících příkazů¶
@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ý přepínač zapnuto/vypnuto s viditelným zaškrtnutím¶
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)
Postranní panel s textovým vstupem, zaškrtávacím polem a dvěma tlačítky¶
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 Předvoleb zrcadlící nastavení doku¶
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 — tohle NEDĚLEJTE¶
print() pro diagnostiku¶
Špatně:
Správně:
register uvnitř třídy¶
Špatně:
Správně:
async def register¶
Špatně:
Správně:
Importy třetích stran bez dodání wheelu¶
Špatně:
Správně (fáze 1): držte se standardní knihovny. Potřebujete-li skutečně něco od třetí strany,
dodejte wheel uvnitř složky modulu a na prvních řádcích main.py rozšiřte sys.path; viz
Balení.
Importy Qt¶
Špatně:
Správně: nikde žádné Qt. Veškeré rozhraní, kterým můžete přispět, vede přes ctx.menus,
ctx.status, ctx.diagnostics, ctx.compare a deklarativní panely doku a nastavení. Nikdy syrový
widget.
Blokování v obsluze¶
Špatně:
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Správně (fáze 1): předejte to vláknu a komunikujte přes 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 nabídce, jejíž horní segment není v plugin.json¶
Špatně:
Správně: držte segment nejvyšší úrovně v cestě zarovnaný s menus[]. Výše použijte
"Plugins/Mine/Do".
Uložení ctx pro pozdější použití¶
Špatně:
_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ávně: zachyťte si služby, které potřebujete, v uzávěru, jenž je používá. register je místem,
kde se vše propojuje; všechno ostatní je obsluha, která už přístup přes uzávěr má.
8. Kontrolní seznam pro ladění, určený uživateli¶
Když modul vytvoříte, zařaďte tohle na konec své odpovědi, aby uživatel věděl, co zkontrolovat, pokud to nefunguje:
- Spusťte editor z shellu, ve kterém je v
PATHinterpret Pythonu 3.9+ (nebo na nějaký ukazujeMTE_PYTHON). - Hledejte v protokolu
PluginHost: Python plugin backend enabled; interpreter=…. Pokud chybí, skočte na Řešení potíží. - Hledejte
[python:<your.id>] register() ran(pokud jste se drželi příkladuwordcount) — dokazuje, že seregisterspustil. - Chybí-li položka nabídky, hledejte v protokolu vysvětlení
PythonPluginBackend: skipping '<id>': …. - Když kliknutí v nabídce nic neudělá, hledejte v protokolu řádky
[worker stderr] …— tam končí výpisy zásobníku z Pythonu.
9. Omezení, která má zopakovat každá odpověď¶
Když modul vytvoříte, uživateli vždy řekněte:
- Nainstalujte jej kterýmkoli způsobem:
- obsah složky zazipujte jako
<name>.mteplugina použijte Předvolby → Zásuvné moduly → Instalovat zásuvný modul... (balíček se ověří dřív, než se čehokoli dotkne), nebo - složku vložte do uživatelského adresáře editoru se zásuvnými moduly
(
%LOCALAPPDATA%/MTE/Plugins/ve Windows;~/Library/Application Support/MTE/Plugins/na macOS;~/.local/share/MTE/Plugins/v Linuxu).
- obsah složky zazipujte jako
- Restartujte editor.
- Nastavte
MTE_PYTHON, pokud protokol říká »no Python interpreter found«. - Nahlaste zpět řádek protokolu
[python:<id>] …, abychom mohli potvrdit, že se spustil.
10. Zdroj pravdy¶
Je-li jakékoli tvrzení na této stránce v rozporu s tím, co klientský balíček v Pythonu skutečně
implementuje, vyhrává balíček. Dodává se vedle editoru v <exe>/Python/mte/ — než o chování
rozhodnete, přečtěte si onen zdroj. Strana protokolu v C++ je v
Components/PluginHost/Src/PyServiceBridge.cpp a specifikace přenosu je v Python/PROTOCOL.md.