Przejdź do treści

ctx — kontekst wtyczki

register(ctx) otrzymuje jeden argument: PluginContext. W Fazie 1 udostępnia pięć usług. Każda to mała klasa Pythona, którą worker buduje na kanale RPC; źródła leżą w Python/mte/.

def register(ctx):
    ctx.commands       # register/invoke commands
    ctx.menus          # add menu items & separators
    ctx.events         # subscribe to editor events
    ctx.log            # write to the host's diagnostics stream
    ctx.status         # post transient status-bar messages
    ctx.editor         # buffer / caret / selection access (Phase 2)
    ctx.settings       # per-plugin key/value store (Phase 2)
    ctx.app_settings   # editor-wide read-only settings view (Phase 2)
    ctx.completions    # register auto-completion providers (Phase 2)
    ctx.docks          # declarative dock panels (Phase 3)
    ctx.settings_page  # declarative Preferences pages (Phase 3)
    ctx.ui             # runtime get/set for panel controls (Phase 3)

Nie przechowuj ctx w zmiennej globalnej z zamiarem użycia później — choć atrybuty są stabilne przez życie wtyczki, traktuj ctx jako obiekt per-rejestracja. Jeśli potrzebujesz, trzymaj referencje do poszczególnych usług (np. log = ctx.log).


commands

ctx.commands.command(*, id, title, shortcut="", category="", scope="")

Dekorator rejestrujący polecenie i wiążący opakowaną funkcję jako jego handler.

  • id (str, wymagane): unikalny identyfikator w stylu odwróconego DNS (np. "wordcount.count"). Te same reguły co w plugin.json — musi być globalnie unikalny.
  • title (str, wymagane): czytelna etykieta pokazywana wszędzie tam, gdzie polecenie się pojawia (menu, paleta poleceń).
  • shortcut (str, opcjonalne): przenośny domyślny łańcuch skrótu, np. "Ctrl+Alt+W". Ctrl mapuje się automatycznie na główny modyfikator platformy (Cmd na macOS). Puste = brak. Użytkownik może go przemapować lub odpiąć w Preferencje ▸ Skróty; dla poleceń związanych z menu host stosuje remap za Ciebie.
  • category (str, opcjonalne): dowolna etykieta grupująca — pokazywana jako kategoria polecenia w Mapowaniu skrótów.
  • scope (str, opcjonalne): ""/"application" (domyślnie — host wiąże skrót na akcji menu polecenia) albo "pluginWindow" dla skrótów żyjących w UI należącym do wtyczki: host tylko wymienia polecenie w maperze; czytaj ctx.commands.effective_shortcut(id) przy budowie swoich widżetów i stosuj ponownie na zdarzeniu "command.shortcutChanged".
ctx.commands.effective_shortcut(command_id) -> str

Skrót aktualnie obowiązujący po remapach użytkownika i rozstrzygnięciu konfliktów ("" = niezwiązany lub nieznany). Bezpieczny do wywołania z handlerów.

Opakowana funkcja nie przyjmuje argumentów i nic nie zwraca. Wyjątki w jej wnętrzu łapie worker i loguje na stderr; nie wywracają edytora.

@ctx.commands.command(id="wordcount.count",
                      title="Count Words",
                      shortcut="Ctrl+Alt+W",
                      category="Text")
def _count():
    ctx.status.show("counted")

Pod maską: strona Pythona alokuje lokalnie handlerId, rejestruje callback na połączeniu RPC i wysyła do hosta commands.registerCommand. Gdy użytkownik aktywuje polecenie (klik w menu, skrót, paleta), host wysyła RPC callback i Twoja funkcja się wykonuje.

Identyfikatory poleceń są globalne dla procesu, nie per wtyczka. Prefiksuj je swoją przestrzenią nazw odwróconego DNS (wordcount.…), aby uniknąć kolizji.


Ścieżki menu używają / jako separatora. Skrajny lewy segment to menu najwyższego poziomu (Plugins, Tools, …); segmenty pośrednie tworzą podmenu na żądanie; ostatni segment to etykieta pozycji.

ctx.menus.add_item(path, command_id)

ctx.menus.add_item("Plugins/Word Count/Count", "wordcount.count")
  • path (str): "Top/Sub/…/Etykieta". Pośrednie podmenu powstają, jeśli nie istnieją.
  • command_id (str): id wcześniej zarejestrowanego polecenia. Klik w pozycję wywołuje polecenie.

Zwraca nieprzezroczysty łańcuch handlerId. Zachowaj go, jeśli będziesz później potrzebować unsubscribe pozycji (rzadkie w Fazie 1).

Skrajny lewy segment ("Plugins" powyżej) musi odpowiadać title jednego z Twoich wpisów menus[] w plugin.json, aby rozmieszczenie na pasku menu zostało przejęte.

ctx.menus.add_separator(path)

ctx.menus.add_separator("Plugins/Word Count")

Wstawia separator do istniejącego podmenu.

ctx.menus.set_item_checked(handler_id, checked)

Zamienia dodaną wcześniej pozycję w zaznaczalny wpis menu i ustawia jego stan — standardowy idiom trwałego przełącznika wł./wył. z widocznym znacznikiem.

item = ctx.menus.add_item("View/Auto-open Preview",
                          "myplugin.toggleAutoOpen")

# Seed the check mark from the persisted setting so it matches
# reality before the user opens the menu.
ctx.menus.set_item_checked(item, ctx.settings.get("autoOpen", True))

@ctx.commands.command(id="myplugin.toggleAutoOpen",
                      title="Toggle Auto-Open")
def _toggle():
    on = not ctx.settings.get("autoOpen", True)
    ctx.settings.set("autoOpen", on)
    ctx.menus.set_item_checked(item, on)

Pierwsze wywołanie promuje pozycję do zaznaczalnej; kolejne tylko przełączają znacznik. Podanie handler_id, którego host nie zna, to cichy no-op (bezpieczne po zrzuceniu tokenu, np. z opóźnionego callbacku podczas zamykania).


events

Subskrypcja zdarzeń edytora. Dwa równoważne API — dekoratory-cukier są zalecane dla stałego zestawu tematów Fazy 1; surowe subscribe() przydaje się, gdy temat liczysz w czasie działania.

Dekoratory-cukier

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

@ctx.events.on_document_opened
def _opened(ev):
    ctx.log.info(f"opened {ev.path}")

Opakowana funkcja przyjmuje jeden argument: typowaną dataclass odpowiadającą tematowi. Pełną tabelę temat ↔ dataclass znajdziesz w dokumentacji zdarzeń.

Surowe API

handler_id = ctx.events.subscribe("document.saved", _saved)
# later:
ctx.events.unsubscribe(handler_id)

Dostępne właściwości-cukier (wszystkie dekoratory):

Właściwość Odpala się przy
on_document_opened Dokument otwarty w karcie
on_document_closed Dokument zamknięty
on_document_saved Dokument zapisany na dysk
on_selection_changed Kursor / zaznaczenie przesunięte w aktywnym dokumencie
on_active_document_changed Zmieniona karta pierwszoplanowa
on_app_about_to_quit Edytor się zamyka — ostatnia szansa na działanie
on_command_shortcut_changed Zmienił się efektywny skrót polecenia (remap użytkownika / import keymapy / rozstrzygnięcie konfliktu); ładunek: command_id, sequence ("" = niezwiązany)

Nieznane nazwy tematów rzucają ValueError przy subskrypcji. Subskrybenci Fazy 1 mogą działać po odpowiednim handlerze natywnym; traktuj dostarczanie zdarzeń informacyjnie, nie autorytatywnie.


log

ctx.log.trace  ("very-fine-grained")
ctx.log.debug  ("dev-only detail")
ctx.log.info   ("routine event")
ctx.log.warning("recoverable weirdness")
ctx.log.error  ("something is wrong")

Każdy poziom mapuje się na ILogger hosta. Linie mają prefiks [python:<plugin.id>], więc Twoje wyjście łatwo znaleźć w logu diagnostycznym edytora:

[python:org.example.wordcount] counted 42 words

Nigdy nie używaj print() we wtyczce — stdout workera to kanał RPC i pisanie do niego psuje protokół. ctx.log.* to jedyny usankcjonowany sposób emitowania tekstu diagnostycznego.


status

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

Wysyła przejściowe powiadomienie na pasek stanu.

  • message (str): co pokazać. Tylko krótkie łańcuchy — pasek stanu ma stałą szerokość.
  • timeout_ms (int, domyślnie 0): jak długo trzymać na ekranie; 0 pozwala hostowi wybrać rozsądny domyślny czas (kilka sekund).
  • level (str, domyślnie "info"): "info" / "warning" / "error". Wyższe poziomy mogą trzymać minimum ekranowe nieco dłużej, aby błąd nie został natychmiast zmieciony przez rutynowy komunikat info.
ctx.status.show("saved", timeout_ms=1500)
ctx.status.show("no results", level="warning")

ctx.status.show działa w trybie „wyślij i zapomnij". Jeśli host nie ma widocznego paska stanu (build headless / minimalny), wywołanie jest po cichu porzucane.



editor

Pełny synchroniczny dostęp do aktywnych dokumentów. Każda metoda blokuje wątek wywołujący do odpowiedzi hosta (zwykle mikrosekundy, gdy worker i edytor dzielą maszynę). Legalne z każdego handlera polecenia / zdarzenia; nigdy z callbacku async def (zobacz notkę o wątkach na górze tej strony).

Enumeracja dokumentów

doc  = ctx.editor.active_document()      # 0 when no document is open
docs = ctx.editor.open_documents()       # list[int]
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc)         # str or None for untitled

Dostęp do bufora

text  = ctx.editor.text(doc)                        # whole buffer
slice = ctx.editor.text_range(doc, start, end)      # [start, end) bytes
n     = ctx.editor.length(doc)                      # int64
ctx.editor.set_text(doc, "...")

text_range unika ciągnięcia całego bufora przez RPC, gdy potrzebujesz tylko wycinka — preferuj go do sortowań/transformacji na dużych plikach.

Zaznaczenie

sel = ctx.editor.selection_range(doc)               # mte.MatchRange(start, end)
s   = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "new text")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))

Kursor

pos = ctx.editor.caret(doc)                         # mte.EditorPosition(line, column)
ctx.editor.set_caret(doc, mte.EditorPosition(line=7, column=3))

Operacje plikowe

new_doc = ctx.editor.open_file("/path/to/file")     # doc-id or 0 on failure
ok      = ctx.editor.save_document(doc)
dirty   = ctx.editor.is_modified(doc)

Wyszukiwanie

opts = mte.SearchOptions(match_case=True, mode="regex")
hit  = ctx.editor.find_in_range(doc, r"TODO\(.*\)", 0,
                                ctx.editor.length(doc), opts)
if hit.valid:
    new_end = ctx.editor.replace_target(doc, hit, "DONE()", opts)

SearchOptions.mode: "normal" / "extended" / "regex". SearchOptions.direction: "forward" / "backward". Pełną listę pól ma mte.SearchOptions.

Wcięcia i składnia

style = ctx.editor.indentation(doc)                 # IndentationStyle(use_tabs, width)
ctx.editor.set_indentation(doc,
    mte.IndentationStyle(use_tabs=False, width=2))

lang  = ctx.editor.colorizer_id(doc)                # e.g. "python", "" if none

settings

Trwały, typowany magazyn klucz/wartość, zakres plugins/<twoje.id>/ po stronie hosta. Wszystkie metody synchroniczne; typ get/set 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)             # returns bool
count = ctx.settings.get("count", 0)                # returns int
theme = ctx.settings.get("theme", "light")          # returns str

Typy:

Typ Pythona default Typ przewodowy Typ zwracany z get()
bool bool bool
int (nie-bool) int int
float double float
str (lub None) string str

Ponadto:

ctx.settings.contains("wrap")           # bool
ctx.settings.remove("stale_key")
ctx.settings.sync()                     # optional; host also syncs on shutdown

Na zmiany dokonane w oknie Preferencji reaguj subskrypcją SettingsChanged (zobacz dokumentację zdarzeń; pełna powierzchnia zdarzeń to Faza 3).

app_settings

Widok tylko do odczytu konfiguracji całego edytora. Nazwy kluczy żyją w nagłówku C++ PluginApi/AppSettingsKeys.h; autorytatywną listę znajdziesz w nagłówku.

tab_width = ctx.app_settings.get("editor.tabWidth", 4)
theme     = ctx.app_settings.get("editor.theme.variant", "light")

Settery, remove() i sync() na ctx.app_settings rzucają RuntimeError — widok jest celowo tylko do odczytu.


completions

Rejestracja dostawcy uzupełnień uczestniczącego w okienku edytora:

@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
    if not req.prefix:
        return []
    return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
            for w in ("hello", "hello_world", "helper")
            if w.startswith(req.prefix)]

Opakowana funkcja jest wywoływana przy każdym naciśnięciu klawisza (lub Ctrl+Space), gdy dokument pasuje do filtra języka. Pola req:

Pole Typ Znaczenie
document int Edytowany dokument
prefix str Fragment słowa tuż przed kursorem
position int Bajtowe przesunięcie kursora
language_id str Id kolorizera ("python", "cpp", …)
manual bool True, gdy użytkownik nacisnął Ctrl+Space; False przy auto-wyzwoleniu

Zwróć listę mte.CompletionItem (albo gołych łańcuchów, albo dictów {text, detail, kind}). Rodzaje: mte.KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.

Deadline

Host wysyła Twojemu dostawcy żądanie klawiszowe z deadlinem ~30 ms. Jeśli callback go przekroczy, okienko pokaże się bez Twoich kandydatów dla tego naciśnięcia. Buduj kosztowne indeksy w tle i odpowiadaj z cache — nigdy nie rób I/O w callbacku. Uzupełnianie sieciowe w stylu LSP należy do przyszłej powierzchni async, nie tutaj.

languages=[] (albo pominięte) znaczy „każdy dokument". Wiele dostawców na wtyczkę jest dozwolone; każdy dekorator rejestruje własnego.


data_dir

path = ctx.data_dir()                    # pathlib.Path

Zwraca zapisywalny katalog per wtyczka, który host już dla Ciebie utworzył. Używaj na cache, indeksy i wszelkie dane, które posiadasz. ctx.diagnostics_dir() analogicznie zwraca wspólny katalog awarii/logów (dla Ciebie tylko do odczytu — pisze tam raporter awarii).


docks

Deklaratywne panele dokowane — boczne UI budowane ze specyfikacji formularza, nie z widżetów Qt. Słownik kreatorów opisuje mte.ui.

from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button

def register(ctx):
    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="renamer.panel",
        title="Renamer",
        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="renamer.run")]),
        ]),
    )

ctx.docks.add_panel(*, id=None, title="", area="right", form, initially_visible=True) -> str

Rejestruje panel dokowany. Zwraca id panelu (auto-alokowane, gdy id to None).

  • id (str, opcjonalne) — stabilny identyfikator używany przez ctx.ui.get / ctx.ui.set do adresowania kontrolek w tym panelu. Bez niego host alokuje id py-dock/N i je zwraca.
  • title (str) — pokazywany na nagłówku doku.
  • area (str)"left" / "right" / "top" / "bottom". Nieznane wartości spadają na "right".
  • form (dict) — specyfikacja zbudowana kreatorami mte.ui.
  • initially_visible (bool) — czy dok jest widoczny przy ładowaniu wtyczki. Domyślnie True. Użytkownik może go potem ukryć; nie możesz go wymusić z powrotem.

ctx.docks.on_change(callback) -> str

Alokuje id handlera do podpięcia pola on_change kontrolki pod pythonową funkcję. Callback dostaje nową wartość jako łańcuch; kontrolki logiczne dostarczają "true" / "false", liczbowe — liczbę jako łańcuch.

def _on_volume(value: str) -> None:
    ctx.log.info(f"volume={int(value)}")

handler = ctx.docks.on_change(_on_volume)
form = Column([NumberSpin(id="volume", default=5, on_change=handler)])

Idy handlerów on_change mają prefiks py-dock-cb/N i nigdy nie kolidują z idami handlerów poleceń / zdarzeń / uzupełnień.

Callback wykonuje się w pętli zdarzeń workera, NIE w puli wątków — niech będzie szybki. Użyj ctx.log, zmień mały stan w pamięci albo odpal polecenie następcze; nie rób tu ciężkiej pracy.


settings_page

Dodaje stronę w oknie Preferencji edytora. Ten sam słownik specyfikacji formularza co panele dokowane; ta sama trwałość binds="settings:<klucz>".

from mte.ui import Column, Checkbox, NumberSpin

def register(ctx):
    ctx.settings_page.register(
        category="Plugins/Renamer",       # slash-separated tree path
        title="Renamer",
        form=Column([
            Checkbox(id="cs", label="Case sensitive by default",
                     binds="settings:caseSensitive"),
            NumberSpin(id="results", default=100, min=1, max=10_000,
                       binds="settings:resultCap"),
        ]),
    )

ctx.settings_page.register(*, category, title, form, scope="") -> str

  • category (str, wymagane) — ścieżka w drzewie Preferencji rozdzielana ukośnikami, np. "Plugins/My Plugin". Zagnieżdżone kategorie stają się węzłami drzewa.
  • title (str, wymagane) — nagłówek nad stroną.
  • form (dict, wymagane) — specyfikacja zbudowana kreatorami mte.ui.
  • scope (str, opcjonalne) — zakres zdarzenia emitowany w SettingsChanged, gdy użytkownik naciśnie OK / Zastosuj.

Zwraca id handlera.

Anuluj NIE wycofuje zmian

Kontrolki z binds="settings:<klucz>" utrwalają wartości na bieżąco w trakcie edycji, więc Anuluj w oknie Preferencji ich nie cofa. To znane ograniczenie Fazy 3; przyszłe udoskonalenie opakuje ustawienia w buforujące proxy.


ui

Czyta i zapisuje wartości kontrolek w panelach, które już zarejestrowałeś. Wartości na przewodzie są zawsze łańcuchami.

def register(ctx):
    panel_id = ctx.docks.add_panel(id="renamer.panel", ...)

    @ctx.commands.command(id="renamer.echo", title="Echo needle")
    def _echo():
        needle = ctx.ui.get(panel_id, "needle") or ""
        cs = (ctx.ui.get(panel_id, "cs") or "false") == "true"
        ctx.status.show(f"needle={needle!r} case={cs}")

    @ctx.commands.command(id="renamer.clear", title="Clear needle")
    def _clear():
        ctx.ui.set(panel_id, "needle", "")

ctx.ui.get(panel_id, control_id) -> Optional[str]

Czyta bieżącą wartość kontrolki. Zwraca None, gdy panel lub kontrolka są nieznane (to nie błąd — traktuj None i "" jednakowo jako „puste").

  • Label / Button / LineEdit / ComboBox — zwraca tekst.
  • Checkbox — zwraca "true" albo "false".
  • NumberSpin — zwraca liczbę jako łańcuch dziesiętny.
  • List — zwraca tekst aktualnie wybranej pozycji (pusty, gdy nic nie wybrano).

ctx.ui.set(panel_id, control_id, value) -> bool

Zapisuje wartość do kontrolki programowo. Zwraca True przy udanym zapisie, False, gdy panel/kontrolka są nieznane albo wartości nie da się skonwertować (np. nie-liczba wysłana do NumberSpin).

Pythonowe True / False są zamieniane na "true" / "false" przed wysłaniem; wszystko inne przechodzi przez str().

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


Podsumowanie wiązań runtime (Faza 3)

Pole specyfikacji Zachowanie w czasie działania
id="…" Adresuj kontrolkę z ctx.ui.get / ctx.ui.set.
binds="settings:<klucz>" Czyta wartość początkową z ctx.settings; zapisuje przy każdej edycji użytkownika.
on_click="<cmd>" Przycisk wywołuje zarejestrowane id polecenia przy kliku.
on_change=handler Odpala callback zarejestrowany przez ctx.docks.on_change(...) przy edycji użytkownika.

Te same reguły obowiązują w panelach dokowanych i na stronach Preferencji.