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¶
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 wplugin.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".Ctrlmapuje się automatycznie na główny modyfikator platformy (Cmdna 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; czytajctx.commands.effective_shortcut(id)przy budowie swoich widżetów i stosuj ponownie na zdarzeniu"command.shortcutChanged".
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.
menus¶
Ś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)¶
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)¶
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:
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¶
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;0pozwala 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 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¶
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 przezctx.ui.get / ctx.ui.setdo adresowania kontrolek w tym panelu. Bez niego host alokuje idpy-dock/Ni 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 kreatoramimte.ui.initially_visible(bool) — czy dok jest widoczny przy ładowaniu wtyczki. DomyślnieTrue. 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 kreatoramimte.ui.scope(str, opcjonalne) — zakres zdarzenia emitowany wSettingsChanged, 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.