Przejdź do treści

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

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

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:

[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. 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

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, 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 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. Podaj False dla panelu, który nie ma wyskakiwać przy starcie, i pokaż go później przez set_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 rodzaju 0.4 to, 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 opisuje proportionalSize.
  • tab_group (str) — panele z tym samym niepustym identyfikatorem grupy w tym samym area dzielą 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. Zobacz tabGroup.

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=) — glyph to jedna z wartości mte.GLYPH_NONE (samo zabarwienie), GLYPH_CIRCLE, GLYPH_CIRCLE_HOLLOW, GLYPH_CIRCLE_BAR, GLYPH_ARROW, GLYPH_STRIP; kolory to identyfikatory kategorii zarejestrowanych przez Ciebie w ctx.theme. Edytor ma tylko kilka miejsc na znaczniki dla wszystkich wtyczek razem, więc to wywołanie może się nie udać — wtedy zgłasza RpcError. Rejestruj swoje rodzaje raz, w register().
  • 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 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:<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.