Hyppää sisältöön

Modular Text Editorin Python-liitännäisen kirjoittaminen Clauden kanssa

Kohdeyleisö: tekoälyavustaja (Claude, ChatGPT ja muut). Tämä sivu on itsenäinen ohjejoukko: anna se avustajalle yhdessä tehtävän kanssa (»kirjoita minulle liitännäinen, joka tekee X«), niin avustajan tulisi tuottaa täydellinen, oikea ja toimiva liitännäiskansio ilman että sen tarvitsee lukea muuta dokumentaatiota.

Jos olet ihminen, joka yrittää kirjoittaa liitännäisen käsin, aloita täältä — tämä sivu on optimoitu tekoälyn luettavaksi ja ohittaa ystävälliset esimerkit.


1. Mitä tuotat

Kansion, jossa on täsmälleen kaksi tiedostoa (plus valinnaisia lisiä):

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

Palauta kansio yhtenä koodilohkona tiedostoa kohti. Käyttäjä joko pudottaa kansion editorin liitännäishakemistoon tai pakkaa kansion zipiksi, nimeää arkiston uudelleen muotoon <name>.mteplugin ja asentaa sen kohdasta Asetukset → Liitännäiset → Asenna liitännäinen.... Molemmat tavat vaativat uudelleenkäynnistyksen latautuakseen.

Nämä kaksi tiedostoa on kuvattu tyhjentävästi alla. Noudata sääntöjä kirjaimellisesti — isäntä tekee staattisen tarkistuksen ja hylkää liitännäiset, jotka rikkovat niitä.


2. plugin.json — kanoninen mallipohja

{
  "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 }
  ]
}

Ehdottomat säännöt

  1. "apiVersion" täytyy olla 1 (isännän nykyinen versio).
  2. "entry" täytyy olla "python" — mikään muu ei reitity tähän taustajärjestelmään.
  3. "module" täytyy olla "main", ellei ole nimenomaista syytä nimetä moduulia toisin — eikä koskaan pistein erotettua polkua (vaihe 1 tukee vain litteitä moduulinimiä).
  4. "id" täytyy olla käänteisen DNS:n tyylinen (org.<vendor>.<name>) ja globaalisti yksilöllinen käyttäjän asennettujen liitännäisten kesken. Jos käyttäjä ei määrittänyt toimittajaa, käytä muotoa org.example..
  5. menus[] täytyy sisältää vähintään yksi kohta, jonka title vastaa jokaisen tiedostossa main.py käyttämäsi valikkopolun vasemmanpuoleisinta osaa. Suosi arvoa "Plugins" kaikelle käyttäjälle näkyvälle, mikä ei selvästi kuulu ytimen valikkoon.

Valinnaiset kentät

  • "pyRequires": [] — taulukko pip-vaatimusmerkkijonoja (esimerkiksi ["requests>=2.31"]). Kun se ei ole tyhjä, editori valmistelee liitännäiskohtaisen virtuaaliympäristön ensimmäisellä käynnistyksellä (pip install — vaatii verkon tuon yhden kerran) ja ajaa liitännäisen sen alla. Suosi standardikirjastoa, kun se hoitaa työn (ei valmisteluviivettä, toimii verkotta); ilmoita pyRequires, kun tehtävä aidosti tarvitsee kolmannen osapuolen paketin, ja kerro käyttäjälle, että ensimmäinen käynnistys pysähtyy asentamaan sen.
  • "permissions": [] — karkeita kykyilmoituksia (esimerkiksi ["network"]), näytettynä vain luettavana liitännäissivulla. Ilmoita "network", kun liitännäinen tekee ulospäin suuntautuvia kutsuja; se on rehellistä merkintää käyttäjälle eikä pakotusta.
  • "requires": [] — MUIDEN liitännäisten tunnisteet, joita ilman tämä ei voi toimia (ei pip-paketteja). Ne käynnistyvät ensin; jos jokin puuttuu tai ei ole käytössä, tämä liitännäinen ei käynnisty, ja liitännäissivu kertoo miksi. Jätä tyhjäksi, ellei käyttäjän tehtävä todella riipu toisesta liitännäisestä.
  • "apiMinorVersion": 0 — editorin API:n pienin minor-versio, jota liitännäinen tarvitsee. Jätä arvoksi 0, ellet käytä kutsua, joka on dokumentoitu uudemmaksi.

Arvot, joita sinun EI pidä keksiä

  • Mikä tahansa kenttä, jota ei ole lueteltu yllä. Isäntä ohittaa tuntemattomat avaimet; niiden lisääminen on ylläpitoriski.

3. main.py — kanoninen runko

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

Ehdottomat säännöt

  1. register(ctx) TÄYTYY olla ylätason funktio. Staattinen tarkistus vaatii rivin, joka alkaa merkkijonolla def register( sarakkeesta 0. Jos sijoitat sen luokan sisään, koristelet sen moduulitasolla tai teet siitä async def-funktion, liitännäinen hylätään hiljaisesti etsintähetkellä.
  2. Tuo moduulista mte, kun tilaat tapahtumia — tyypitetyt dataluokat (mte.DocumentSaved ja muut) tulevat tuosta paketista. import mte ylhäällä on turvallista, vaikket käyttäisi sitä suoraan; yllä oleva # noqa: F401 estää lint-melun.
  3. Älä koskaan käytä funktiota print(). Työntekijän stdout on RPC-kanava; siihen kirjoittaminen turmelee protokollan. Käytä funktioita ctx.log.info / warning / error.
  4. Älä koskaan tuo mitään, mitä et ole ilmoittanut. Standardikirjaston ja moduulin mte lisäksi jokaisen kolmannen osapuolen tuonnin täytyy esiintyä tiedoston plugin.json kentässä pyRequires (editori asentaa sen pipillä liitännäisen virtuaaliympäristöön ensimmäisellä käynnistyksellä) tai olla liitetty liitännäiskansioon. Ilmoittamaton tuonti nostaa poikkeuksen ModuleNotFoundError käyttäjän koneella.
  5. Komento- ja tapahtumakäsittelijät saavat estää hetkeksi — ne ajetaan työntekijän säiepoolissa, joten synkroniset kutsut kohteisiin ctx.editor, ctx.settings ja ctx.data_dir ovat sallittuja. Mutta täydennystoimittajan takaisinkutsut (@ctx.completions.provider(...)) EIVÄT SAA estää: ne ajetaan tapahtumasilmukassa noin 30 ms:n määräajalla. Ei I/O:ta, ei kutsuja kohteeseen ctx.editor.*, vain välimuistia. Rakenna hakemistot muualla (esimerkiksi tapahtumassa document.opened) ja vastaa sanakirjasta.
  6. Älä tuo Qt:tä, PySideä, PyQt:tä tai Scintillaa. Ne eivät ole käytettävissä työntekijässä.
  7. Komentotunnisteilla täytyy olla etuliite, joka on liitännäisesi nimiavaruus, jotta vältetään törmäykset ytimen ja muiden liitännäisten kanssa. <id>.<verb> yllä; esimerkiksi wordcount.count tai hello.sayHi.
  8. Valikkopolkujen täytyy alkaa ylimmän tason valikolla, jonka olet myös ilmoittanut tiedoston plugin.json kentässä menus[]. Tapa: "Plugins/<Plugin Name>/<Verb>".

4. Koko API — kaikki kohteessa ctx

Sinulla on kahdeksantoista palvelua: commands, menus, events, log, status, editor, languages, settings, app_settings, document_settings, theme, diagnostics, compare, completions, docks, settings_page, ui sekä data_dir()/diagnostics_dir(). Mitään, mitä ei ole lueteltu täällä, ei ole olemassa — älä keksi palveluita. (Liitännäiskohtaiset virtuaaliympäristöt ilmoitetaan kentässä pyRequires eikä ctx-palveluna; virtaavalla ja peruutettavalla protokolla-alustalla ei ole vielä liitännäisille näkyvää API:ta.)

ctx.commands

@ctx.commands.command(*, id, title, shortcut="", category="",
                      requires_editor=False, requires_writable_editor=False)
def _handler(): ...

Koriste. Rekisteröi komennon ja sitoo funktion _handler (nollaparametrinen funktio, joka palauttaa arvon None). Työntekijä nappaa funktion _handler poikkeukset ja kirjoittaa ne stderriin; ne eivät kaada editoria.

Aseta requires_editor, kun komento lukee aktiivista asiakirjaa (puskuri, valinta, kohdistin), ja requires_writable_editor, kun se muuttaa tekstiä. Isäntä harmaannuttaa silloin komennon siellä, missä se ei voisi toimia — välilehdellä, jolla ei ole asiakirjaa (vertailu- tai lokinäkymä), ja kirjoituslipun osalta vain luettavassa asiakirjassa — sen sijaan että antaisi käyttäjän käynnistää hiljaisen tyhjäkäynnin. Kirjoituslippu sisältää toisen; jätä molemmat pois komennoilta, jotka vain avaavat ikkunan tai paneelin.

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 on "Ylin/Ali/…/Nimiö". Välialivalikot luodaan tarvittaessa.

set_item_checked muuttaa kohdan valittavaksi valikkokohdaksi ja asettaa sen merkin. Käytä sitä pysyviin päälle/pois-kytkimiin. Ensimmäinen kutsu muuttaa kohdan; seuraavat kutsut vain kääntävät. Hiljainen tyhjäkäynti tuntemattomille tunnisteille.

set_item_enabled harmaannuttaa kohdan. Käytä sitä, kun komento tarvitsee edellytyksen, jotta käyttämätön kohta näyttää käyttämättömältä sen sijaan että se ei tekisi mitään napsautettaessa. Idempotentti; kohdat alkavat käytössä olevina; hiljainen tyhjäkäynti tuntemattomille tunnisteille.

ctx.events

Seitsemäntoista aihetta, kukin kevyellä koristeella ja raa'alla muodolla.

Kevyt muoto Raaka aihe Hyötykuorma
@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) — jokaisella muokkauksella, pidä se hyvin yksinkertaisena
@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() — kerran ajoa kohti, ensimmäisen ruudun jälkeen
@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) — napsautus kirjanmerkkisarakkeessa; modifiers-bitit: mte.MODIFIER_*

Raaka muoto:

handler_id = ctx.events.subscribe("<topic>", callback)
ctx.events.unsubscribe(handler_id)  # to unbind

Älä keksi uusia aiheita — raaka muoto tarkistaa ne ja heittää poikkeuksen ValueError kaikesta, mitä ei ole taulukossa.

ctx.log

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

Lähetä ja unohda. Rivit saavat etuliitteen [python:<plugin.id>] isännän diagnostiikkalokissa.

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 täytyy olla "info" / "warning" / "error". timeout_ms=0 tarkoittaa, että isäntä valitsee oletuksen. show() on ohimeneviin ilmoituksiin; kaikki, mikä jää näytölle (elävä laskuri, tilalippu), on osio — nämä kaksi eivät koskaan tuhoa toisiaan.

ctx.editor (vaihe 2)

Kukin metodi on synkroninen: se estää kutsuvan säikeen. Sallittua def-tyyppisestä komento- tai tapahtumatakaisinkutsusta (työntekijä jakaa synkroniset käsittelijät säiepooliin); kiellettyä async def-käsittelijöistä ja kohteen ctx.completions toimittajista (jotka ajetaan suoraan tapahtumasilmukassa).

Asiakirjoihin viitataan läpinäkymättömillä int-tunnisteilla (0 = ei asiakirjaa).

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 (vaihe 2)

Tyypitetty avain–arvo-varasto liitännäiskohtaisesti rajattuna (ctx.settings) sekä vain luettava näkymä sovelluksenlaajuisiin asetuksiin (ctx.app_settings). Tyyppi päätellään oletusarvon Python-tyypistä.

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 heittävät poikkeuksen RuntimeError. Bool täytyy tarkistaa ennen int-tyyppiä omassa logiikassasi lukiessasi (Pythonissa isinstance(True, int) == True).

ctx.completions (vaihe 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)]

Olion req kentät: document, prefix, position, language_id, manual. Lajit: KIND_KEYWORD / KIND_WORD / KIND_SYMBOL / KIND_SNIPPET / KIND_UNKNOWN. Palauta list[CompletionItem], tavallisia merkkijonoja tai sanakirjoja. Takaisinkutsun TÄYTYY palata noin 30 ms:n sisällä, muutoin isäntä pudottaa tuloksesi tuon näppäinpainalluksen osalta — ei I/O:ta, ei kutsua ctx.editor.text(...), vain välimuistia. Rakenna hakemistot muualla (esimerkiksi tapahtumassa document.opened).

ctx.data_dir() / ctx.diagnostics_dir() (vaihe 2)

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

Käytä funktiota data_dir() välimuisteihin ja hakemistoihin. diagnostics_dir() on vain luettava (kaatumisraportoija kirjoittaa sinne).

ctx.docks — kuvailevat telakkapaneelit (vaihe 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")]),
    ]),
)

Rakenna lomakemäärittely mte.ui-apureilla. Ohjausobjektisanasto on kiinteä: Label / Button / Checkbox / LineEdit / NumberSpin / ComboBox / List rakenteiden Column / Row / Group sisällä.

  • binds="settings:<key>" — ohjausobjektin arvo tallentuu automaattisesti kohteen ctx.settings kautta (sama varasto kuin ctx.settings.set).
  • on_click="<cmd.id>" — vain painikkeet; kutsuu rekisteröityä komentoa.
  • on_change=<handler> — varaa funktiolla ctx.docks.on_change(callback); laukeaa käyttäjän muokkauksilla.

on_change-takaisinkutsut ajetaan työntekijän tapahtumasilmukassa (EI säiepoolissa). Pidä ne halpoina — ei editorikutsuja, ei pitkää työtä. Muuta pientä muistissa olevaa tilaa, kirjaa lokiin tai laukaise komento; tee raskas työ komennosta.

Anna initially_visible=False paneelille, joka ei saa ponnahtaa esiin käynnistyksessä, ja tuo se sitten esiin komennostaan kutsulla ctx.docks.set_panel_visible(panel_id, True) (joka myös nostaa sen etualalle; idempotentti; tuntemattomat tunnisteet ohitetaan). ctx.docks.is_panel_visible(panel_id) kertoo, onko paneeli auki (taustalla oleva välilehti lasketaan). Useita yhteen kuuluvia paneeleita: anna sama tab_group="<plugin.id>/panels" ja sama area, niin ne jakavat yhden välilehdin varustetun telakan.

ctx.line_markers — merkit asiakirjan riveillä

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

Rekisteröi lajit kerran funktiossa register(); editorissa on vain muutama paikka. Varaa marginaalin napsautus vain, kun liitännäinen antaa sille merkityksen — napsautukset saapuvat joka tapauksessa koristeen @ctx.events.on_margin_clicked kautta.

ctx.languages — syntaksivärjäys

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 täytyy nimetä olemassa oleva taustajärjestelmän leksikaalinen jäsennin; styles ja keywords käyttävät jäsentimen omia nimiä (sama muoto kuin mukana tulevissa colorizers/*.json-tiedostoissa), eivät koskaan numeerisia tunnisteita.

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 — aaltoviivat

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

Lähetä KOKO joukko lähteellesi joka kerta; se korvaa edellisen. source on pakollinen.

ctx.theme — teemoitettavat värit omaan käyttöliittymääsi

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 — vierekkäinen vertailu

ctx.compare.open(mte.CompareSource(title, path=..., content=...),
                 mte.CompareSource(...), ignore_whitespace=False,
                 ignore_case=False, ignore_eol=False)   # -> bool

ctx.events.publish

Vain aiheet workspace.rootChanged / workspace.filesChanged ovat liitännäisen julkaistavissa; mikä tahansa muu heittää poikkeuksen ValueError. Jokainen isäntätapahtuma paitsi document.aboutToSave on tilattavissa (katso koristeluettelo); natiivilla tapahtumalla DocumentAboutToClose ei ole lainkaan Python-aihetta — käytä aihetta document.closed.

ctx.settings_page — kuvailevat asetussivut (vaihe 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"),
    ]),
)

Samat rakentajat, sama binds-semantiikka. Kaksi ohjausobjektia, jotka jakavat arvon binds="settings:<key>", peilaavat toisiaan reaaliajassa — telakkapaneelilla ja asetussivulla voi molemmilla olla valintaruutu samalle asetukselle, ja toisen kytkeminen kääntää toisen.

Vaiheen 3 varauma

Arvot tallentuvat innokkaasti jokaisella käyttäjän muokkauksella — Peruuta ei peru sitä, mitä käyttäjä muutti. Tämä paranee tulevassa hienosäädössä.

ctx.ui.get / ctx.ui.set — ajonaikainen pääsy paneelien ohjausobjekteihin (vaihe 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 palauttaa ensisijaisen arvon merkkijonona:

  • Label / Button / LineEdit / ComboBox → teksti
  • Checkbox → "true" / "false"
  • NumberSpin → kokonaisluku desimaalina
  • List → valitun kohdan teksti (tyhjä merkkijono, jos ei mitään)

Isännän ohjaama set on hiljainen: se EI laukaise ohjausobjektin on_change-kutsua uudelleen, joten voit päivittää omia ohjausobjektejasi komentokäsittelijästä ilman äärettömiä silmukoita.


5. Täysi läpikäyty esimerkki — wordcount-liitännäinen

Kopioi tämä sanatarkasti ja muuta kentät id, name ja otsikot sopimaan tapaukseesi. Se käyttää jokaista vaiheen 1 palvelua.

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

Käyttäjän odotettu kulku: avaa editori, tallenna tiedosto kolme kertaa ja napsauta sitten Plugins → Word Count → Report save count. Tilarivillä näkyy 3 save(s) this session. Loki kirjaa jokaisen vaiheen.


6. Yleiset tehtävät — kopioitavat reseptit

Kukin resepti on pelkkä runko: liitä se funktion def register(ctx): sisään.

Valikkokomennon lisääminen

@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")

Tallennukseen reagoiminen

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

Varoittaminen lopetettaessa

@ctx.events.on_app_about_to_quit
def _on_quit(_ev):
    ctx.log.warning("mine: last chance to persist state")

Kirjaaminen tietylle tasolle

ctx.log.trace  ("very fine")
ctx.log.debug  ("dev")
ctx.log.info   ("routine")
ctx.log.warning("odd")
ctx.log.error  ("wrong")

Valikkoerotin

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

Useita toisiinsa liittyviä komentoja

@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")

Pysyvä päälle/pois-kytkin näkyvällä valintamerkillä

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)

Sivupaneeli, jossa on tekstinsyöttö, valintaruutu ja kaksi painiketta

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", "")

Asetussivu, joka peilaa paneelin asetuksia

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. Antimallit — ÄLÄ tee näitä

Väärin:

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

Oikein:

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

register luokan sisällä

Väärin:

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

Oikein:

def register(ctx):
    ...

async def register

Väärin:

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

Oikein:

def register(ctx):
    ...

Kolmannen osapuolen tuonnit ilman wheel-tiedoston toimittamista

Väärin:

import requests   # <-- ImportError on most user machines

Oikein (vaihe 1): pysy standardikirjastossa. Jos todella tarvitset jotain kolmannelta osapuolelta, toimita wheel liitännäiskansion sisällä ja laajenna polkua sys.path tiedoston main.py ensimmäisillä riveillä; katso Paketointi.

Qt-tuonnit

Väärin:

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

Oikein: ei Qt:tä missään. Kaikki käyttöliittymä, jota voit tarjota, kulkee kohteiden ctx.menus, ctx.status, ctx.diagnostics, ctx.compare sekä kuvailevien telakka- ja asetuspaneelien kautta. Ei koskaan raakaa ohjausobjektia.

Estäminen käsittelijässä

Väärin:

def _handler():
    time.sleep(30)   # <-- stalls the worker for 30s
    result = requests.get("https://slow.example")

Oikein (vaihe 1): siirrä se säikeeseen ja viesti kohteen ctx.log kautta:

def _handler():
    import threading
    def _work():
        time.sleep(30)
        ctx.log.info("done")
    threading.Thread(target=_work, daemon=True).start()
    ctx.status.show("started")

Valikkopolku, jonka ylin osa ei ole tiedostossa plugin.json

Väärin:

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

Oikein: pidä polun ylin osa linjassa kentän menus[] kanssa. Käytä yllä muotoa "Plugins/Mine/Do".

Olion ctx säilyttäminen myöhempää käyttöä varten

Väärin:

_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?

Oikein: kaappaa tarvitsemasi palvelut siihen sulkeumaan, joka niitä käyttää. register on se paikka, jossa kytkennät tehdään; kaikki muu on käsittelijä, jolla on jo pääsy sulkeuman kautta.


8. Vianetsintälista käyttäjälle

Sisällytä tämä vastauksesi loppuun, kun tuotat liitännäisen, jotta käyttäjä tietää, mitä tarkistaa, jos se ei toimi:

  1. Käynnistä editori komentotulkista, jossa Python 3.9+ -tulkki on PATH-muuttujassa (tai jossa MTE_PYTHON osoittaa sellaiseen).
  2. Etsi lokista tekstiä PluginHost: Python plugin backend enabled; interpreter=…. Jos se puuttuu, hyppää kohtaan Vianetsintä.
  3. Etsi tekstiä [python:<your.id>] register() ran (jos noudatit wordcount-esimerkkiä) — se todistaa, että register ajettiin.
  4. Jos valikkokohta puuttuu, tarkista lokista selitykset muotoa PythonPluginBackend: skipping '<id>': ….
  5. Jos valikon napsauttaminen ei tee mitään, tarkista lokista rivit [worker stderr] … — Pythonin jäljitykset päätyvät sinne.

9. Rajoitukset, jotka toistetaan jokaisen vastauksen lopussa

Kun tuotat liitännäisen, kerro aina käyttäjälle:

  • Asenna se jommallakummalla tavalla:
    • pakkaa kansion sisältö zipiksi nimellä <name>.mteplugin ja käytä kohtaa Asetukset → Liitännäiset → Asenna liitännäinen... (joka tarkistaa paketin ennen kuin mihinkään kosketaan), tai
    • pudota kansio editorin käyttäjäkohtaiseen liitännäishakemistoon (%LOCALAPPDATA%/MTE/Plugins/ Windowsissa; ~/Library/Application Support/MTE/Plugins/ macOS:ssä; ~/.local/share/MTE/Plugins/ Linuxissa).
  • Käynnistä editori uudelleen.
  • Aseta MTE_PYTHON, jos loki sanoo »no Python interpreter found«.
  • Raportoi takaisin [python:<id>] …-lokirivi, jotta voimme vahvistaa sen ajautuneen.

10. Totuuden lähde

Jos jokin tämän sivun väite on ristiriidassa sen kanssa, mitä Python-asiakaspaketti todella toteuttaa, paketti voittaa. Se toimitetaan editorin viereen polkuun <exe>/Python/mte/ — lue tuo lähdekoodi ennen kuin päätät käyttäytymisestä. Protokollan C++-puoli on tiedostossa Components/PluginHost/Src/PyServiceBridge.cpp, ja siirtotien määrittely on tiedostossa Python/PROTOCOL.md.