Przejdź do treści

Pisanie wtyczki Pythona dla Modular Text Editor z Claude

Odbiorca: asystent AI (Claude / ChatGPT / itp.). Ta strona to samowystarczalny zestaw instrukcji: przekaż ją asystentowi razem z zadaniem („napisz mi wtyczkę, która robi X"), a asystent powinien wyprodukować kompletny, poprawny, działający folder wtyczki bez czytania reszty dokumentacji.

Jeśli jesteś człowiekiem piszącym wtyczkę ręcznie, zacznij tutaj — ta strona jest zoptymalizowana pod konsumpcję przez AI i pomija przyjazne przykłady.


1. Co produkujesz

Folder z dokładnie dwoma plikami (plus opcjonalne dodatki):

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

Zwróć folder jako blok kodu per plik. Użytkownik albo wrzuca folder do katalogu wtyczek edytora, albo zipuje folder, zmienia nazwę archiwum na <nazwa>.mteplugin i instaluje przez Preferencje → Wtyczki → Zainstaluj…. Obie ścieżki wymagają restartu do załadowania.

Oba pliki są opisane wyczerpująco niżej. Trzymaj się reguł dosłownie — host wykonuje statyczną sondę i odrzuca wtyczki, które je łamią.


2. plugin.json — szablon kanoniczny

{
  "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 }
  ]
}

Twarde reguły

  1. "apiVersion" musi być 1 (bieżąca wersja hosta).
  2. "entry" musi być "python" — nic innego nie kieruje do tego backendu.
  3. "module" musi być "main", chyba że istnieje jawny powód, by nazwać moduł inaczej — i nigdy ścieżka z kropkami (Faza 1 wspiera tylko płaskie nazwy modułów).
  4. "id" musi być w stylu odwróconego DNS (org.<vendor>.<name>) i globalnie unikalne wśród wtyczek użytkownika. Jeśli użytkownik nie podał vendora, użyj org.example..
  5. menus[] musi zawierać co najmniej jeden wpis, którego title odpowiada skrajnie lewemu segmentowi każdej ścieżki menu użytej w main.py. Dla rzeczy skierowanych do użytkownika, które nie pasują oczywiście do menu rdzenia, preferuj "Plugins".

Pola opcjonalne

  • "pyRequires": [] — tablica łańcuchów wymagań pip (np. ["requests>=2.31"]). Gdy niepusta, edytor przygotowuje venv per wtyczka przy pierwszym starcie (pip install — ten jeden raz potrzebna sieć) i uruchamia wtyczkę pod nim. Preferuj bibliotekę standardową, gdy wystarcza (brak opóźnienia provisioning-u, działa offline); deklaruj pyRequires, gdy zadanie naprawdę wymaga pakietu zewnętrznego, i powiedz użytkownikowi, że pierwszy start zatrzyma się na instalację.
  • "permissions": [] — zgrubne deklaracje możliwości (np. ["network"]), pokazywane tylko do odczytu na stronie Wtyczek. Deklaruj "network", gdy wtyczka wykonuje połączenia wychodzące; to uczciwa etykieta dla użytkownika, nie egzekwowanie.

Wartości, których NIE wymyślaj

  • Żadnych pól spoza listy powyżej. Host ignoruje nieznane klucze; dodawanie ich to ryzyko utrzymaniowe.

3. main.py — szkielet kanoniczny

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

Twarde reguły

  1. register(ctx) MUSI być funkcją najwyższego poziomu. Statyczna sonda wymaga linii zaczynającej się od def register( w kolumnie 0. Zagnieżdżenie w klasie, dekorowanie na poziomie modułu albo async def powoduje ciche odrzucenie wtyczki przy wykrywaniu.
  2. Importuj z mte, gdy subskrybujesz zdarzenia — typowane dataclassy (mte.DocumentSaved itd.) pochodzą z tego pakietu. import mte na górze jest bezpieczny nawet nieużywany wprost; # noqa: F401 wyżej ucisza linter.
  3. Nigdy print(). Stdout workera to kanał RPC; pisanie do niego psuje protokół. Używaj ctx.log.info / warning / error.
  4. Nigdy nie importuj niczego niezadeklarowanego. Poza biblioteką standardową i mte każdy import zewnętrzny musi wystąpić w pyRequires w plugin.json (edytor pip-instaluje go do venv wtyczki przy pierwszym starcie) albo być vendorowany w folderze wtyczki. Niezadeklarowany import rzuca ModuleNotFoundError na maszynie użytkownika.
  5. Handlery poleceń / zdarzeń mogą krótko blokować — działają w puli wątków workera, więc synchroniczne wywołania ctx.editor / ctx.settings / ctx.data_dir są legalne. Ale callbacki dostawców uzupełnień (@ctx.completions.provider(...)) NIE MOGĄ blokować: działają na pętli zdarzeń z deadlinem ~30 ms. Bez I/O, bez wywołań ctx.editor.*, tylko cache. Indeksy buduj gdzie indziej (np. na document.opened) i odpowiadaj z dicta.
  6. Nie importuj Qt, PySide, PyQt ani Scintilli. Nie są dostępne w workerze.
  7. Identyfikatory poleceń muszą mieć prefiks przestrzeni nazw Twojej wtyczki, aby uniknąć kolizji z rdzeniem / innymi wtyczkami. <id>.<verb> wyżej; np. wordcount.count, hello.sayHi.
  8. Ścieżki menu muszą zaczynać się od menu najwyższego poziomu zadeklarowanego też w menus[] w plugin.json. Konwencja: "Plugins/<Nazwa wtyczki>/<Czasownik>".

4. Kompletne API — wszystko na ctx

Masz dwanaście usług (Fazy 1 + 2 + 3). Cokolwiek niewymienione tutaj nie istnieje — nie wymyślaj usług. (Venvy per wtyczka deklaruje się przez pyRequires, nie usługę ctx; substrat protokołu streaming/cancel nie ma jeszcze API dla wtyczek.)

ctx.commands

@ctx.commands.command(*, id, title, shortcut="", category="")
def _handler(): ...

Dekorator. Rejestruje polecenie i wiąże _handler (funkcję bezargumentową zwracającą None). Wyjątki w _handler łapie worker i pisze na stderr; nie wywracają edytora.

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

path to "Top/Sub/…/Etykieta". Pośrednie podmenu powstają na żądanie.

set_item_checked zamienia pozycję w zaznaczalny wpis menu i ustawia znacznik. Używaj do trwałych przełączników wł./wył. Pierwsze wywołanie promuje; kolejne tylko przełączają. Cichy no-op na nieznanych id.

ctx.events

Sześć tematów, każdy z dekoratorem-cukrem i formą surową.

Cukier Surowy temat Ładunek
@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_selection_changed selection.changed mte.SelectionChanged(document: int)
@ctx.events.on_active_document_changed document.activeChanged mte.ActiveDocumentChanged(document: int)
@ctx.events.on_app_about_to_quit app.aboutToQuit mte.AppAboutToQuit()

Forma surowa:

handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id)  # to unbind

Nie wymyślaj nowych tematów — forma surowa je waliduje i rzuca ValueError na wszystko spoza tabeli.

ctx.log

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

Wyślij i zapomnij. Linie mają prefiks [python:<plugin.id>] w logu diagnostycznym hosta.

ctx.status

ctx.status.show(message: str, timeout_ms: int = 0, level: str = "info")

level musi być "info" / "warning" / "error". timeout_ms=0 znaczy, że host wybiera domyślny.

ctx.editor (Faza 2)

Każda metoda jest synchroniczna: blokuje wątek wywołujący. W porządku z callbacku polecenia / zdarzenia def (worker odpala handlery sync na puli wątków); nielegalne z handlerów async def i z dostawców ctx.completions (działających wprost na pętli zdarzeń).

Dokumenty adresują nieprzezroczyste identyfikatory int (0 = brak dokumentu).

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
ok      = ctx.editor.save_document(doc)
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))
lang    = ctx.editor.colorizer_id(doc)              # "python", "cpp", ...

mte.SearchOptions.mode: "normal" / "extended" / "regex". mte.SearchOptions.direction: "forward" / "backward".

ctx.settings / ctx.app_settings (Faza 2)

Typowany magazyn klucz/wartość per wtyczka (ctx.settings) plus widok tylko do odczytu ustawień całej aplikacji (ctx.app_settings). Typ wnioskowany z pythonowego typu wartości domyślnej.

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 rzucają RuntimeError. We własnej logice odczytu sprawdzaj bool przed int (w Pythonie isinstance(True, int) == True).

ctx.completions (Faza 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)]

Pola req: document, prefix, position, language_id, manual. Rodzaje: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Zwracaj list[CompletionItem], gołe łańcuchy albo dicty. Callback MUSI wrócić w ~30 ms, inaczej host porzuca Twoje wyniki dla tego naciśnięcia — bez I/O, bez ctx.editor.text(...), tylko cache. Indeksy buduj gdzie indziej (np. na document.opened).

ctx.data_dir() / ctx.diagnostics_dir() (Faza 2)

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

Używaj data_dir() na cache i indeksy. diagnostics_dir() jest tylko do odczytu (pisze tam raporter awarii).

ctx.docks — deklaratywne panele dokowane (Faza 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")]),
    ]),
)

Specyfikację formularza buduj pomocnikami mte.ui. Słownik kontrolek jest stały: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List w Column / Row / Group.

  • binds="settings:<klucz>" — wartość kontrolki utrwala się automatycznie przez ctx.settings (ten sam magazyn co ctx.settings.set).
  • on_click="<cmd.id>" — tylko Button; wywołuje zarejestrowane polecenie.
  • on_change=<handler> — alokuj przez ctx.docks.on_change(callback); odpala się na edycjach użytkownika.

Callbacki on_change działają na pętli zdarzeń workera (NIE na puli wątków). Niech będą tanie — bez wywołań edytora, bez długiej pracy. Zmień mały stan w pamięci, zaloguj albo odpal polecenie; ciężką pracę rób z polecenia.

ctx.settings_page — deklaratywne strony Preferencji (Faza 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"),
    ]),
)

Te same kreatory, ta sama semantyka binds. Dwie kontrolki dzielące binds="settings:<klucz>" odzwierciedlają się w czasie rzeczywistym — panel dokowany i strona Preferencji mogą obie trzymać checkbox tego samego ustawienia, a przełączenie jednego przełącza drugi.

Zastrzeżenie Fazy 3

Wartości utrwalają się na bieżąco przy każdej edycji użytkownika — Anuluj nie wycofuje tego, co zmienił. Poprawi się to w przyszłym udoskonaleniu.

ctx.ui.get / ctx.ui.set — dostęp runtime do kontrolek panelu (Faza 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 zwraca wartość główną jako łańcuch:

  • Label / Button / LineEdit / ComboBox → tekst
  • Checkbox → "true" / "false"
  • NumberSpin → liczba dziesiętnie
  • List → tekst wybranej pozycji (pusty łańcuch, gdy brak)

set sterowany przez hosta jest cichy: NIE odpala ponownie on_change kontrolki, więc możesz aktualizować własne widżety z handlera polecenia bez nieskończonych pętli.


5. Pełny przykład — wtyczka wordcount

Skopiuj dosłownie i zmień id, name i title pod swój przypadek. Ćwiczy każdą usługę Fazy 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")

Oczekiwany przepływ: otwórz edytor, zapisz plik trzy razy, potem kliknij Plugins → Word Count → Report save count. Pasek stanu pokaże 3 save(s) this session. Log rejestruje każdy krok.


6. Częste zadania — przepisy kopiuj-wklej

Każdy przepis to snippet samego ciała: wklej wewnątrz def register(ctx):.

Dodaj polecenie w menu

@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")

Reaguj na zapis

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

Ostrzeż przy wyjściu

@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
    ctx.log.warning("mine: last chance to persist state")

Loguj na konkretnym poziomie

ctx.log.trace  ("very fine")
ctx.log.debug  ("dev")
ctx.log.info   ("routine")
ctx.log.warning("odd")
ctx.log.error  ("wrong")

Separator menu

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

Kilka powiązanych poleceń

@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")

Trwały przełącznik wł./wył. z widocznym znacznikiem

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)

Panel boczny z polem tekstowym, checkboxem i dwoma przyciskami

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", "")

Strona Preferencji lustrzana do ustawień 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. Antywzorce — NIE rób tego

Źle:

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

Dobrze:

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

register w klasie

Źle:

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

Dobrze:

def register(ctx):
    ...

async def register

Źle:

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

Dobrze:

def register(ctx):
    ...

Importy zewnętrzne bez dostarczenia pakietu

Źle:

import requests   # <-- ImportError on most user machines

Dobrze (Faza 1): trzymaj się biblioteki standardowej. Jeśli naprawdę potrzebujesz czegoś zewnętrznego, dostarcz wheel w folderze wtyczki i rozszerz sys.path w pierwszych liniach main.py; zobacz Pakowanie.

Importy Qt

Źle:

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

Dobrze: żadnego Qt. Całe UI, jakie możesz wnieść, idzie przez ctx.menus, ctx.status i (od Fazy 3) deklaratywne panele dok/ustawień. Nigdy surowy widżet.

Blokowanie w handlerze

Źle:

def _handler():
    time.sleep(30)   # <-- stalls the worker for 30s
    result = requests.get("https://slow.example")

Dobrze (Faza 1): oddeleguj do wątku, komunikuj przez 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")

Ścieżka menu, której górny segment nie jest w plugin.json

Źle:

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

Dobrze: trzymaj górny segment ścieżki zgodny z menus[]. Powyżej użyj "Plugins/Mine/Do".

Przechowywanie ctx na później

Źle:

_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?

Dobrze: przechwyć potrzebne usługi w domknięciu, które ich używa. register to miejsce okablowania; wszystko inne to handler, który już ma dostęp przez domknięcie.


8. Checklist debugowania dla użytkownika

Dołącz to na końcu odpowiedzi, gdy produkujesz wtyczkę, aby użytkownik wiedział, co sprawdzić, gdy nie działa:

  1. Uruchom edytor z powłoki, w której interpreter Pythona 3.9+ jest w PATH (albo MTE_PYTHON na niego wskazuje).
  2. Poszukaj w logu PluginHost: Python plugin backend enabled; interpreter=…. Jeśli brak, skocz do Rozwiązywania problemów.
  3. Poszukaj [python:<twoje.id>] register() ran (jeśli poszedłeś za przykładem wordcount) — dowód, że register się wykonał.
  4. Jeśli brakuje pozycji menu, sprawdź w logu wyjaśnienia PythonPluginBackend: skipping '<id>': ….
  5. Jeśli klik w menu nic nie robi, sprawdź w logu linie [worker stderr] … — tam lądują pythonowe tracebacki.

9. Ograniczenia do powtórzenia na końcu każdej odpowiedzi

Gdy produkujesz wtyczkę, zawsze powiedz użytkownikowi:

  • Zainstaluj na jeden z dwóch sposobów:
    • zzipuj zawartość folderu jako <nazwa>.mteplugin i użyj Preferencje → Wtyczki → Zainstaluj… (waliduje pakiet, zanim czegokolwiek dotknie), albo
    • wrzuć folder do katalogu wtyczek użytkownika (%LOCALAPPDATA%/MTE/Plugins/ na Windows; ~/Library/Application Support/MTE/Plugins/ na macOS; ~/.local/share/MTE/Plugins/ na Linuksie).
  • Zrestartuj edytor.
  • Ustaw MTE_PYTHON, jeśli log mówi „no Python interpreter found".
  • Odeślij linię logu [python:<id>] …, abyśmy potwierdzili działanie.

10. Źródło prawdy

Jeśli cokolwiek na tej stronie kłóci się z tym, co faktycznie implementuje pakiet klienta Pythona, wygrywa pakiet. Leży obok edytora w <exe>/Python/mte/ — przeczytaj to źródło przed rozstrzygnięciem zachowania. Strona C++ protokołu jest w Components/PluginHost/Src/PyServiceBridge.cpp, a specyfikacja przewodowa w Python/PROTOCOL.md.