ctx — le contexte du plugin¶
register(ctx) reçoit un seul argument : un PluginContext. Il expose les
services ci-dessous (toute la surface d'IPluginContext qui s'accommode du
modèle hors processus). Chacun est une petite classe Python que le worker
construit au-dessus du canal RPC ; le code source se trouve dans
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
Deux parties du contrat natif n'ont pas encore d'équivalent Python : les fournisseurs de survol (les bandes de priorité du survol ne s'appliquent donc pas aux extensions Python) et les fournisseurs du menu contextuel de l'éditeur (la zone texte / marge du menu contextuel non plus). Les deux répondent pendant que l'utilisateur attend, ce qui exige le même traitement d'échéance que pour les fournisseurs de complétion avant qu'un aller-retour vers le worker soit sûr.
Ne stockez pas ctx dans une variable globale en espérant l'utiliser plus
tard — même si les attributs sont stables pendant la durée de vie du plugin,
traitez ctx comme propre à l'enregistrement. Conservez des références vers
les services individuels si vous en avez besoin (log = ctx.log, par
exemple).
commands¶
Décorateur qui enregistre une commande et lie l'appelable décoré comme son gestionnaire.
id(str, obligatoire) : identifiant unique de style DNS inversé (p. ex."wordcount.count"). Mêmes règles que dansplugin.json— doit être globalement unique.title(str, obligatoire) : libellé lisible affiché partout où la commande apparaît (menus, palette de commandes).shortcut(str, optionnel) : chaîne de raccourci par défaut portable, p. ex."Ctrl+Alt+W".Ctrlcorrespond automatiquement au modificateur principal de la plateforme (Cmdsur macOS). Laissez vide pour aucun. L'utilisateur peut le réaffecter ou le délier dans Préférences ▸ Raccourcis ; pour les commandes liées à un menu, l'hôte applique le remappage pour vous.category(str, optionnel) : étiquette de regroupement libre — affichée comme catégorie de la commande dans le mappeur de raccourcis.scope(str, optionnel) :""/"application"(par défaut — l'hôte lie le raccourci sur l'action de menu de la commande) ou"pluginWindow"pour les combinaisons vivant dans une UI possédée par le plugin : l'hôte se contente de lister la commande dans le mappeur ; lisezctx.commands.effective_shortcut(id)lors de la construction de vos widgets et réappliquez sur l'événement"command.shortcutChanged".
Le raccourci actuellement en vigueur après remappage par l'utilisateur et
résolution des conflits ("" = non lié ou inconnu). Peut être appelé sans
risque depuis les gestionnaires.
La fonction décorée ne prend aucun argument et ne renvoie rien. Les exceptions qu'elle lève sont interceptées par le worker et journalisées sur stderr ; elles ne font pas planter l'éditeur.
@ctx.commands.command(id="wordcount.count",
title="Count Words",
shortcut="Ctrl+Alt+W",
category="Text")
def _count():
ctx.status.show("counted")
Sous le capot : le côté Python alloue localement un handlerId, enregistre
le callback sur la connexion RPC et déclenche commands.registerCommand
vers l'hôte. Quand l'utilisateur active la commande (clic de menu,
raccourci, palette), l'hôte envoie un RPC callback et votre fonction
s'exécute.
Les ids de commandes sont à l'échelle du processus, pas par plugin.
Préfixez vos ids avec votre espace de noms DNS inversé (wordcount.…) pour
éviter les collisions.
menus¶
Les chemins de menu utilisent / comme séparateur. Le segment le plus à
gauche est un menu de premier niveau (Plugins, Tools, …) ; les segments
intermédiaires créent des sous-menus à la demande ; le dernier segment est
le libellé de l'élément.
ctx.menus.add_item(path, command_id)¶
path(str) :"Top/Sub/…/Label". Les sous-menus intermédiaires sont créés s'ils n'existent pas.command_id(str) : id d'une commande enregistrée au préalable. Un clic sur l'élément appelle la commande.
Renvoie une chaîne handlerId opaque. Conservez-la si vous devrez faire un
unsubscribe de l'élément plus tard (rare en phase 1).
Le segment le plus à gauche ("Plugins" ci-dessus) doit correspondre au
title de l'une de vos entrées menus[] dans plugin.json pour que le
placement dans la barre de menus soit pris en compte.
ctx.menus.add_separator(path)¶
Insère un séparateur dans un sous-menu existant.
ctx.menus.set_item_checked(handler_id, checked)¶
Transforme l'élément précédemment ajouté en entrée de menu cochable et définit son état coché — l'idiome standard pour une bascule actif/inactif persistante affichant une coche visible.
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)
Le premier appel promeut l'élément en élément cochable ; les appels
suivants ne font que basculer la coche. Passer un handler_id inconnu de
l'hôte est un no-op silencieux (appel sans risque après l'abandon de votre
jeton, p. ex. depuis un callback retardé se déclenchant pendant l'arrêt).
ctx.menus.set_item_enabled(handler_id, enabled)¶
Active ou grise un élément précédemment ajouté. À utiliser lorsque la commande dépend d'une condition préalable — quelque chose à fermer, quelque chose à rouvrir — afin que le menu montre son indisponibilité au lieu d'offrir une entrée qui ne fait silencieusement rien.
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)
Un élément désactivé reste visible mais ne peut pas être déclenché, ni par
un clic ni par son raccourci. L'appel est idempotent, vous n'avez donc
jamais à mémoriser l'état précédent, et un handler_id inconnu est un no-op
silencieux, comme ci-dessus. Les éléments démarrent activés : un appel juste
après add_item prépare un élément indisponible au démarrage.
events¶
Abonnez-vous aux événements de l'éditeur. Deux API équivalentes — les
décorateurs sucrés sont recommandés ; le subscribe() brut est disponible
si vous voulez calculer le sujet à l'exécution.
Décorateurs sucrés¶
@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}")
La fonction décorée prend un argument : une dataclass typée correspondant au sujet. Voir la référence des événements pour le tableau complet sujet ↔ dataclass.
API brute¶
handler_id = ctx.events.subscribe("document.saved", _saved)
# later:
ctx.events.unsubscribe(handler_id)
Propriétés sucrées disponibles (toutes des décorateurs) :
| Propriété | Se déclenche sur |
|---|---|
on_document_opened |
Document ouvert dans un onglet |
on_document_closed |
Document fermé |
on_document_saved |
Document écrit sur le disque |
on_selection_changed |
Caret / sélection déplacé dans le document actif |
on_active_document_changed |
Onglet de premier plan changé |
on_document_modified_changed |
Le drapeau propre/modifié a basculé (modified: bool) |
on_text_changed |
Chaque modification (position, added_bytes, removed_bytes) — restez trivial |
on_settings_changed |
Préférences appliquées (scope : "" tout l'éditeur, "plugins/<id>") |
on_theme_changed |
Thème changé ou modifié (variant) — relisez les couleurs de ctx.theme |
on_workspace_root_changed |
Dossier workspace ouvert / fermé (root) |
on_workspace_files_changed |
Ensemble de fichiers du workspace modifié (files) |
on_host_window_shown |
Une fois par exécution, après la première image — planifiez ici le travail de démarrage lent |
on_app_about_to_quit |
L'éditeur se ferme — dernière occasion de s'exécuter |
on_command_shortcut_changed |
Le raccourci effectif d'une commande a changé (remappage utilisateur / import de keymap / résolution de conflit) ; charge utile : command_id, sequence ("" = non lié) |
Les noms de sujets inconnus lèvent ValueError au moment de l'abonnement.
Les abonnés peuvent s'exécuter après le gestionnaire natif
correspondant ; traitez la livraison d'événements comme informative, pas
comme faisant autorité.
Publier¶
ctx.events.publish(topic, payload) diffuse aux abonnés natifs et Python
indifféremment — accepté uniquement pour les deux sujets workspace
(workspace.rootChanged, workspace.filesChanged) ; tout le reste lève
ValueError. Voir la référence des événements.
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")
Chaque niveau correspond à l'ILogger de l'hôte. Les lignes sont préfixées
[python:<plugin.id>], de sorte que votre sortie est repérable dans le
journal de diagnostics de l'éditeur :
Ne faites jamais de print() depuis un plugin — le stdout du worker est le
canal RPC, et y écrire corrompt le protocole. ctx.log.* est la seule
manière autorisée d'émettre du texte de diagnostic.
status¶
Publie une notification transitoire dans la barre d'état.
message(str) : ce qu'il faut afficher. Chaînes courtes uniquement — la barre d'état a une largeur fixe.timeout_ms(int, 0 par défaut) : durée d'affichage à l'écran ;0laisse l'hôte choisir une valeur raisonnable (quelques secondes).level(str,"info"par défaut) :"info"/"warning"/"error". Les niveaux supérieurs peuvent prolonger un peu la durée minimale d'affichage afin qu'une erreur ne soit pas immédiatement effacée par un message d'information de routine.
ctx.status.show est de type « lancer et oublier ». Si l'hôte n'a pas de
barre d'état visible (headless / build minimale), l'appel est silencieusement
abandonné. ctx.status.clear() efface la notification transitoire affichée.
Segments persistants¶
Pour un état vivant (un compte de mots, l'état d'un serveur), utilisez un segment que vous possédez et rafraîchissez — il n'écrase jamais une notification et réciproquement :
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 ordonne les segments des extensions de gauche à droite (plus bas
= plus à gauche ; les égalités gardent l'ordre d'enregistrement). L'arrêt de
l'extension retire tout ce qui est encore détenu.
editor¶
Accès synchrone complet au(x) document(s) actif(s). Chaque méthode bloque le
thread appelant jusqu'à la réponse de l'hôte (typiquement quelques
microsecondes quand le worker et l'éditeur partagent la même machine).
Autorisé depuis tout gestionnaire de commande / d'événement ; jamais
depuis un callback async def (voir la note sur les threads en haut de
cette page).
Énumération des documents¶
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
Accès au tampon¶
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 évite de tirer tout le tampon à travers le RPC quand vous
n'avez besoin que d'une tranche — préférez-le pour trier/transformer de
gros fichiers.
Sélection¶
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?
Caret¶
pos = ctx.editor.caret(doc) # mte.EditorPosition(line, column)
ctx.editor.set_caret(doc, mte.EditorPosition(line=7, column=3))
Opérations sur les fichiers¶
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 est l'unique appel pour « emmener l'utilisateur là-bas » (une
erreur de compilation, une trame de pile, un résultat de recherche) : il ouvre
le fichier ou active son onglet, place le curseur sans sélection, déplie un
repli qui masque la ligne, fait défiler la ligne dans la vue — centrée si elle
était hors de l'écran — et donne le focus à l'éditeur. Ligne et colonne
partent de 0 (la colonne en octets UTF-8) et sont bornées, jamais refusées ;
il renvoie l'identifiant du document, ou 0 si le fichier n'a pas pu être
ouvert, et alors rien n'a bougé. reveal_line se contente d'afficher une
ligne : il ne déplace pas le curseur, ne change pas d'onglet et ne prend pas
le focus.
Signets, marques, surlignage d'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
Recherche¶
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". Voir
mte.SearchOptions pour la liste complète des champs.
Recherche incrémentale (le motif grandit au fil de la frappe, depuis une ancre épinglée) :
ctx.editor.set_incremental_search_anchor(doc)
hit = ctx.editor.incremental_search(doc, "TOD", opts) # MatchRange
Indentation et syntaxe¶
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
Vue scindée / groupes d'onglets¶
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
Un document affiché dans deux volets est un seul document, avec un seul
id : group_of() ne peut donc nommer qu'une de ses places. groups_of()
les rend toutes, et show_document_in_group() crée une seconde vue
partagée — exactement ce que fait Affichage ▸ Scinder le document à
droite — si bien qu'aucun événement document_opened n'est émis.
ctx.editor reflète désormais l'IEditorService natif un pour un.
languages¶
La coloration syntaxique est une fonctionnalité du cœur — l'hôte détecte le
langage d'un document et le colore sans aucune extension. ctx.languages
vous permet de vous y joindre : lister ce que l'hôte connaît, lire ou
surcharger le langage d'un document, relancer la détection et contribuer des
définitions de langage complètes.
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 prend la même forme qu'un fichier colorizers/*.json livré (ou un
dict definition= complet) : lexer nomme un lexer backend existant (l'hôte
n'en gagne pas à l'exécution), styles associe les noms de styles de ce
lexer à des catégories sémantiques, et keywords adresse ses listes de mots
par la description de slot du lexer — l'hôte résout les deux exactement comme
pour un fichier livré, vous n'écrivez jamais d'id moteur. Enregistrer un id
déjà existant le remplace tant que vous détenez le handler id ; l'arrêt de
l'extension retire tout ce qui est encore détenu.
document_settings¶
Réglages par document, en lecture seule, qui posent la .editorconfig d'un
projet par-dessus vos ctx.settings. Pour une clé k et un document doc,
une lecture résout mte.<plugin.id>.<k> depuis la .editorconfig qui
s'applique à ce document, puis retombe sur ctx.settings, puis sur votre
valeur par défaut :
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?
Les écritures passent par ctx.settings — jamais vers la .editorconfig.
diagnostics¶
Poussez des soulignements ondulés et des glyphes de marge pour un document —
depuis un linter, un serveur de langage, un journal de build. L'hôte les
rend ; vous ne touchez jamais la vue. Publier remplace tout ce que vous aviez
publié pour la paire (document, source), envoyez donc l'ensemble complet à
chaque fois :
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 est obligatoire — c'est ce qui sépare votre ensemble de ceux des
autres producteurs (extension LSP, correcteur orthographique) sur le même
document. Sévérités : SEVERITY_ERROR / SEVERITY_WARNING /
SEVERITY_INFORMATION / SEVERITY_HINT. Les décalages sont des plages
d'octets semi-ouvertes [start, end).
theme¶
Enregistrez les couleurs que peint votre propre interface comme catégories nommées et thémables ; l'éditeur de thèmes les liste automatiquement et les surcharges de l'utilisateur circulent dans le fichier de thème :
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)
Relisez resolve_color sur ctx.events.on_theme_changed.
compare¶
Ouvre (ou met au premier plan) la vue de comparaison côte à côte de l'éditeur
sur deux sources. Chaque côté est un fichier sur disque (path=), un texte
littéral (content=) ou les deux (le contenu est affiché, le chemin donne à
la persistance de session une identité restaurable) :
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
Renvoie False quand un côté ne peut pas être lu.
settings¶
Magasin clé/valeur typé et persistant, restreint à plugins/<your.id>/ sur
l'hôte. Toutes les méthodes sont synchrones ; get/set sont inférés à partir
du type Python de la valeur par défaut.
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
Types :
Type Python de default |
Type sur le fil | Type de retour de get() |
|---|---|---|
bool |
bool |
bool |
int (non booléen) |
int |
int |
float |
double |
float |
str (ou None) |
string |
str |
Également :
ctx.settings.contains("wrap") # bool
ctx.settings.remove("stale_key")
ctx.settings.sync() # optional; host also syncs on shutdown
Réagissez aux changements effectués dans la boîte de dialogue Préférences en
vous abonnant à settings.changed (ctx.events.on_settings_changed ; voir
la référence des événements).
app_settings¶
Vue en lecture seule de la configuration globale de l'éditeur. Les noms de
clés bien connus sont exportés comme constantes dans mte.keys (miroir de
l'en-tête C++ PluginApi/AppSettingsKeys.h, qui reste la liste faisant
autorité) :
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"
Les setters, remove() et sync() sur ctx.app_settings lèvent
RuntimeError — la vue est volontairement en lecture seule.
completions¶
Enregistrez un fournisseur de complétion qui participe à la popup de l'éditeur :
@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)]
L'appelable décoré est invoqué à chaque frappe (ou Ctrl+Espace) dont le
document correspond au filtre de langage. Champs de req :
| Champ | Type | Signification |
|---|---|---|
document |
int |
Document en cours d'édition |
prefix |
str |
Fragment de mot immédiatement avant le caret |
position |
int |
Offset du caret en octets |
language_id |
str |
Id du coloriseur ("python", "cpp", …) |
manual |
bool |
True quand l'utilisateur a pressé Ctrl+Espace ; False en déclenchement automatique |
Renvoyez une liste de mte.CompletionItem (ou de simples chaînes, ou des
dicts avec {text, detail, kind}). Kinds : mte.KIND_KEYWORD /
KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.
Échéance
L'hôte envoie à votre fournisseur une requête de frappe avec une échéance d'environ 30 ms. Si votre callback la dépasse, la popup s'affiche sans vos candidats pour cette frappe. Construisez tout index coûteux en arrière-plan et répondez depuis le cache — ne faites jamais d'E/S dans le callback. La complétion réseau de style LSP relève d'une future surface asynchrone, pas d'ici.
languages=[] (ou omis) signifie « tous les documents ». Plusieurs
fournisseurs par plugin sont autorisés ; chaque décorateur enregistre le
sien.
data_dir¶
Renvoie le répertoire par plugin accessible en écriture que l'hôte a déjà
créé pour vous. À utiliser pour les caches, les index et toute donnée qui
vous appartient. ctx.diagnostics_dir() renvoie de même le répertoire
partagé des crashs/journaux (en lecture seule pour vous — c'est le
rapporteur de crash qui y écrit).
docks¶
Panneaux ancrables déclaratifs — une UI de panneau latéral construite à
partir d'une spécification de formulaire, pas de widgets Qt. Voir
mte.ui pour le vocabulaire des constructeurs.
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¶
Enregistre un panneau ancrable. Renvoie l'id du panneau (alloué
automatiquement quand id vaut None).
id(str, optionnel) — identifiant stable utilisé parctx.ui.get / ctx.ui.setpour adresser les contrôles de ce panneau. Si vous n'en fournissez pas, l'hôte alloue un idpy-dock/Net le renvoie.title(str) — affiché dans l'en-tête du panneau.area(str) —"left"/"right"/"top"/"bottom". Les valeurs inconnues retombent sur"right".form(dict) — une spécification construite avec les constructeursmte.ui.initially_visible(bool) — si le panneau est affiché au chargement du plugin.Truepar défaut. PassezFalsepour un panneau qui ne doit pas surgir au démarrage, et révélez-le plus tard avecset_panel_visible.proportional_size(float) — la taille par défaut du panneau exprimée en part de la fenêtre principale : sa largeur dans les zones"left"/"right", sa hauteur dans"top"/"bottom".0.0(le défaut) conserve le comportement en pixels que veut un panneau outil : le panneau garde sa taille et un redimensionnement de la fenêtre le laisse tranquille. Une fraction comme0.4est ce que veut un aperçu : maximiser la fenêtre élargit le panneau avec elle. Seul le défaut est à vous ; dès que l'utilisateur fait glisser le séparateur du panneau, l'hôte adopte et mémorise sa part. Les valeurs hors plage sont bornées, et l'hôte garde une étendue minimale pour le panneau comme pour l'éditeur. Les règles complètes sont sousproportionalSize.tab_group(str) — les panneaux qui ont le même identifiant de groupe non vide dans la mêmeareapartagent un seul panneau ancré, sous forme d'onglets, au lieu de se placer côte à côte. Préfixez-le comme un identifiant de panneau ("renamer/panels"). Ce n'est que la disposition par défaut : une disposition enregistrée par l'utilisateur l'emporte. VoirtabGroup.
ctx.docks.is_panel_visible(handler_id) -> bool¶
Indique si un panneau ajouté avec add_panel est ouvert — un panneau en
onglet derrière un autre compte comme ouvert ; un panneau enregistré avec
initially_visible=False et jamais affiché, un panneau fermé et un
identifiant inconnu, non. Associez-le à set_panel_visible pour afficher vos
panneaux le temps d'une tâche, puis remettre ce que l'utilisateur avait :
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)¶
Révèle ou masque un panneau ajouté avec add_panel. L'afficher le remonte
aussi au-dessus des docks qui partagent sa zone, pour qu'une commande qui
« ouvre » son panneau le mette vraiment sous les yeux de l'utilisateur.
Idempotent — répétez la même valeur librement, aucun état à suivre ; un id
inconnu est ignoré en silence.
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¶
Alloue un id de gestionnaire pour relier le champ on_change d'un contrôle
à un appelable Python. Le callback reçoit la nouvelle valeur sous forme de
chaîne ; les contrôles booléens livrent "true" / "false", les contrôles
numériques livrent l'entier sous forme de chaîne.
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)])
Les ids de gestionnaires on_change sont préfixés py-dock-cb/N et
n'entrent jamais en collision avec les ids de gestionnaires de commandes /
événements / complétions.
Le callback est invoqué sur la boucle d'événements du worker, PAS sur le
pool de threads — restez rapide. Utilisez ctx.log, modifiez un petit état
en mémoire ou déclenchez une commande de suivi ; ne faites pas de travail
lourd ici.
line_markers¶
Des glyphes de marge et des teintes de ligne entière attachés aux lignes du document, qui suivent les modifications de l'utilisateur — points d'arrêt, flèche de l'instruction courante, bandes de modifications — ainsi que la revendication du clic dans la colonne des signets. Le service natif et toutes ses règles sont décrits sous Marqueurs de ligne et clic dans la marge.
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=)—glyphvautmte.GLYPH_NONE(teinte seule),GLYPH_CIRCLE,GLYPH_CIRCLE_HOLLOW,GLYPH_CIRCLE_BAR,GLYPH_ARROWouGLYPH_STRIP; les couleurs sont des identifiants de catégories que vous avez enregistrées avecctx.theme. L'éditeur n'a que quelques emplacements de marqueurs pour toutes les extensions réunies, cet appel peut donc échouer — il lève alorsRpcError. Enregistrez vos types une seule fois, dansregister().add_marker(kind, doc, line)— ligne à partir de 0, non bornée : une ligne hors du document renvoie 0 et ne place rien.- Les marqueurs se déplacent avec leurs lignes, survivent à un rechargement
sur le même numéro de ligne et disparaissent avec leur document —
rajoutez les vôtres dans
ctx.events.on_document_opened.
Le clic dans la marge¶
@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()
Chaque clic gauche dans la colonne des signets est publié comme
margin.clicked (document, line, margin = "symbol", modifiers —
bits mte.MODIFIER_SHIFT / MODIFIER_CONTROL / MODIFIER_ALT /
MODIFIER_META). Par défaut, l'éditeur bascule aussi un signet à ce clic ;
tant qu'une extension détient claim_margin_click(), il ne le fait pas et
laisse le clic aux extensions. ctx.line_markers.release(claim) rend le
clic.
settings_page¶
Ajoute une page dans la boîte de dialogue Préférences de l'éditeur.
Même vocabulaire de spécification de formulaire que les panneaux ancrables ;
même persistance 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, obligatoire) — chemin séparé par des barres obliques dans l'arborescence des Préférences, p. ex."Plugins/My Plugin". Les catégories imbriquées apparaissent comme des nœuds de l'arborescence.title(str, obligatoire) — titre affiché au-dessus de la page.form(dict, obligatoire) — une spécification construite avec les constructeursmte.ui.scope(str, optionnel) — portée d'événement émise surSettingsChangedaprès que l'utilisateur a cliqué sur OK / Appliquer.
Renvoie l'id du gestionnaire.
Annuler n'annule PAS les changements
Les contrôles avec binds="settings:<key>" persistent immédiatement au
fil des modifications de l'utilisateur, donc cliquer sur Annuler
dans la boîte de dialogue Préférences ne les défait pas. C'est une
limitation connue de la phase 3 ; un raffinement futur enveloppera les
réglages sous-jacents dans un proxy de mise en tampon.
ui¶
Lit et écrit les valeurs des contrôles dans les panneaux que vous avez déjà enregistrés. Les valeurs sont toujours des chaînes sur le fil.
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]¶
Lit la valeur actuelle d'un contrôle. Renvoie None quand le panneau ou le
contrôle est inconnu (ce n'est pas une erreur — traitez None et ""
comme « vide »).
- Label / Button / LineEdit / ComboBox — renvoie le texte.
- Checkbox — renvoie
"true"ou"false". - NumberSpin — renvoie l'entier sous forme de chaîne décimale.
- List — renvoie le texte de l'élément actuellement sélectionné (vide quand rien n'est sélectionné).
ctx.ui.set(panel_id, control_id, value) -> bool¶
Écrit une valeur dans un contrôle par programmation. Renvoie True quand
l'écriture a réussi, False quand le panneau/contrôle est inconnu ou que la
valeur ne peut pas être convertie (p. ex. un non-entier envoyé à un
NumberSpin).
Les booléens Python True / False sont convertis en "true" / "false"
avant l'envoi ; tout le reste passe par str().
Un set piloté par l'hôte ne redéclenche PAS le callback on_change du
contrôle, vous pouvez donc mettre à jour vos propres widgets depuis un
gestionnaire de commande sans boucles infinies.
Récapitulatif des liaisons à l'exécution (phase 3)¶
| Champ de la spécification | Comportement à l'exécution |
|---|---|
id="…" |
Adresser le contrôle depuis ctx.ui.get / ctx.ui.set. |
binds="settings:<key>" |
Lit la valeur initiale depuis ctx.settings ; réécrit à chaque modification de l'utilisateur. |
on_click="<cmd>" |
Les boutons invoquent l'id de commande enregistré au clic. |
on_change=handler |
Déclenche le callback enregistré via ctx.docks.on_change(...) à chaque modification de l'utilisateur. |
Les mêmes règles s'appliquent aux panneaux ancrables et aux pages de Préférences.