Preskočiť na obsah

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

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

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á

  1. "apiVersion" musí byť 1 (aktuálna verzia hostiteľa).
  2. "entry" musí byť "python" — nič iné na tento backend nesmeruje.
  3. "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).
  4. "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žite org.example..
  5. menus[] musí obsahovať aspoň jednu položku, ktorej title zodpovedá najľavejšiemu segmentu každej cesty v ponuke, ktorú použijete v main.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); pyRequires deklarujte, 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. Nechajte 0, 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á

  1. register(ctx) MUSÍ byť funkciou na najvyššej úrovni. Statická kontrola vyžaduje riadok začínajúci sa def register( v stĺpci 0. Ak ju vnoríte do triedy, dekorujete na úrovni modulu alebo z nej urobíte async def, modul sa pri vyhľadávaní potichu odmietne.
  2. Keď sa prihlasujete na odber udalostí, importujte z mte — typované dátové triedy (mte.DocumentSaved a podobne) pochádzajú z tohto balíka. import mte navrchu je bezpečný, aj keď ho priamo nepoužívate; # noqa: F401 vyššie zabráni šumu z lintera.
  3. Nikdy nepoužívajte print(). Stdout pracovného procesu je kanálom RPC; zápis doň protokol poškodí. Použite ctx.log.info / warning / error.
  4. Nikdy neimportujte nič, čo ste nedeklarovali. Okrem štandardnej knižnice a mte sa musí každý import tretej strany objaviť v poli pyRequires súboru plugin.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ľa ModuleNotFoundError.
  5. 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 či ctx.data_dir sú 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 do ctx.editor.*, iba z vyrovnávacej pamäte. Indexy budujte inde (napríklad pri document.opened) a odpovedajte zo slovníka.
  6. Neimportujte Qt, PySide, PyQt ani Scintillu. V pracovnom procese nie sú k dispozícii.
  7. 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íklad wordcount.count, hello.sayHi.
  8. Cesty v ponukách sa musia začínať hlavnou ponukou, ktorú ste zároveň deklarovali v menus[] súboru plugin.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

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

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)

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

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á cez ctx.settings (to isté úložisko ako ctx.settings.set).
  • on_click="<cmd.id>" — iba pri tlačidlách; vyvolá zaregistrovaný príkaz.
  • on_change=<handler> — pridelíte ho cez ctx.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.

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č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

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

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

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

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

Zle:

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

Správne:

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

register vnútri triedy

Zle:

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

Správne:

def register(ctx):
    ...

async def register

Zle:

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

Správne:

def register(ctx):
    ...

Importy tretích strán bez dodania balíka

Zle:

import requests   # <-- ImportError on most user machines

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:

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

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:

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

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

  1. Spustite editor zo shellu, v ktorom je v PATH interpret Pythonu 3.9+ (prípadne naň smeruje MTE_PYTHON).
  2. Hľadajte v logu PluginHost: Python plugin backend enabled; interpreter=…. Ak chýba, prejdite na Riešenie problémov.
  3. Hľadajte [python:<your.id>] register() ran (ak ste sa držali príkladu wordcount) — dokazuje to, že register prebehol.
  4. Ak položka ponuky chýba, hľadajte v logu vysvetlenia PythonPluginBackend: skipping '<id>': ….
  5. 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>.mteplugin a 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).
  • 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.