Creare un plugin Python per Modular Text Editor con Claude¶
Destinatario: un assistente IA (Claude / ChatGPT / ecc.). Questa pagina è un insieme di istruzioni autosufficiente: consegnala all'assistente insieme al compito («scrivimi per favore un plugin che fa X») e l'assistente dovrebbe produrre una cartella di plugin completa, corretta e funzionante senza aver bisogno di leggere il resto della documentazione.
Se sei una persona che sta cercando di scrivere un plugin a mano, parti da qui: questa pagina è ottimizzata per il consumo da parte di un'IA e salta gli esempi amichevoli.
1. Che cosa stai producendo¶
Una cartella con esattamente due file (più eventuali extra):
Restituisci la cartella con un blocco di codice per file. L'utente o deposita la
cartella nella cartella dei plugin dell'editor, oppure comprime la cartella e
rinomina l'archivio <name>.mteplugin, per poi installarlo tramite
Preferenze → Estensioni → Installa estensione.... Entrambe le strade richiedono un riavvio per
caricarlo.
I due file sono descritti esaustivamente qui sotto. Segui le regole alla lettera: l'host esegue una sonda statica e respinge i plugin che le infrangono.
2. plugin.json — modello canonico¶
{
"id": "org.example.<short-name>",
"name": "<Human Display Name>",
"version": "1.0.0",
"vendor": "<Author or Team>",
"description": "<One sentence about what it does.>",
"apiVersion": 1,
"entry": "python",
"module": "main",
"order": 900,
"menus": [
{ "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
}
Regole inderogabili¶
"apiVersion"dev'essere1(la versione attuale dell'host)."entry"dev'essere"python": nient'altro instrada verso questo backend."module"dev'essere"main"a meno che non ci sia un motivo esplicito per dare al modulo un altro nome — e mai un percorso con punti (la fase 1 supporta solo nomi di modulo piatti)."id"dev'essere in stile DNS inverso (org.<vendor>.<name>) e univoco a livello globale tra i plugin installati dall'utente. Se l'utente non ha indicato un fornitore, usaorg.example..menus[]deve contenere almeno una voce il cuititlecorrisponda al segmento più a sinistra di ogni percorso di menu che usi inmain.py. Preferisci"Plugins"per qualsiasi cosa rivolta all'utente che non appartenga in modo evidente a un menu del nucleo.
Campi facoltativi¶
"pyRequires": []— un array di stringhe di requisito pip (per esempio["requests>=2.31"]). Quando non è vuoto, l'editor predispone un venv per plugin al primo avvio (pip install— richiede la rete quell'unica volta) ed esegue il plugin al suo interno. Preferisci la libreria standard quando basta (nessun ritardo di predisposizione, funziona offline); dichiarapyRequiresquando il compito ha davvero bisogno di un pacchetto di terze parti, e avvisa l'utente che il primo avvio si fermerà per installarlo."permissions": []— dichiarazioni grossolane di capacità (per esempio["network"]), mostrate in sola lettura nella pagina Plugin. Dichiara"network"quando il plugin fa chiamate verso l'esterno; è un'etichetta onesta per l'utente, non un'imposizione."requires": []— id di ALTRI plugin senza i quali questo non può funzionare (non pacchetti pip). Si avviano per primi; se uno manca o è spento, questo plugin non si avvia e la pagina Plugin dice perché. Lascialo vuoto, a meno che il compito dell'utente non dipenda davvero da un altro plugin."apiMinorVersion": 0— la versione minor minima dell'API dell'editor di cui il plugin ha bisogno. Lasciala a0, a meno che tu non usi una chiamata documentata come più recente.
Valori che NON devi inventare¶
- Qualsiasi campo non elencato sopra. L'host ignora le chiavi sconosciute; aggiungerle è un rischio di manutenzione.
3. main.py — scheletro canonico¶
"""<Short description of what this plugin does.>"""
import mte # noqa: F401 -- pulls in dataclasses for event payloads
def register(ctx):
# Register commands, menu items, and event subscriptions here.
# Everything below is illustrative -- delete what you don't need.
@ctx.commands.command(id="<id>.<verb>",
title="<Human Label>",
shortcut="Ctrl+Alt+<letter>", # optional
category="<Group>") # optional
def _handler():
ctx.status.show("hello", timeout_ms=2000)
ctx.log.info("<id>: handler ran")
ctx.menus.add_item("Plugins/<Human Label>/<Item>", "<id>.<verb>")
Regole inderogabili¶
register(ctx)DEVE essere una funzione di primo livello. La sonda statica richiede una riga che inizi condef register(alla colonna 0. Se la annidi dentro una classe, la decori a livello di modulo o la rendi unasync def, il plugin viene respinto in silenzio al momento dell'individuazione.- Importa da
mtequando ti sottoscrivi agli eventi: le dataclass tipizzate (mte.DocumentSaved, ecc.) vengono da quel pacchetto.import mtein cima è sicuro anche se non lo usi direttamente; il# noqa: F401qui sopra evita il rumore del linter. - Non usare mai
print(). Lo stdout del worker è il canale RPC; scriverci corrompe il protocollo. Usactx.log.info / warning / error. - Non
importare mai nulla che non abbia dichiarato. Oltre alla libreria standard e amte, ogni importazione di terze parti deve comparire nelpyRequiresdiplugin.json(l'editor la installa con pip nel venv del plugin al primo avvio) oppure essere inclusa direttamente nella cartella del plugin. Un'importazione non dichiarata sollevaModuleNotFoundErrorsulla macchina dell'utente. - I gestori di comandi e di eventi possono bloccarsi brevemente: girano sul
pool di thread del worker, quindi le chiamate sincrone verso
ctx.editor/ctx.settings/ctx.data_dirsono lecite. Ma le callback dei fornitori di completamento (@ctx.completions.provider(...)) NON DEVONO bloccarsi: girano sul ciclo degli eventi con una scadenza di ~30 ms. Niente I/O, nessuna chiamata actx.editor.*, solo cache. Costruisci gli indici altrove (per esempio sudocument.opened) e rispondi da un dizionario. - Non importare Qt, PySide, PyQt o Scintilla. Non sono disponibili nel worker.
- Gli id dei comandi devono essere preceduti dallo spazio dei nomi del tuo
plugin, per evitare collisioni con il nucleo o con altri plugin.
<id>.<verb>qui sopra; per esempiowordcount.count,hello.sayHi. - I percorsi di menu devono iniziare con un menu di primo livello che hai
dichiarato anche nel
menus[]diplugin.json. Convenzione:"Plugins/<Plugin Name>/<Verb>".
4. API completa — tutto ciò che sta su ctx¶
Hai diciotto servizi: commands, menus, events, log, status, editor,
languages, settings, app_settings, document_settings, theme, diagnostics,
compare, completions, docks, settings_page, ui e
data_dir()/diagnostics_dir(). Tutto ciò che non è elencato qui non esiste: non
inventare servizi. (I venv per plugin si dichiarano tramite pyRequires, non con
un servizio di ctx; il substrato di protocollo con flusso e annullamento non ha
ancora un'API rivolta ai plugin.)
ctx.commands¶
@ctx.commands.command(*, id, title, shortcut="", category="",
requires_editor=False, requires_writable_editor=False)
def _handler(): ...
Decoratore. Registra un comando e vi lega _handler (una funzione senza
argomenti che restituisce None). Le eccezioni in _handler vengono catturate
dal worker e scritte su stderr; non fanno andare in crash l'editor.
Imposta requires_editor quando il comando legge il documento attivo (buffer,
selezione, cursore) e requires_writable_editor quando ne modifica il testo.
L'host disattiva allora il comando dove non potrebbe funzionare — su una scheda
senza documento (una vista diff o di log) e, per il flag di scrittura, su un
documento di sola lettura — invece di lasciare che l'utente invochi
un'operazione silenziosamente nulla. Il flag di scrittura implica l'altro; lascia
entrambi disattivati per i comandi che si limitano ad aprire una finestra o un
pannello.
ctx.menus¶
ctx.menus.add_item(path, command_id) # -> handler_id (str)
ctx.menus.add_separator(path) # -> handler_id (str)
ctx.menus.set_item_checked(handler_id, checked) # -> None
ctx.menus.set_item_enabled(handler_id, enabled) # -> None
path è "Top/Sub/…/Label". I sottomenu intermedi vengono creati su richiesta.
set_item_checked trasforma la voce in una voce di menu spuntabile e ne imposta
il segno. Usalo per gli interruttori acceso/spento persistenti. La prima chiamata
promuove; le successive si limitano a ribaltare. Operazione nulla e silenziosa su
id sconosciuti.
set_item_enabled disattiva la voce. Usalo quando il comando ha bisogno di una
precondizione, così che una voce non disponibile sembri non disponibile invece di
non fare nulla quando viene cliccata. Idempotente; le voci nascono abilitate;
operazione nulla e silenziosa su id sconosciuti.
ctx.events¶
Diciassette argomenti, ciascuno con un decoratore zuccherino e una forma grezza.
| Zuccherino | Argomento grezzo | Contenuto |
|---|---|---|
@ctx.events.on_document_opened |
document.opened |
mte.DocumentOpened(document: int, path: str) |
@ctx.events.on_document_closed |
document.closed |
mte.DocumentClosed(document: int) |
@ctx.events.on_document_saved |
document.saved |
mte.DocumentSaved(document: int) |
@ctx.events.on_document_modified_changed |
document.modifiedChanged |
mte.DocumentModifiedChanged(document: int, modified: bool) |
@ctx.events.on_document_encoding_changed |
document.encodingChanged |
mte.DocumentEncodingChanged(document: int, encoding: mte.DocumentEncoding) |
@ctx.events.on_document_line_ending_changed |
document.lineEndingChanged |
mte.DocumentLineEndingChanged(document: int, ending: mte.LineEnding) |
@ctx.events.on_text_changed |
text.changed |
mte.TextChanged(document, position, added_bytes, removed_bytes) — a ogni modifica, mantienilo banale |
@ctx.events.on_selection_changed |
selection.changed |
mte.SelectionChanged(document: int) |
@ctx.events.on_active_document_changed |
document.activeChanged |
mte.ActiveDocumentChanged(document: int) |
@ctx.events.on_settings_changed |
settings.changed |
mte.SettingsChanged(scope: str) |
@ctx.events.on_theme_changed |
theme.changed |
mte.ThemeChanged(variant: str) |
@ctx.events.on_workspace_root_changed |
workspace.rootChanged |
mte.WorkspaceRootChanged(root: str) |
@ctx.events.on_workspace_files_changed |
workspace.filesChanged |
mte.WorkspaceFilesChanged(files: list[mte.WorkspaceFile]) |
@ctx.events.on_command_shortcut_changed |
command.shortcutChanged |
mte.CommandShortcutChanged(command_id: str, sequence: str) |
@ctx.events.on_host_window_shown |
host.windowShown |
mte.HostWindowShown() — una volta per esecuzione, dopo il primo fotogramma |
@ctx.events.on_app_about_to_quit |
app.aboutToQuit |
mte.AppAboutToQuit() |
@ctx.events.on_margin_clicked |
margin.clicked |
mte.MarginClicked(document: int, line: int, margin: str, modifiers: int) — un clic nella colonna dei segnalibri; bit di modifiers: mte.MODIFIER_* |
Forma grezza:
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
Non inventare nuovi argomenti: la forma grezza li valida e solleva ValueError
per qualsiasi cosa non sia in tabella.
ctx.log¶
Mordi-e-fuggi. Le righe sono precedute da [python:<plugin.id>] nel log
diagnostico dell'host.
ctx.status¶
ctx.status.show(message: str, timeout_ms: int = 0, level: str = "info")
ctx.status.clear()
seg = ctx.status.add_segment(id: str, priority: int = 100) # persistent
seg.set_text(str); seg.set_visible(bool); seg.remove()
level dev'essere "info" / "warning" / "error". timeout_ms=0 significa
che l'host sceglie un valore predefinito. show() serve per le notifiche
temporanee; tutto ciò che resta sullo schermo (un conteggio dal vivo, un
indicatore di modalità) è un segmento: i due non si calpestano mai a vicenda.
ctx.editor (fase 2)¶
Ogni metodo è sincrono: blocca il thread chiamante. Va bene da una callback
def di comando o di evento (il worker smista i gestori sincroni su un pool di
thread); illecito dai gestori async def e dai fornitori di
ctx.completions (che girano direttamente sul ciclo degli eventi).
I documenti sono referenziati da id int opachi (0 = nessun documento).
doc = ctx.editor.active_document()
docs = ctx.editor.open_documents()
ctx.editor.set_active_document(doc)
path = ctx.editor.file_path(doc) # str or None
text = ctx.editor.text(doc)
slice = ctx.editor.text_range(doc, start, end) # [start, end) bytes
ctx.editor.set_text(doc, "new text")
n = ctx.editor.length(doc)
sel = ctx.editor.selection_range(doc) # mte.MatchRange
s = ctx.editor.selected_text(doc)
ctx.editor.replace_selection(doc, "hi")
ctx.editor.set_selection(doc, mte.MatchRange(4, 9))
pos = ctx.editor.caret(doc) # mte.EditorPosition
ctx.editor.set_caret(doc, mte.EditorPosition(line=5, column=0))
new_doc = ctx.editor.open_file("/path/to/file") # 0 on failure
here = ctx.editor.open_file_at("/path/to/file", mte.EditorPosition(41, 0)) # open + caret + scroll + focus
ctx.editor.reveal_line(doc, 120) # scroll only; caret and focus stay
ok = ctx.editor.save_document(doc)
closed = ctx.editor.close_document(doc) # prompts if modified; False = cancelled
dirty = ctx.editor.is_modified(doc)
hit = ctx.editor.find_in_range(doc, "TODO", 0,
ctx.editor.length(doc),
mte.SearchOptions(match_case=True))
new_end = ctx.editor.replace_target(doc, hit, "DONE",
mte.SearchOptions())
style = ctx.editor.indentation(doc) # IndentationStyle
ctx.editor.set_indentation(doc,
mte.IndentationStyle(use_tabs=False, width=2))
ctx.editor.set_wrap_indent(doc, "same") # "left" | "same" | "indent"
lang = ctx.editor.colorizer_id(doc) # "python", "cpp", ...
mte.SearchOptions.mode: "normal" / "extended" / "regex".
mte.SearchOptions.direction: "forward" / "backward".
ctx.settings / ctx.app_settings (fase 2)¶
Archivio chiave/valore tipizzato circoscritto a ciascun plugin (ctx.settings)
più una vista in sola lettura delle impostazioni globali dell'applicazione
(ctx.app_settings). Il tipo viene dedotto dal tipo Python del valore
predefinito.
ctx.settings.set("wrap", True)
ctx.settings.set("count", 42)
ctx.settings.set("theme", "dark")
wrap = ctx.settings.get("wrap", False) # bool
count = ctx.settings.get("count", 0) # int
theme = ctx.settings.get("theme", "light") # str
ctx.settings.contains("wrap")
ctx.settings.remove("stale")
# App-wide (read-only)
tw = ctx.app_settings.get("editor.tabWidth", 4)
ctx.app_settings.set/remove/sync sollevano RuntimeError. Nella tua logica,
quando leggi, devi controllare il booleano prima dell'intero (in Python
isinstance(True, int) == True).
ctx.completions (fase 2)¶
@ctx.completions.provider(languages=["python"])
def _complete(req: mte.CompletionRequest):
return [mte.CompletionItem(text=w, kind=mte.KIND_KEYWORD)
for w in ("hello", "hello_world")
if w.startswith(req.prefix)]
Campi di req: document, prefix, position, language_id, manual. Tipi:
KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.
Restituisci list[CompletionItem], semplici stringhe o dizionari. La callback
DEVE restituire entro ~30 ms, altrimenti l'host scarta i tuoi risultati per
quella battuta: niente I/O, niente ctx.editor.text(...), solo cache. Costruisci
gli indici altrove (per esempio su document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (fase 2)¶
Usa data_dir() per cache e indici. diagnostics_dir() è in sola lettura (vi
scrive il segnalatore di arresti anomali).
ctx.docks — pannelli ancorabili dichiarativi (fase 3)¶
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
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="myplugin.panel", # returned; use it for ctx.ui.get / ctx.ui.set
title="My Plugin",
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="myplugin.run")]),
]),
)
Costruisci la specifica di modulo con gli ausili di mte.ui. Il
vocabolario dei controlli è fisso: Label / Button / Checkbox / LineEdit /
NumberSpin / ComboBox / List dentro Column / Row / Group.
binds="settings:<key>"— il valore del controllo si rende persistente automaticamente tramitectx.settings(lo stesso archivio dictx.settings.set).on_click="<cmd.id>"— solo per i Button; invoca un comando registrato.on_change=<handler>— allocalo tramitectx.docks.on_change(callback); scatta alle modifiche dell'utente.
Le callback di on_change girano sul ciclo degli eventi del worker (NON sul pool
di thread). Mantienile leggere: nessuna chiamata all'editor, nessun lavoro lungo.
Modifica un piccolo stato in memoria, scrivi nel log o lancia un comando; il
lavoro pesante fallo dal comando.
Passa initially_visible=False per un pannello che non deve comparire all'avvio,
e poi mostralo dal suo comando con
ctx.docks.set_panel_visible(panel_id, True) (lo porta anche in primo piano; è
idempotente; gli id sconosciuti vengono ignorati).
ctx.docks.is_panel_visible(panel_id) dice se un pannello è aperto (una
scheda in secondo piano conta). Più pannelli che vanno insieme: passa lo
stesso tab_group="<plugin.id>/panels" e la stessa area, e condivideranno
un unico pannello ancorato a schede.
ctx.line_markers — segni sulle righe del documento¶
kind = ctx.line_markers.register_kind(glyph=mte.GLYPH_CIRCLE,
glyph_color="<category id>", # from ctx.theme
line_tint="", z_order=0) # RpcError when no slot is left
m = ctx.line_markers.add_marker(kind, doc, line) # 0 = refused; follows edits
ctx.line_markers.line_of(m) # -1 = gone (document closed)
ctx.line_markers.markers(kind, doc) # [mte.LineMarkerPlacement(id, line)]
ctx.line_markers.remove_marker(m)
claim = ctx.line_markers.claim_margin_click() # stop bookmark toggling on margin clicks
Registra i tipi una volta sola in register(); l'editor ha solo pochi
posti. Rivendica il clic sul margine solo quando il plugin gli dà un
significato: i clic arrivano comunque tramite
@ctx.events.on_margin_clicked.
ctx.languages — colorazione della sintassi¶
ctx.languages.available() # list[LanguageInfo(id, name)]
ctx.languages.language_for(doc) # "" when uncoloured
ctx.languages.set_language_for(doc, "json") # "" clears; False = unknown id
ctx.languages.auto_detect_for(doc)
h = ctx.languages.register(id=, name=, lexer=, extensions=[...],
keywords=[{"group": ..., "words": ...}],
styles={"<LEXER STYLE NAME>": "comment", ...})
ctx.languages.unregister(h) # restores what it displaced
lexer deve nominare un lexer di backend esistente; styles e keywords usano
i nomi propri del lexer (stessa forma dei colorizers/*.json distribuiti), mai
id numerici.
ctx.document_settings / mte.keys¶
ctx.document_settings.get_bool(doc, key, default) # .editorconfig-layered, read-only
ctx.document_settings.get_int / get_double / get_string / contains
ctx.app_settings.get(mte.keys.EDITOR_FONT_SIZE, mte.keys.DEFAULT_EDITOR_FONT_SIZE)
ctx.diagnostics — sottolineature ondulate¶
ctx.diagnostics.publish(doc, source: str, [mte.Diagnostic(start=, end=,
message=, severity=mte.SEVERITY_WARNING, code="")])
ctx.diagnostics.publish(doc, source, []) # retract only your source
ctx.diagnostics.clear(doc) # clears EVERY producer's set
Invia ogni volta l'insieme COMPLETO per la tua source; sostituisce il
precedente. source è obbligatoria.
ctx.theme — colori tematizzabili per la tua interfaccia¶
h = ctx.theme.register_category(id, display_label, default_light=0x..., default_dark=0x...)
rgb = ctx.theme.resolve_color(id) # 0xRRGGBB; re-read on on_theme_changed
ctx.theme.has_override(id); ctx.theme.active_variant(); ctx.theme.categories()
ctx.compare — diff affiancato¶
ctx.compare.open(mte.CompareSource(title, path=..., content=...),
mte.CompareSource(...), ignore_whitespace=False,
ignore_case=False, ignore_eol=False) # -> bool
ctx.events.publish¶
Un plugin può pubblicare solo workspace.rootChanged /
workspace.filesChanged; qualsiasi altra cosa solleva ValueError. Ci si può
sottoscrivere a ogni evento dell'host tranne document.aboutToSave (vedi
l'elenco dei decoratori); l'evento nativo DocumentAboutToClose non ha
alcun argomento Python: usa document.closed.
ctx.settings_page — pagine delle Preferenze dichiarative (fase 3)¶
from mte.ui import Column, Checkbox, NumberSpin
ctx.settings_page.register(
category="Plugins/My Plugin", # slash-separated tree path
title="My Plugin",
form=Column([
Checkbox(id="verbose", label="Verbose logging",
binds="settings:verbose"),
NumberSpin(id="cap", default=100, min=1, max=10_000,
binds="settings:cap"),
]),
)
Stessi costruttori, stessa semantica di binds. Due controlli che condividono un
binds="settings:<key>" si rispecchiano a vicenda in tempo reale: il pannello
ancorabile e la pagina delle Preferenze possono entrambi ospitare una casella di
spunta per la stessa impostazione, e ribaltarne una ribalta l'altra.
Avvertenza della fase 3
I valori si rendono persistenti con impazienza a ogni modifica dell'utente: Annulla non ripristina ciò che l'utente ha cambiato. Migliorerà in un perfezionamento futuro.
ctx.ui.get / ctx.ui.set — accesso ai controlli del pannello a tempo di esecuzione (fase 3)¶
# Read the current value; returns str or None.
current = ctx.ui.get(panel_id, "needle")
# Write. Returns bool (False when panel/control unknown, or when the
# value can't be coerced to the target type).
ctx.ui.set(panel_id, "needle", "hello")
ctx.ui.set(panel_id, "cs", True) # bool -> "true"/"false"
get restituisce il valore primario come stringa:
- Label / Button / LineEdit / ComboBox → il testo
- Checkbox →
"true"/"false" - NumberSpin → l'intero in forma decimale
- List → il testo della voce attualmente selezionata (stringa vuota se non ce n'è nessuna)
Un set pilotato dall'host è silenzioso: NON fa scattare di nuovo l'on_change
del controllo, così puoi aggiornare i tuoi widget da un gestore di comando senza
cicli infiniti.
5. Esempio completo e svolto — il plugin wordcount¶
Copialo alla lettera e cambia id, name e i title per adattarli al tuo caso.
Mette alla prova ogni servizio della fase 1.
{
"id": "org.example.wordcount",
"name": "Word Count",
"version": "1.0.0",
"vendor": "Example, Inc.",
"description": "Counts words the user has typed since launch and logs saves.",
"apiVersion": 1,
"entry": "python",
"module": "main",
"order": 900,
"menus": [
{ "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
]
}
"""Reports a session word-count total via a menu command and logs saves."""
import mte # noqa: F401
# Module-level state is fine -- one worker per plugin, one register() call.
_state = {"saves": 0}
def register(ctx):
ctx.log.info("wordcount: register() ran")
@ctx.commands.command(id="wordcount.report",
title="Report save count",
shortcut="Ctrl+Alt+W",
category="Text")
def _report():
n = _state["saves"]
ctx.status.show(f"{n} save(s) this session", timeout_ms=2500)
ctx.log.info(f"wordcount: user asked for count; {n} saves so far")
ctx.menus.add_item("Plugins/Word Count/Report save count",
"wordcount.report")
@ctx.events.on_document_saved
def _saved(ev: mte.DocumentSaved):
_state["saves"] += 1
ctx.log.info(f"wordcount: doc {ev.document} saved -- total now {_state['saves']}")
@ctx.events.on_app_about_to_quit
def _quit(_ev):
ctx.log.info(f"wordcount: shutting down after {_state['saves']} saves")
Flusso previsto per l'utente: apri l'editor, salva un file tre volte, poi clicca
Plugins → Word Count → Report save count. La barra di stato mostra
3 save(s) this session. Il log registra ogni passo.
6. Compiti comuni — ricette da copiare e incollare¶
Ogni ricetta è un frammento di solo corpo: incollalo dentro def register(ctx):.
Aggiungere un comando di menu¶
@ctx.commands.command(id="mine.hello", title="Say Hi", shortcut="Ctrl+Alt+Y")
def _hi():
ctx.status.show("hi")
ctx.menus.add_item("Plugins/Mine/Say Hi", "mine.hello")
Reagire al salvataggio¶
Avvisare all'uscita¶
@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
ctx.log.warning("mine: last chance to persist state")
Scrivere nel log a un livello specifico¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Separatore di menu¶
Più comandi correlati¶
@ctx.commands.command(id="mine.a", title="Do A")
def _a(): ...
@ctx.commands.command(id="mine.b", title="Do B")
def _b(): ...
ctx.menus.add_item("Plugins/Mine/Do A", "mine.a")
ctx.menus.add_item("Plugins/Mine/Do B", "mine.b")
Interruttore acceso/spento persistente con segno di spunta visibile¶
KEY = "verbose"
item = ctx.menus.add_item("View/Verbose Logging", "mine.toggleVerbose")
# Seed the mark from the persisted state so it matches on launch.
ctx.menus.set_item_checked(item, ctx.settings.get(KEY, False))
@ctx.commands.command(id="mine.toggleVerbose", title="Verbose Logging")
def _toggle():
on = not ctx.settings.get(KEY, False)
ctx.settings.set(KEY, on)
ctx.menus.set_item_checked(item, on)
Pannello laterale con un campo di testo, una casella di spunta e due pulsanti¶
from mte.ui import Column, Row, Label, LineEdit, Checkbox, Button
def _on_needle(value): ctx.log.info(f"needle={value!r}")
needle_h = ctx.docks.on_change(_on_needle)
panel = ctx.docks.add_panel(
id="mine.panel", title="Mine", area="right",
form=Column([
Label(text="Search:"),
LineEdit(id="needle", placeholder="text...",
binds="settings:needle", on_change=needle_h),
Checkbox(id="cs", label="Case", binds="settings:cs"),
Row([Button(text="Run", on_click="mine.run"),
Button(text="Clear", on_click="mine.clear")]),
]),
)
@ctx.commands.command(id="mine.run", title="Run")
def _run():
needle = ctx.ui.get(panel, "needle") or ""
cs = ctx.ui.get(panel, "cs") == "true"
ctx.status.show(f"searching for {needle!r} (case={cs})")
@ctx.commands.command(id="mine.clear", title="Clear")
def _clear():
ctx.ui.set(panel, "needle", "")
Pagina delle Preferenze che rispecchia le impostazioni di un pannello¶
from mte.ui import Column, Checkbox
# Both controls bind to the SAME settings key -- toggling one
# flips the other in real time.
ctx.docks.add_panel(id="mine.panel", title="Mine", area="right",
form=Column([Checkbox(id="cs", label="Case",
binds="settings:cs")]))
ctx.settings_page.register(category="Plugins/Mine", title="Mine",
form=Column([Checkbox(id="cs", label="Case sensitive",
binds="settings:cs")]))
7. Anti-schemi — NON fare queste cose¶
print() per la diagnostica¶
Sbagliato:
Giusto:
register dentro una classe¶
Sbagliato:
Giusto:
async def register¶
Sbagliato:
Giusto:
Importazioni di terze parti senza distribuire il wheel¶
Sbagliato:
Giusto (fase 1): attieniti alla libreria standard. Se hai davvero bisogno di
qualcosa di terze parti, distribuisci il wheel dentro la cartella del plugin ed
estendi sys.path nelle prime righe di main.py; vedi
Creazione dei pacchetti.
Importazioni di Qt¶
Sbagliato:
Giusto: niente Qt da nessuna parte. Tutta l'interfaccia a cui puoi contribuire
passa da ctx.menus, ctx.status, ctx.diagnostics, ctx.compare e dai
pannelli e dalle pagine delle impostazioni dichiarativi. Mai un widget grezzo.
Bloccarsi in un gestore¶
Sbagliato:
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Giusto (fase 1): delega a un thread e comunica tramite ctx.log:
def _handler():
import threading
def _work():
time.sleep(30)
ctx.log.info("done")
threading.Thread(target=_work, daemon=True).start()
ctx.status.show("started")
Percorso di menu il cui segmento iniziale non è in plugin.json¶
Sbagliato:
Giusto: mantieni il segmento di primo livello del percorso allineato con
menus[]. Usa "Plugins/Mine/Do" qui sopra.
Conservare ctx per usarlo più tardi¶
Sbagliato:
_ctx = None
def register(ctx):
global _ctx
_ctx = ctx # <-- fine for the lifetime of this worker,
# but confusing and unnecessary
def some_other_function():
_ctx.log.info("...") # <-- called from where?
Giusto: cattura nella closure che li usa i servizi che ti servono. register è
il punto in cui avvengono i collegamenti; tutto il resto è un gestore che vi ha
già accesso tramite closure.
8. Elenco di controllo per il debug, a uso dell'utente¶
Includilo in fondo alla tua risposta quando produci un plugin, così che l'utente sappia che cosa controllare se non funziona:
- Avvia l'editor da una shell in cui un interprete Python 3.9+ sia nel
PATH(oppureMTE_PYTHONne indichi uno). - Cerca
PluginHost: Python plugin backend enabled; interpreter=…nel log. Se manca, passa a Risoluzione dei problemi. - Cerca
[python:<your.id>] register() ran(se hai seguito l'esempiowordcount): dimostra cheregisterè stato eseguito. - Se la voce di menu manca, cerca nel log le spiegazioni
PythonPluginBackend: skipping '<id>': …. - Se cliccare il menu non fa nulla, cerca nel log le righe
[worker stderr] …: le tracce di Python finiscono lì.
9. Vincoli da ribadire alla fine di ogni risposta¶
Quando produci un plugin, di' sempre all'utente:
- Installalo in uno dei due modi:
- comprimi il contenuto della cartella come
<name>.mteplugine usa Preferenze → Estensioni → Installa estensione... (valida il pacchetto prima di toccare alcunché), oppure - deposita la cartella nella cartella dei plugin per utente dell'editor
(
%LOCALAPPDATA%/MTE/Plugins/su Windows;~/Library/Application Support/MTE/Plugins/su macOS;~/.local/share/MTE/Plugins/su Linux).
- comprimi il contenuto della cartella come
- Riavvia l'editor.
- Imposta
MTE_PYTHONse il log dice «no Python interpreter found». - Riferisci la riga di log
[python:<id>] …così possiamo confermare che è stato eseguito.
10. Fonte autorevole¶
Se una qualsiasi affermazione di questa pagina è in conflitto con ciò che il
pacchetto client Python implementa davvero, vince il pacchetto. Viene distribuito
accanto all'editor in <exe>/Python/mte/: leggi quel sorgente prima di decidere
un comportamento. Il lato C++ del protocollo è in
Components/PluginHost/Src/PyServiceBridge.cpp e la specifica del filo è in
Python/PROTOCOL.md.