Vai al contenuto

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

<plugin-id>/
├── plugin.json      # required
└── main.py          # required

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

  1. "apiVersion" dev'essere 1 (la versione attuale dell'host).
  2. "entry" dev'essere "python": nient'altro instrada verso questo backend.
  3. "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).
  4. "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, usa org.example..
  5. menus[] deve contenere almeno una voce il cui title corrisponda al segmento più a sinistra di ogni percorso di menu che usi in main.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); dichiara pyRequires quando 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 a 0, 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

  1. register(ctx) DEVE essere una funzione di primo livello. La sonda statica richiede una riga che inizi con def register( alla colonna 0. Se la annidi dentro una classe, la decori a livello di modulo o la rendi un async def, il plugin viene respinto in silenzio al momento dell'individuazione.
  2. Importa da mte quando ti sottoscrivi agli eventi: le dataclass tipizzate (mte.DocumentSaved, ecc.) vengono da quel pacchetto. import mte in cima è sicuro anche se non lo usi direttamente; il # noqa: F401 qui sopra evita il rumore del linter.
  3. Non usare mai print(). Lo stdout del worker è il canale RPC; scriverci corrompe il protocollo. Usa ctx.log.info / warning / error.
  4. Non importare mai nulla che non abbia dichiarato. Oltre alla libreria standard e a mte, ogni importazione di terze parti deve comparire nel pyRequires di plugin.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 solleva ModuleNotFoundError sulla macchina dell'utente.
  5. 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_dir sono 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 a ctx.editor.*, solo cache. Costruisci gli indici altrove (per esempio su document.opened) e rispondi da un dizionario.
  6. Non importare Qt, PySide, PyQt o Scintilla. Non sono disponibili nel worker.
  7. 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 esempio wordcount.count, hello.sayHi.
  8. I percorsi di menu devono iniziare con un menu di primo livello che hai dichiarato anche nel menus[] di plugin.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

ctx.log.trace  (str)
ctx.log.debug  (str)
ctx.log.info   (str)
ctx.log.warning(str)
ctx.log.error  (str)

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)

p = ctx.data_dir()   # pathlib.Path -- writable, host-created

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 tramite ctx.settings (lo stesso archivio di ctx.settings.set).
  • on_click="<cmd.id>" — solo per i Button; invoca un comando registrato.
  • on_change=<handler> — allocalo tramite ctx.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.

wordcount/plugin.json
{
  "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 }
  ]
}
wordcount/main.py
"""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

@ctx.events.on_document_saved
def _on_save(ev):
    ctx.log.info(f"saved doc {ev.document}")

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

ctx.menus.add_separator("Plugins/Mine")

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

Sbagliato:

def _handler():
    print("did the thing")   # <-- corrupts the RPC channel

Giusto:

def _handler():
    ctx.log.info("did the thing")

register dentro una classe

Sbagliato:

class Plugin:
    def register(self, ctx):   # <-- static probe rejects this
        ...

Giusto:

def register(ctx):
    ...

async def register

Sbagliato:

async def register(ctx):   # <-- probe accepts but runtime never awaits
    ...

Giusto:

def register(ctx):
    ...

Importazioni di terze parti senza distribuire il wheel

Sbagliato:

import requests   # <-- ImportError on most user machines

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:

from PySide6 import QtWidgets   # <-- not available; no Qt in the worker

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:

"menus": [ { "title": "Plugins", "barPriority": 570 } ]
ctx.menus.add_item("Tools/Mine/Do", "mine.do")   # <-- Tools ≠ Plugins

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:

  1. Avvia l'editor da una shell in cui un interprete Python 3.9+ sia nel PATH (oppure MTE_PYTHON ne indichi uno).
  2. Cerca PluginHost: Python plugin backend enabled; interpreter=… nel log. Se manca, passa a Risoluzione dei problemi.
  3. Cerca [python:<your.id>] register() ran (se hai seguito l'esempio wordcount): dimostra che register è stato eseguito.
  4. Se la voce di menu manca, cerca nel log le spiegazioni PythonPluginBackend: skipping '<id>': ….
  5. 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>.mteplugin e 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).
  • Riavvia l'editor.
  • Imposta MTE_PYTHON se 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.