Aller au contenu

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

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

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 dans plugin.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". Ctrl correspond automatiquement au modificateur principal de la plateforme (Cmd sur 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 ; lisez ctx.commands.effective_shortcut(id) lors de la construction de vos widgets et réappliquez sur l'événement "command.shortcutChanged".
ctx.commands.effective_shortcut(command_id) -> str

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.


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)

ctx.menus.add_item("Plugins/Word Count/Count", "wordcount.count")
  • 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)

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

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 :

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

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

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

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 ; 0 laisse 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("saved", timeout_ms=1500)
ctx.status.show("no results", level="warning")

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

path = ctx.data_dir()                    # pathlib.Path

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é par ctx.ui.get / ctx.ui.set pour adresser les contrôles de ce panneau. Si vous n'en fournissez pas, l'hôte alloue un id py-dock/N et 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 constructeurs mte.ui.
  • initially_visible (bool) — si le panneau est affiché au chargement du plugin. True par défaut. Passez False pour un panneau qui ne doit pas surgir au démarrage, et révélez-le plus tard avec set_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 comme 0.4 est 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 sous proportionalSize.
  • tab_group (str) — les panneaux qui ont le même identifiant de groupe non vide dans la même area partagent 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. Voir tabGroup.

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=) — glyph vaut mte.GLYPH_NONE (teinte seule), GLYPH_CIRCLE, GLYPH_CIRCLE_HOLLOW, GLYPH_CIRCLE_BAR, GLYPH_ARROW ou GLYPH_STRIP ; les couleurs sont des identifiants de catégories que vous avez enregistrées avec ctx.theme. L'éditeur n'a que quelques emplacements de marqueurs pour toutes les extensions réunies, cet appel peut donc échouer — il lève alors RpcError. Enregistrez vos types une seule fois, dans register().
  • 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 constructeurs mte.ui.
  • scope (str, optionnel) — portée d'événement émise sur SettingsChanged aprè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.