Criando um plugin Python do Modular Text Editor com o Claude¶
Público: um assistente de IA (Claude / ChatGPT / etc.). Esta página é um conjunto de instruções autocontido: entregue-a ao assistente junto com a tarefa ("por favor, escreva um plugin que faça X"), e o assistente deve produzir uma pasta de plugin completa, correta e funcional sem precisar ler o resto da documentação.
Se você é uma pessoa tentando escrever um plugin à mão, comece aqui — esta página é otimizada para consumo por IA e pula os exemplos amigáveis.
1. O que você está produzindo¶
Uma pasta com exatamente dois arquivos (mais extras opcionais):
Devolva a pasta como um bloco de código por arquivo. O usuário ou coloca a pasta no
diretório de plugins do editor, ou compacta a pasta e renomeia o arquivo para
<name>.mteplugin, instalando-o então por Preferências → Plug-ins → Instalar um plug-in....
As duas vias precisam de um reinício para carregar.
Os dois arquivos são descritos exaustivamente abaixo. Siga as regras literalmente — o host faz uma sondagem estática e rejeita plugins que as quebram.
2. plugin.json — modelo canônico¶
{
"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 }
]
}
Regras rígidas¶
- O
"apiVersion"precisa ser1(a versão atual do host). - O
"entry"precisa ser"python"— nada mais é encaminhado para este backend. - O
"module"precisa ser"main", a menos que haja um motivo explícito para nomear o módulo de outra forma — e nunca um caminho com pontos (a fase 1 suporta apenas nomes de módulo planos). - O
"id"precisa ser no estilo DNS reverso (org.<vendor>.<name>) e único globalmente entre os plugins instalados do usuário. Se o usuário não especificou um fabricante, useorg.example.. - O
menus[]precisa conter ao menos uma entrada cujotitlecoincida com o segmento mais à esquerda de todo caminho de menu que você usar nomain.py. Prefira"Plugins"para qualquer coisa voltada ao usuário que não pertença obviamente a um menu do núcleo.
Campos opcionais¶
"pyRequires": []— um array de cadeias de requisitos do pip (por exemplo,["requests>=2.31"]). Quando não vazio, o editor provisiona um ambiente virtual por plugin na primeira inicialização (pip install— precisa de rede aquela única vez) e roda o plugin sob ele. Prefira a biblioteca padrão quando ela resolver (sem atraso de provisionamento, funciona offline); declarepyRequiresquando a tarefa realmente precisar de um pacote de terceiros, e avise o usuário de que a primeira inicialização vai pausar para instalá-lo."permissions": []— declarações grosseiras de capacidade (por exemplo,["network"]), exibidas em modo somente leitura na página de Plugins. Declare"network"quando o plugin faz chamadas para fora; é rotulagem honesta para o usuário, não uma imposição."requires": []— ids de OUTROS plugins sem os quais este não funciona (não pacotes pip). Eles iniciam primeiro; se algum estiver ausente ou desligado, este plugin não inicia e a página de Plugins diz por quê. Deixe vazio, a menos que a tarefa do usuário dependa de fato de outro plugin."apiMinorVersion": 0— a menor versão minor da API do editor de que o plugin precisa. Deixe em0, a menos que use uma chamada documentada como mais nova.
Valores que você NÃO deve inventar¶
- Qualquer campo não listado acima. O host ignora chaves desconhecidas; acrescentá-las é um risco de manutenção.
3. main.py — esqueleto canônico¶
"""<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>")
Regras rígidas¶
register(ctx)PRECISA ser uma função de nível superior. A sondagem estática exige uma linha começando pordef register(na coluna 0. Se você a aninhar dentro de uma classe, decorá-la no escopo do módulo, ou torná-laasync def, o plugin é rejeitado em silêncio na hora da descoberta.- Importe de
mtequando assinar eventos — as dataclasses tipadas (mte.DocumentSaved, etc.) vêm daquele pacote. Umimport mteno topo é seguro mesmo que você não o use diretamente; o# noqa: F401acima evita ruído do analisador de estilo. - Nunca use
print(). O stdout do trabalhador é o canal RPC; escrever nele corrompe o protocolo. Usectx.log.info / warning / error. - Nunca importe nada que você não declarou. Além da biblioteca padrão e de
mte, toda importação de terceiros precisa aparecer nopyRequiresdoplugin.json(o editor a instala com pip no ambiente virtual do plugin na primeira inicialização) ou estar embutida dentro da pasta do plugin. Uma importação não declarada levantaModuleNotFoundErrorna máquina do usuário. - Manipuladores de comando/evento podem bloquear brevemente — eles rodam no
pool de threads do trabalhador, então chamadas síncronas a
ctx.editor/ctx.settings/ctx.data_dirsão legais. Mas retrochamadas de provedor de autocompletar (@ctx.completions.provider(...)) NÃO PODEM bloquear: elas rodam no laço de eventos com um prazo de ~30 ms. Sem E/S, sem chamadas actx.editor.*, apenas cache. Construa índices em outro lugar (por exemplo, emdocument.opened) e responda a partir de um dicionário. - Não importe Qt, PySide, PyQt ou Scintilla. Eles não estão disponíveis no trabalhador.
- Os ids de comando precisam levar o prefixo do espaço de nomes do seu plugin,
para evitar colisões com o núcleo / outros plugins.
<id>.<verb>, como acima; por exemplo,wordcount.count,hello.sayHi. - Os caminhos de menu precisam começar com um menu de primeiro nível que você
também declarou no
menus[]doplugin.json. Convenção:"Plugins/<Plugin Name>/<Verb>".
4. API completa — tudo o que há em ctx¶
Você tem dezoito serviços: 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(). Qualquer coisa
não listada aqui não existe — não invente serviços. (Ambientes virtuais por plugin
são declarados via pyRequires, não por um serviço de ctx; o substrato de
protocolo com fluxo/cancelamento ainda não tem API voltada aos plugins.)
ctx.commands¶
@ctx.commands.command(*, id, title, shortcut="", category="",
requires_editor=False, requires_writable_editor=False)
def _handler(): ...
Decorador. Registra um comando e vincula _handler (uma função sem argumentos que
devolve None). Exceções em _handler são capturadas pelo trabalhador e escritas
em stderr; elas não derrubam o editor.
Defina requires_editor quando o comando lê o documento ativo (buffer, seleção,
cursor) e requires_writable_editor quando ele altera o texto. O host então esmaece
o comando onde ele não poderia funcionar — em uma aba sem documento (um diff ou uma
visualização de log) e, para o sinalizador de escrita, em um documento somente
leitura — em vez de deixar o usuário invocar algo que silenciosamente não faz nada.
O sinalizador de escrita implica o outro; deixe os dois desligados para comandos que
apenas abrem uma caixa de diálogo ou um painel.
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 é "Topo/Sub/…/Rótulo". Submenus intermediários são criados sob demanda.
O set_item_checked transforma o item em uma entrada de menu marcável e define a
marca dele. Use-o para alternâncias persistentes de liga/desliga. A primeira chamada
promove; as seguintes apenas invertem. Não faz nada em silêncio com ids
desconhecidos.
O set_item_enabled esmaece o item. Use-o quando o comando precisa de uma condição
prévia, para que uma entrada indisponível pareça indisponível em vez de não fazer
nada quando clicada. Idempotente; os itens começam habilitados; não faz nada em
silêncio com ids desconhecidos.
ctx.events¶
Dezessete tópicos, cada um com um decorador açucarado e uma forma bruta.
| Açucarado | Tópico bruto | Carga útil |
|---|---|---|
@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) — toda edição, mantenha trivial |
@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() — uma vez por execução, depois do primeiro quadro |
@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) — um clique na coluna de marcadores; bits de modifiers: mte.MODIFIER_* |
Forma bruta:
handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id) # to unbind
Não invente tópicos novos — a forma bruta os valida e levanta ValueError para
qualquer coisa fora da tabela.
ctx.log¶
Dispare-e-esqueça. As linhas recebem o prefixo [python:<plugin.id>] no log de
diagnóstico do 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()
O level precisa ser "info" / "warning" / "error". timeout_ms=0 significa
que o host escolhe um padrão. O show() é para notificações passageiras; qualquer
coisa que fique na tela (uma contagem ao vivo, um sinalizador de modo) é um
segmento — os dois nunca se atropelam.
ctx.editor (fase 2)¶
Todo método é síncrono: ele bloqueia a thread chamadora. Tudo bem a partir de
uma retrochamada def de comando/evento (o trabalhador despacha manipuladores
síncronos em um pool de threads); ilegal a partir de manipuladores async def e
de provedores de ctx.completions (que rodam diretamente no laço de eventos).
Os documentos são referenciados por ids int opacos (0 = nenhum 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)¶
Armazenamento tipado de chave/valor restrito por plugin (ctx.settings), mais uma
visão somente leitura das configurações de todo o aplicativo (ctx.app_settings). O
tipo é inferido do tipo Python do padrão.
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 levantam RuntimeError. Na sua própria lógica,
verifique bool antes de int ao ler (em 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)]
Campos de req: document, prefix, position, language_id, manual. Tipos:
KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN.
Devolva list[CompletionItem], cadeias simples ou dicionários. A retrochamada
PRECISA retornar em ~30 ms ou o host descarta os seus resultados para aquela tecla
— sem E/S, sem ctx.editor.text(...), apenas cache. Construa índices em outro lugar
(por exemplo, em document.opened).
ctx.data_dir() / ctx.diagnostics_dir() (fase 2)¶
Use data_dir() para caches e índices. O diagnostics_dir() é somente leitura (o
relator de falhas grava ali).
ctx.docks — painéis acopláveis declarativos (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")]),
]),
)
Construa a especificação de formulário com os auxiliares de
mte.ui. O vocabulário de controles é fixo: Label / Button /
Checkbox / LineEdit / NumberSpin / ComboBox / List dentro de Column / Row /
Group.
binds="settings:<key>"— o valor do controle persiste automaticamente porctx.settings(o mesmo armazenamento dectx.settings.set).on_click="<cmd.id>"— apenas em Button; invoca um comando registrado.on_change=<handler>— aloque-o viactx.docks.on_change(callback); dispara nas edições do usuário.
As retrochamadas de on_change rodam no laço de eventos do trabalhador (NÃO no pool
de threads). Mantenha-as baratas — sem chamadas ao editor, sem trabalho longo.
Altere um pequeno estado em memória, registre no log, ou dispare um comando; faça o
trabalho pesado a partir do comando.
Passe initially_visible=False para um painel que não deve aparecer na
inicialização e revele-o então, a partir do comando dele, com
ctx.docks.set_panel_visible(panel_id, True) (que também o traz para a frente;
idempotente; ids desconhecidos ignorados).
ctx.docks.is_panel_visible(panel_id) diz se um painel está aberto (uma aba
em segundo plano conta). Vários painéis que andam juntos: passe o mesmo
tab_group="<plugin.id>/panels" e a mesma area, e eles compartilham um
único painel acoplável em abas.
ctx.line_markers — marcas nas linhas do 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
Registre os tipos uma única vez em register(); o editor tem só poucos
espaços. Só reivindique o clique na margem quando o plugin lhe der um
significado — os cliques chegam por @ctx.events.on_margin_clicked de
qualquer forma.
ctx.languages — coloração de sintaxe¶
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
O lexer precisa nomear um analisador léxico de backend existente; styles e
keywords usam os nomes do próprio analisador (a mesma forma dos
colorizers/*.json distribuídos), nunca ids numéricos.
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 — traços ondulados¶
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
Envie o conjunto COMPLETO da sua source toda vez; ele substitui o anterior. A
source é obrigatória.
ctx.theme — cores adaptáveis ao tema para a sua interface¶
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 lado a lado¶
ctx.compare.open(mte.CompareSource(title, path=..., content=...),
mte.CompareSource(...), ignore_whitespace=False,
ignore_case=False, ignore_eol=False) # -> bool
ctx.events.publish¶
Apenas workspace.rootChanged / workspace.filesChanged podem ser publicados por
um plugin; qualquer outra coisa levanta ValueError. Todo evento do host, exceto
document.aboutToSave, pode ser assinado (veja a lista de decoradores); o
evento nativo DocumentAboutToClose não tem tópico em Python algum — use
document.closed.
ctx.settings_page — páginas de Preferências declarativas (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"),
]),
)
Os mesmos construtores, a mesma semântica de binds. Dois controles que
compartilham um binds="settings:<key>" se espelham em tempo real — o painel
acoplável e a página de Preferências podem ter ambos uma caixa de seleção para a
mesma configuração, e alternar uma inverte a outra.
Ressalva da fase 3
Os valores persistem avidamente a cada edição do usuário — Cancelar não reverte o que o usuário alterou. Isso vai melhorar em um refinamento futuro.
ctx.ui.get / ctx.ui.set — acesso em tempo de execução aos controles de painel (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"
O get devolve o valor principal como cadeia:
- Label / Button / LineEdit / ComboBox → texto
- Checkbox →
"true"/"false" - NumberSpin → inteiro em decimal
- List → texto do item selecionado no momento (cadeia vazia se nenhum)
Um set iniciado pelo host é silencioso: ele NÃO dispara de novo o on_change do
controle, então você pode atualizar os seus próprios widgets a partir de um
manipulador de comando sem laços infinitos.
5. Exemplo completo resolvido — plugin wordcount¶
Copie isto literalmente e mude o id, o name e os title para o seu caso. Ele
exercita todos os serviços da 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")
Fluxo esperado do usuário: abrir o editor, salvar um arquivo três vezes e então
clicar em Plugins → Word Count → Report save count. A barra de status exibe
3 save(s) this session. O log registra cada passo.
6. Tarefas comuns — receitas para copiar e colar¶
Cada receita é um trecho só de corpo: cole dentro de def register(ctx):.
Acrescentar um comando de 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")
Reagir ao salvamento¶
Avisar na saída¶
@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
ctx.log.warning("mine: last chance to persist state")
Registrar em um nível específico¶
ctx.log.trace ("very fine")
ctx.log.debug ("dev")
ctx.log.info ("routine")
ctx.log.warning("odd")
ctx.log.error ("wrong")
Separador de menu¶
Vários comandos relacionados¶
@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")
Alternância persistente de liga/desliga com marca visível¶
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)
Painel lateral com uma entrada de texto, uma caixa de seleção e dois botões¶
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", "")
Página de Preferências espelhando as configurações de um painel¶
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. Antipadrões — NÃO faça isto¶
print() para diagnóstico¶
Errado:
Certo:
register dentro de uma classe¶
Errado:
Certo:
async def register¶
Errado:
Certo:
Importações de terceiros sem distribuir o wheel¶
Errado:
Certo (fase 1): fique na biblioteca padrão. Se você realmente precisa de algo de
terceiros, distribua o wheel dentro da pasta do plugin e estenda o sys.path nas
primeiras linhas do main.py; veja Empacotamento.
Importações do Qt¶
Errado:
Certo: nada de Qt em lugar algum. Toda a interface que você pode contribuir passa
por ctx.menus, ctx.status, ctx.diagnostics, ctx.compare e os painéis e
páginas de configurações declarativos. Nunca um widget cru.
Bloquear em um manipulador¶
Errado:
def _handler():
time.sleep(30) # <-- stalls the worker for 30s
result = requests.get("https://slow.example")
Certo (fase 1): delegue a uma thread e comunique-se por 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")
Caminho de menu cujo segmento do topo não está no plugin.json¶
Errado:
Certo: mantenha o segmento de primeiro nível do caminho alinhado com menus[]. Use
"Plugins/Mine/Do" no caso acima.
Guardar ctx para uso posterior¶
Errado:
_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?
Certo: capture no closure que os usa os serviços de que você precisa. O register é
onde a fiação acontece; todo o resto é um manipulador que já tem acesso por closure.
8. Lista de verificação de depuração para o usuário¶
Inclua isto ao final da sua resposta quando produzir um plugin, para que o usuário saiba o que conferir se ele não funcionar:
- Inicie o editor a partir de um shell em que um interpretador Python 3.9+ esteja
no
PATH(ou em que oMTE_PYTHONaponte para um). - Procure
PluginHost: Python plugin backend enabled; interpreter=…no log. Se estiver faltando, vá para Solução de problemas. - Procure
[python:<your.id>] register() ran(se você seguiu o exemplo dowordcount) — prova que oregisterfoi executado. - Se o item de menu estiver faltando, procure no log explicações do tipo
PythonPluginBackend: skipping '<id>': …. - Se clicar no menu não faz nada, procure no log linhas
[worker stderr] …— os rastreamentos do Python param ali.
9. Restrições a reiterar ao final de toda resposta¶
Quando você produzir um plugin, sempre diga ao usuário:
- Instale-o de uma das duas formas:
- compacte o conteúdo da pasta como
<name>.mteplugine use Preferências → Plug-ins → Instalar um plug-in... (valida o pacote antes de tocar em qualquer coisa), ou - coloque a pasta no diretório de plugins por usuário do editor
(
%LOCALAPPDATA%/MTE/Plugins/no Windows;~/Library/Application Support/MTE/Plugins/no macOS;~/.local/share/MTE/Plugins/no Linux).
- compacte o conteúdo da pasta como
- Reinicie o editor.
- Defina o
MTE_PYTHONse o log disser "no Python interpreter found". - Informe a linha de log
[python:<id>] …para que possamos confirmar que ele rodou.
10. Fonte de verdade¶
Se qualquer afirmação desta página conflitar com o que o pacote cliente Python de
fato implementa, o pacote vence. Ele é distribuído ao lado do editor em
<exe>/Python/mte/ — leia aquele código-fonte antes de decidir um comportamento. O
lado C++ do protocolo está em Components/PluginHost/Src/PyServiceBridge.cpp e a
especificação do canal está em Python/PROTOCOL.md.