Přeskočit obsah

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):

<plugin-id>/
├── plugin.json      # required
└── main.py          # required

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

  1. "apiVersion" musí být 1 (aktuální verze hostitele).
  2. "entry" musí být "python" — nic jiného na tento backend nesměruje.
  3. "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ů).
  4. "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žijte org.example..
  5. menus[] musí obsahovat alespoň jednu položku, jejíž title odpovídá nejlevějšímu segmentu každé cesty v nabídkách, kterou v main.py použí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ě); pyRequires deklarujte, 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. Nechte 0, 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

  1. 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áte async def, modul se při hledání tiše odmítne.
  2. Když odebíráte události, importujte z mte — typované dataclass (mte.DocumentSaved a podobně) pocházejí z onoho balíku. import mte nahoře je bezpečný, i když jej přímo nepoužíváte; # noqa: F401 výše brání šumu v linteru.
  3. Nikdy print(). Stdout pracovníka je kanálem RPC; zápis do něj protokol poškozuje. Používejte ctx.log.info / warning / error.
  4. Nikdy neimportujte nic, co jste nedeklarovali. Nad rámec standardní knihovny a mte musí každý import třetí strany být uveden v pyRequires v plugin.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.
  5. 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_dir jsou 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í do ctx.editor.*, pouze mezipaměť. Indexy budujte jinde (například při document.opened) a odpovídejte ze slovníku.
  6. Neimportujte Qt, PySide, PyQt ani Scintillu. V pracovníkovi nejsou k dispozici.
  7. 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říklad wordcount.count, hello.sayHi.
  8. Cesty v nabídkách musí začínat nabídkou nejvyšší úrovně, kterou jste rovněž deklarovali v menus[] v plugin.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

ctx.log.trace  (str)
ctx.log.debug  (str)
ctx.log.info   (str)
ctx.log.warning(str)
ctx.log.error  (str)

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)

p = ctx.data_dir()   # pathlib.Path -- writable, host-created

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řes ctx.settings (totéž úložiště jako ctx.settings.set).
  • on_click="<cmd.id>" — pouze tlačítka; vyvolá zaregistrovaný příkaz.
  • on_change=<handler> — přidělte přes ctx.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.

wordcount/plugin.json
{
  "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 }
  ]
}
wordcount/main.py
"""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í

@ctx.events.on_document_saved
def _on_save(ev):
    ctx.log.info(f"saved doc {ev.document}")

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

ctx.menus.add_separator("Plugins/Mine")

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

Špatně:

def _handler():
    print("did the thing")   # <-- corrupts the RPC channel

Správně:

def _handler():
    ctx.log.info("did the thing")

register uvnitř třídy

Špatně:

class Plugin:
    def register(self, ctx):   # <-- static probe rejects this
        ...

Správně:

def register(ctx):
    ...

async def register

Špatně:

async def register(ctx):   # <-- probe accepts but runtime never awaits
    ...

Správně:

def register(ctx):
    ...

Importy třetích stran bez dodání wheelu

Špatně:

import requests   # <-- ImportError on most user machines

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ě:

from PySide6 import QtWidgets   # <-- not available; no Qt in the worker

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ě:

"menus": [ { "title": "Plugins", "barPriority": 570 } ]
ctx.menus.add_item("Tools/Mine/Do", "mine.do")   # <-- Tools ≠ Plugins

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:

  1. Spusťte editor z shellu, ve kterém je v PATH interpret Pythonu 3.9+ (nebo na nějaký ukazuje MTE_PYTHON).
  2. Hledejte v protokolu PluginHost: Python plugin backend enabled; interpreter=…. Pokud chybí, skočte na Řešení potíží.
  3. Hledejte [python:<your.id>] register() ran (pokud jste se drželi příkladu wordcount) — dokazuje, že se register spustil.
  4. Chybí-li položka nabídky, hledejte v protokolu vysvětlení PythonPluginBackend: skipping '<id>': ….
  5. 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>.mteplugin a 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).
  • 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.