ctx — kontekst wtyczki¶
register(ctx) otrzymuje jeden argument: PluginContext. Udostępnia
poniższe usługi (całą powierzchnię IPluginContext, która mieści się w
modelu out-of-process). 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 / files / search / bookmarks / groups
ctx.languages # list languages, read / switch a document's language, contribute one
ctx.settings # per-plugin key/value store
ctx.app_settings # editor-wide read-only settings view (keys: mte.keys)
ctx.document_settings # .editorconfig-layered per-document reads
ctx.theme # themeable colour categories for your own UI
ctx.diagnostics # publish squiggles + margin glyphs
ctx.compare # open the side-by-side compare view
ctx.completions # register auto-completion providers
ctx.docks # declarative dock panels (+ set_panel_visible, is_panel_visible)
ctx.line_markers # margin glyphs / line tints that follow edits; the margin-click claim
ctx.settings_page # declarative Preferences pages
ctx.ui # runtime get/set for panel controls
Dwie części natywnego kontraktu nie mają jeszcze odpowiednika w Pythonie: providery hovera (więc pasma priorytetu hovera nie dotyczą wtyczek w Pythonie) i providery menu kontekstowego edytora (więc nie dotyczy ich też obszar tekst / margines menu kontekstowego). Na oba odpowiada się, gdy użytkownik czeka, co wymaga takiego samego traktowania z terminem jak w przypadku providerów uzupełnień, zanim wywołanie w obie strony do workera będzie bezpieczne.
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).
ctx.menus.set_item_enabled(handler_id, enabled)¶
Włącza lub wygasza dodaną wcześniej pozycję. Używaj, gdy polecenie wymaga spełnionego warunku — czegoś do zamknięcia, czegoś do przywrócenia — aby menu pokazywało, że jest niedostępne, zamiast oferować wpis, który po cichu nic nie robi.
item = ctx.menus.add_item("File/Close Thing", "myplugin.closeThing")
# Nothing to close yet:
ctx.menus.set_item_enabled(item, False)
# ... and from every place that changes the state the command needs:
ctx.menus.set_item_enabled(item, thing_is_open)
Wygaszona pozycja pozostaje widoczna, ale nie da się jej uruchomić — ani
kliknięciem, ani skrótem. Wywołanie jest idempotentne, więc nigdy nie
musisz pamiętać poprzedniego stanu, a nieznany handler_id to cichy no-op
jak wyżej. Pozycje startują jako aktywne: jedno wywołanie tuż po
add_item ustawia pozycję niedostępną zaraz po starcie.
events¶
Subskrypcja zdarzeń edytora. Dwa równoważne API — dekoratory-cukier są
zalecane; 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_document_modified_changed |
Przełączyła się flaga czysty/brudny (modified: bool) |
on_text_changed |
Każda edycja (position, added_bytes, removed_bytes) — trzymaj trywialny |
on_settings_changed |
Zastosowano Preferencje (scope: "" cały edytor, "plugins/<id>") |
on_theme_changed |
Zmieniono lub edytowano motyw (variant) — odczytaj ponownie kolory z ctx.theme |
on_workspace_root_changed |
Otwarto / zamknięto folder workspace (root) |
on_workspace_files_changed |
Zmienił się zbiór plików workspace (files) |
on_host_window_shown |
Raz na uruchomienie, po pierwszej klatce — tu planuj wolną pracę startową |
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
mogą działać po odpowiednim handlerze natywnym; traktuj dostarczanie
zdarzeń informacyjnie, nie autorytatywnie.
Publikowanie¶
ctx.events.publish(topic, payload) rozgłasza do subskrybentów natywnych i
pythonowych — akceptowane tylko dla dwóch tematów workspace
(workspace.rootChanged, workspace.filesChanged); cokolwiek innego rzuca
ValueError. Zobacz dokumentację zdarzeń.
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. ctx.status.clear() usuwa aktualnie wyświetlane tymczasowe
powiadomienie.
Trwałe segmenty¶
Do stanu na żywo (licznik słów, status serwera) użyj segmentu, którego jesteś właścicielem i który odświeżasz — nigdy nie nadpisuje powiadomienia i odwrotnie:
seg = ctx.status.add_segment("wordcount.total", priority=100)
seg.set_text("2,431 words")
seg.set_visible(False) # hide without giving it up
seg.remove() # take it off the bar (idempotent)
priority porządkuje segmenty wtyczek od lewej do prawej (niższa = bardziej
na lewo; remisy zachowują kolejność rejestracji). Zamknięcie wtyczki usuwa
wszystko, co wciąż trzyma.
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))
ranges = ctx.editor.selection_ranges(doc) # list[MatchRange]: one per caret
block = ctx.editor.is_rectangular_selection(doc) # Alt+drag column block?
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
here = ctx.editor.open_file_at("/path/to/file", mte.EditorPosition(41, 0))
ctx.editor.reveal_line(doc, 120) # scroll there; caret and focus stay
ok = ctx.editor.save_document(doc)
closed = ctx.editor.close_document(doc) # prompts if modified; False = user cancelled
dirty = ctx.editor.is_modified(doc)
ctx.editor.set_modified(doc, True) # force the tab asterisk
ro = ctx.editor.is_read_only(doc) # e.g. Monitoring (tail -f) mode
enc = ctx.editor.document_encoding(doc) # mte.DocumentEncoding, e.g. UTF8_BOM
ctx.editor.set_document_encoding(doc, mte.DocumentEncoding.UTF8) # takes effect on the next save
eol = ctx.editor.line_ending(doc) # mte.LineEnding, e.g. CRLF
ctx.editor.set_line_ending(doc, mte.LineEnding.LF) # Enter inserts it; the next save writes it
scratch = ctx.editor.create_untitled("text", "Untitled-3") # new buffer, doc-id
sid = ctx.editor.stable_id(doc) # survives Save As; compare with ==
clip = ctx.editor.clipboard_text() # system clipboard, "" if no text
# Declare that you snapshot unsaved buffers (the host then stops asking
# "save changes?" when the window closes):
ctx.editor.set_unsaved_changes_preserved(True)
ctx.editor.unsaved_changes_preserved() # bool, global flag
open_file_at to jedno wywołanie typu „zabierz tam użytkownika" (błąd
kompilacji, ramka stosu, wynik wyszukiwania): otwiera plik albo aktywuje jego
kartę, stawia kursor bez zaznaczenia, rozwija zwinięty fragment, który ukrywa
wiersz, przewija wiersz do widoku — na środek, gdy był poza ekranem — i daje
edytorowi fokus. Wiersz i kolumna liczone są od 0 (kolumna w bajtach UTF-8) i
są przycinane, nigdy odrzucane; zwraca id dokumentu albo 0, gdy pliku nie
udało się otworzyć — wtedy nic się nie przesunęło. reveal_line tylko
pokazuje wiersz: nie przesuwa kursora, nie przełącza kart i nie zabiera
fokusu.
Zakładki, znaczniki, podświetlanie URL¶
ctx.editor.toggle_bookmark(doc, line) # 0-based line
lines = ctx.editor.bookmarked_lines(doc) # list[int]
nxt = ctx.editor.next_bookmark(doc, from_line, wrap=True) # -1 = none
prv = ctx.editor.previous_bookmark(doc, from_line, wrap=True)
ctx.editor.clear_bookmarks(doc)
ctx.editor.mark_range(doc, mte.MatchRange(4, 9)) # "mark" indicator
ctx.editor.clear_marks(doc)
ctx.editor.set_url_highlight_enabled(doc, True)
ctx.editor.is_url_highlight_enabled(doc)
url = ctx.editor.url_at_caret(doc) # "" when none
ctx.editor.open_url_at_caret(doc) # bool
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.
Wyszukiwanie przyrostowe (wzorzec rośnie w miarę pisania, od przypiętej kotwicy):
ctx.editor.set_incremental_search_anchor(doc)
hit = ctx.editor.incremental_search(doc, "TOD", opts) # MatchRange
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))
# Where word-wrapped continuation rows start:
# "left" (window edge) / "same" (align with the line; default) / "indent"
ctx.editor.set_wrap_indent(doc, "same")
mode = ctx.editor.wrap_indent(doc) # same strings back
lang = ctx.editor.colorizer_id(doc) # e.g. "python", "" if none
# Semantic classification from the active colorizer (cheap, no re-lex):
# list[mte.CategorySpan(start, length, category)] with category names
# "comment" / "string" / "keyword" / ... -- a spell checker keeps to
# comments and strings this way.
spans = ctx.editor.text_categories(doc, 0, ctx.editor.length(doc))
ctx.editor.trigger_completion(doc) # pop the list as if Ctrl+Space
Widok dzielony / grupy kart¶
n = ctx.editor.group_count() # >= 1
g = ctx.editor.group_of(doc) # -1 if not open
w = ctx.editor.group_weights() # relative widths
ctx.editor.move_document_to_group(doc, 1) # creates trailing groups on demand
ctx.editor.set_group_weights([2, 1])
ctx.editor.set_document_tab_index(doc, 0) # reorder within its pane
gs = ctx.editor.groups_of(doc) # EVERY pane showing it
ctx.editor.show_document_in_group(doc, 1) # split it into pane 1
Dokument pokazany w dwóch panelach to jeden dokument z jednym id,
więc group_of() nazywa tylko jedno z jego miejsc. groups_of() zwraca
wszystkie, a show_document_in_group() tworzy drugi współdzielony
widok — dokładnie to, co robi Widok ▸ Podziel dokument w prawo — więc
nie pada zdarzenie document_opened.
ctx.editor odzwierciedla teraz natywne IEditorService jeden do jednego.
languages¶
Kolorowanie składni to funkcja core — host rozpoznaje język dokumentu i
koloruje go bez żadnej wtyczki. ctx.languages pozwala się dołączyć: wylicz,
co host zna, odczytaj lub nadpisz język dokumentu, uruchom ponownie
rozpoznawanie i wnieś całe definicje języków.
for lang in ctx.languages.available(): # list[mte.LanguageInfo(id, name)]
...
lang_id = ctx.languages.language_for(doc) # "" when uncoloured
ok = ctx.languages.set_language_for(doc, "json") # "" clears; False = unknown
ctx.languages.auto_detect_for(doc) # undo a manual override
handler = ctx.languages.register(
id="mylang", name="My Language", lexer="cpp",
extensions=["mylang", "ml2"],
keywords=[{"group": "Primary keywords and identifiers",
"words": "if else while"}],
styles={"SCE_C_COMMENT": "comment", "SCE_C_STRING": "string"})
...
ctx.languages.unregister(handler) # restores what it displaced
register przyjmuje ten sam kształt co dostarczony plik colorizers/*.json
(albo cały słownik definition=): lexer wskazuje istniejący lekser backendu
(host nie zyskuje nowych lekserów w czasie działania), styles mapuje
nazwy stylów tego leksera na kategorie semantyczne, a keywords adresuje
jego listy słów przez opis slotu leksera — host rozwiązuje oba dokładnie jak
dla dostarczonego pliku, więc nigdy nie wpisujesz id silnika. Rejestracja
istniejącego id zastępuje je na tak długo, jak trzymasz handler id;
zamknięcie wtyczki wycofuje wszystko, co wciąż trzyma.
document_settings¶
Tylko do odczytu, ustawienia per dokument z warstwą .editorconfig projektu
nad Twoimi ctx.settings. Dla klucza k i dokumentu doc odczyt rozwiązuje
mte.<plugin.id>.<k> z .editorconfig obowiązującego dla tego dokumentu,
potem ctx.settings, potem Twój default:
wrap = ctx.document_settings.get_bool(doc, "wrap", False)
width = ctx.document_settings.get_int(doc, "width", 80)
ratio = ctx.document_settings.get_double(doc, "ratio", 1.0)
name = ctx.document_settings.get_string(doc, "profile", "")
ctx.document_settings.contains(doc, "wrap") # set in either layer?
Zapisy idą przez ctx.settings — nigdy z powrotem do .editorconfig.
diagnostics¶
Wypchnij podkreślenia i glify na marginesie dla dokumentu — z lintera,
serwera języka, logu builda. Host je renderuje; Ty nigdy nie dotykasz
widoku. Publikacja zastępuje wszystko, co wcześniej opublikowałeś dla pary
(dokument, source), więc wysyłaj za każdym razem pełny zestaw:
ctx.diagnostics.publish(doc, "mylinter", [
mte.Diagnostic(start=12, end=18, message="unused variable",
severity=mte.SEVERITY_WARNING, code="W0612"),
])
ctx.diagnostics.publish(doc, "mylinter", []) # retract only your own
ctx.diagnostics.clear(doc) # clears EVERY producer's set on doc
source jest wymagane — to ono oddziela Twój zestaw od zestawów innych
producentów (wtyczka LSP, spell checker) na tym samym dokumencie. Poziomy:
SEVERITY_ERROR / SEVERITY_WARNING / SEVERITY_INFORMATION /
SEVERITY_HINT. Offsety to półotwarte zakresy bajtów [start, end).
theme¶
Zarejestruj kolory, którymi maluje Twoje UI, jako nazwane kategorie z motywu; Edytor motywów wyświetli je automatycznie, a nadpisania użytkownika krążą w pliku motywu:
h = ctx.theme.register_category("com.example.my/match", "Match highlight",
default_light=0xFFF3B0, default_dark=0x8A6D00)
rgb = ctx.theme.resolve_color("com.example.my/match") # 0xRRGGBB int
themed = ctx.theme.has_override("com.example.my/match")
variant = ctx.theme.active_variant() # "light" / "dark" / ""
cats = ctx.theme.categories() # list[mte.ThemeCategory]
ctx.theme.unregister_category(h)
Odczytaj ponownie resolve_color na ctx.events.on_theme_changed.
compare¶
Otwórz (lub sfokusuj) widok porównania obok siebie nad dwoma źródłami.
Każda strona to plik na dysku (path=), dosłowny tekst (content=) albo
oba (pokazywany jest content, a ścieżka daje utrwalaniu sesji tożsamość do
odtworzenia):
shown = ctx.compare.open(
mte.CompareSource("before", path="/tmp/a.txt"),
mte.CompareSource("after", content=new_text),
ignore_whitespace=True) # ignore_case / ignore_eol too
Zwraca False, gdy którejś strony nie da się odczytać.
settings¶
Trwały, typowany magazyn klucz/wartość, zakres plugins/<your.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ą
settings.changed (ctx.events.on_settings_changed; zobacz
dokumentację zdarzeń).
app_settings¶
Widok tylko do odczytu konfiguracji całego edytora. Dobrze znane nazwy
kluczy są wyeksportowane jako stałe w mte.keys (odzwierciedlają nagłówek
C++ PluginApi/AppSettingsKeys.h, który pozostaje listą autorytatywną):
import mte
size = ctx.app_settings.get(mte.keys.EDITOR_FONT_SIZE, mte.keys.DEFAULT_EDITOR_FONT_SIZE)
family = ctx.app_settings.get(mte.keys.EDITOR_FONT_FAMILY, "") # "" = host default
theme = ctx.app_settings.get(mte.keys.UI_THEME, mte.keys.DEFAULT_UI_THEME) # "system"/"light"/"dark"
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, proportional_size=0.0, tab_group="") -> 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. PodajFalsedla panelu, który nie ma wyskakiwać przy starcie, i pokaż go później przezset_panel_visible.proportional_size(float) — domyślny rozmiar panelu jako udział w oknie głównym: jego szerokości w obszarach"left"/"right", jego wysokości w"top"/"bottom".0.0(domyślnie) zachowuje zachowanie pikselowe, którego chce panel narzędziowy — panel trzyma swój rozmiar, a zmiana rozmiaru okna go nie rusza. Ułamek w rodzaju0.4to, czego chce podgląd: maksymalizacja okna poszerza panel razem z nim. Twoja jest tylko wartość domyślna; gdy użytkownik przeciągnie separator panelu, host przyjmuje i pamięta jego udział. Wartości spoza zakresu są przycinane, a host zachowuje minimalny rozmiar i dla panelu, i dla edytora. Pełne reguły opisujeproportionalSize.tab_group(str) — panele z tym samym niepustym identyfikatorem grupy w tym samymareadzielą jeden dok jako karty zamiast leżeć obok siebie. Nadaj mu przestrzeń nazw jak identyfikatorowi panelu ("renamer/panels"). To tylko układ domyślny: wygrywa układ zapisany przez użytkownika. ZobacztabGroup.
ctx.docks.is_panel_visible(handler_id) -> bool¶
Czy panel dodany przez add_panel jest otwarty — panel schowany jako
karta za innym liczy się jako otwarty; panel zarejestrowany z
initially_visible=False i nigdy niepokazany, panel zamknięty oraz nieznane
id — nie. Połącz to z set_panel_visible, aby pokazać swoje panele na czas
zadania, a potem przywrócić to, co miał użytkownik:
was_open = ctx.docks.is_panel_visible(panel)
ctx.docks.set_panel_visible(panel, True)
# ... when the task ends:
ctx.docks.set_panel_visible(panel, was_open)
ctx.docks.set_panel_visible(handler_id, visible)¶
Pokazuje lub ukrywa panel dodany przez add_panel. Pokazanie podnosi też
panel ponad doki dzielące z nim obszar, więc polecenie, które „otwiera"
swój panel, naprawdę stawia go przed oczami użytkownika. Idempotentne —
tę samą wartość powtarzaj dowolnie, bez śledzenia stanu; nieznane id jest
po cichu ignorowane.
panel = ctx.docks.add_panel(id="mine.panel", title="Mine", form=form,
initially_visible=False)
@ctx.commands.command("mine.show", title="Show My Panel")
def _show():
ctx.docks.set_panel_visible(panel, True)
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.
line_markers¶
Glify na marginesie i zabarwienie całego wiersza przypięte do wierszy dokumentu, które podążają za edycjami użytkownika — punkty przerwania, strzałka bieżącej instrukcji, paski zmian — oraz przejęcie kliknięcia w kolumnie zakładek. Natywną usługę i wszystkie jej reguły opisuje strona Znaczniki wierszy i kliknięcie marginesu.
bp_colour = ctx.theme.register_category("myplugin.breakpoint", "Breakpoint",
default_light=0xCC0000,
default_dark=0xFF5555)
bp = ctx.line_markers.register_kind(glyph=mte.GLYPH_CIRCLE,
glyph_color="myplugin.breakpoint",
line_tint="", z_order=50)
marker = ctx.line_markers.add_marker(bp, doc, line) # 0 = refused
ctx.line_markers.line_of(marker) # where it is NOW; -1 = gone
ctx.line_markers.markers(bp, doc) # [mte.LineMarkerPlacement(id, line)]
ctx.line_markers.remove_marker(marker)
ctx.line_markers.clear_markers(bp, doc) # doc 0 = every document
ctx.line_markers.release(bp) # removes the kind and its markers
register_kind(glyph=, glyph_color=, line_tint=, z_order=)—glyphto jedna z wartościmte.GLYPH_NONE(samo zabarwienie),GLYPH_CIRCLE,GLYPH_CIRCLE_HOLLOW,GLYPH_CIRCLE_BAR,GLYPH_ARROW,GLYPH_STRIP; kolory to identyfikatory kategorii zarejestrowanych przez Ciebie wctx.theme. Edytor ma tylko kilka miejsc na znaczniki dla wszystkich wtyczek razem, więc to wywołanie może się nie udać — wtedy zgłaszaRpcError. Rejestruj swoje rodzaje raz, wregister().add_marker(kind, doc, line)— wiersz liczony od 0, nieprzycinany: wiersz spoza dokumentu zwraca 0 i niczego nie stawia.- Znaczniki przesuwają się razem ze swoimi wierszami, przeżywają ponowne
wczytanie pliku na tym samym numerze wiersza i znikają razem z dokumentem —
dodawaj swoje ponownie w
ctx.events.on_document_opened.
Kliknięcie marginesu¶
@ctx.events.on_margin_clicked
def _clicked(ev): # mte.MarginClicked
if ev.modifiers & mte.MODIFIER_CONTROL:
edit_condition(ev.document, ev.line)
else:
toggle_breakpoint(ev.document, ev.line)
claim = ctx.line_markers.claim_margin_click()
Każde kliknięcie lewym przyciskiem w kolumnie zakładek jest publikowane jako
margin.clicked (document, line, margin = "symbol", modifiers —
bity mte.MODIFIER_SHIFT / MODIFIER_CONTROL / MODIFIER_ALT /
MODIFIER_META). Domyślnie edytor przełącza przy tym zakładkę; dopóki
dowolna wtyczka trzyma claim_margin_click(), nie robi tego i zostawia
kliknięcie wtyczkom. ctx.line_markers.release(claim) oddaje kliknięcie.
settings_page¶
Dodaje stronę w oknie Preferencji edytora. Ten sam słownik specyfikacji
formularza co panele dokowane; ta sama trwałość binds="settings:<key>".
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:<key>" 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:<key>" |
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.