Vai al contenuto

API per plugin — panoramica

Tutti i tipi di PluginApi vivono nel namespace MTE::plugin. Le intestazioni pubbliche non possono includere alcuna intestazione Qt, Scintilla o interna di MTE — solo la libreria standard. L'unica eccezione deliberata è la cartella opzionale PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h) per i plugin che distribuiscono i propri widget Qt.

Lo stesso contratto è soddisfatto da due backend: il caricatore Qt nativo (librerie condivise) e il backend Python fuori processo (Plugin Python). Questa sezione documenta la superficie C++; ogni servizio qui presente che sia neutro rispetto al backend ha una controparte Python su ctx.

Ciclo di vita di un plugin

Un plugin nativo implementa una sola classe:

class IPlugin {
public:
    virtual PluginInfo info() const = 0;                    // id, name, version, apiVersion
    virtual bool initialize(IPluginContext& ctx) = 0;       // register everything here
    virtual void shutdown() noexcept = 0;                   // release every token
};
  • info() deve restituire gli stessi id e apiVersion di plugin.json; l'host li confronta e, in caso di discordanza, rifiuta di caricare.
  • initialize() viene eseguito una volta all'avvio (dopo che ogni plugin è stato individuato). Registra qui comandi, menu, fornitori e pannelli; restituisci false solo in caso di un vero fallimento (l'host scarica il plugin e lo registra nel log).
  • shutdown() viene eseguito all'uscita (o quando il plugin sta per essere scaricato). Non deve sollevare eccezioni; esegui reset() su ogni token e azzera i puntatori ai servizi che hai memorizzato.

Token RAII

Ogni registrazione restituisce un token (CommandToken, MenuToken, DockToken, SettingsPageToken, Subscription, …) — un BasicToken spostabile ma non copiabile che possiede la registrazione: distruggerlo o chiamarne reset() revoca il comando / la voce di menu / il pannello / la sottoscrizione. Conserva ogni token come membro e rilascialo in shutdown(). Contano due funzioni di accesso:

  • id() — una vista TokenId non distruttiva, per le API che indirizzano l'elemento senza prenderne possesso (per esempio IMenuRegistry::setItemChecked / setItemEnabled).
  • release() — stacca e restituisce l'id; il token non revoca più nulla. Raramente è quello che vuoi.

Il localizzatore di servizi

initialize() riceve un IPluginContext, un localizzatore di servizi posseduto dall'host. Non conservare il contesto oltre initialize(); conserva i singoli riferimenti ai servizi che ti servono (ogni servizio sopravvive a qualsiasi plugin).

Accesso Interfaccia Serve per
ctx.editor() IEditorService leggere/modificare il buffer, la selezione, il cursore, i file, la ricerca, i segnalibri, il rientro, i gruppi della vista divisa
ctx.commands() ICommandRegistry registrare / invocare azioni dell'utente (con scorciatoie)
ctx.menus() IMenuRegistry voci di menu, separatori, interruttori spuntabili, disattivazione delle voci
ctx.docks() IDockRegistry pannelli laterali ancorabili
ctx.documents() IDocumentTypeRegistry tipi di scheda di primo livello della MainWindow (diff, vista di log, …)
ctx.compare() ICompareService aprire la vista di confronto a due riquadri
ctx.diagnostics() IDiagnosticsSink pubblicare sottolineature ondulate + glifi nel margine
ctx.events() IEventBus sottoscrivere / pubblicare eventi dell'editor
ctx.completions() ICompletionRegistry contribuire candidati al completamento automatico
ctx.contextMenu() IContextMenuRegistry voci dinamiche nel menu contestuale dell'editor
ctx.snippetSessions() ISnippetSessionService fornire frammenti espandibili con Tab (la sessione dei campi la gestisce l'host)
ctx.hover() IHoverRegistry suggerimenti al passaggio del mouse
ctx.signatureHelp() ISignatureHelpRegistry suggerimenti di chiamata su ( / ,
ctx.symbols() ISymbolRegistry simboli del documento / dell'area di lavoro (vai al simbolo)
ctx.languages() ILanguageRegistry elencare i linguaggi, leggere / cambiare il linguaggio di un documento, contribuire una definizione di linguaggio
ctx.statusBar() IStatusBar messaggi temporanei + segmenti persistenti
ctx.settings() ISettings configurazione chiave/valore persistente per plugin
ctx.appSettings() const ISettings& leggere le impostazioni globali dell'applicazione (AppSettingsKeys.h)
ctx.documentSettings() IDocumentSettings configurazione per documento stratificata con .editorconfig
ctx.settingsRegistry() ISettingsRegistry aggiungere una pagina alla finestra delle Preferenze
ctx.themePalette() IThemePalette colori del tema con nome, integrazione con l'Editor dei temi
ctx.lineMarkers() ILineMarkerService glifi nel margine e tinte di riga che seguono le modifiche (punti di interruzione, strisce delle modifiche); la rivendicazione del clic sul margine
ctx.log() ILogger trace / debug / info / warning / error
ctx.pluginDataDir() std::filesystem::path la tua cartella privata scrivibile
ctx.diagnosticsDir() std::filesystem::path cartella condivisa di artefatti di crash + log (in sola lettura per i plugin)

Estendere l'API in seguito significa aggiungere una nuova interfaccia di servizio e il relativo accesso: un'aggiunta compatibile secondo la regola di versionamento, per cui i plugin esistenti continuano a caricarsi. La toolchain che la compilazione di un plugin deve rispettare (compilatore, runtime, minor di Qt) è elencata in Scrivere un plugin, «Requisiti di compilazione».

IEditorService — il modello del documento

L'editor come lo vedono i plugin: niente Scintilla, niente Qt. I documenti sono referenziati da un DocumentId opaco (std::uint64_t, 0 = non valido). Gli scostamenti sono scostamenti in byte nel testo UTF-8; MatchRange è un intervallo semiaperto {start, end} in cui noMatch() significa «non trovato»; EditorPosition è {line, column} a base 0.

// Documents
DocumentId               activeDocument() const;
std::vector<DocumentId>  openDocuments() const;
void                     setActiveDocument(DocumentId);
std::optional<std::filesystem::path> filePath(DocumentId) const;
DocumentId               openFile(std::filesystem::path);
DocumentId               openFileAt(std::filesystem::path, EditorPosition); // jump there
void                     revealLine(DocumentId, std::int64_t line);        // show, don't move
bool                     saveDocument(DocumentId);
bool                     closeDocument(DocumentId);  // prompts if modified
bool                     isModified(DocumentId) const;
DocumentEncoding         documentEncoding(DocumentId) const;
void                     setDocumentEncoding(DocumentId, DocumentEncoding);
LineEnding               lineEnding(DocumentId) const;
void                     setLineEnding(DocumentId, LineEnding);

// Text, selection, caret
std::string   text(DocumentId) const;              // whole buffer
std::string   textRange(DocumentId, start, end) const;
void          setText(DocumentId, std::string_view);
std::int64_t  length(DocumentId) const;
std::string   selectedText(DocumentId) const;
void          replaceSelection(DocumentId, std::string_view);
MatchRange    selectionRange(DocumentId) const;
void          setSelection(DocumentId, MatchRange);
EditorPosition caret(DocumentId) const;
void          setCaret(DocumentId, EditorPosition);

// Search & replace (SearchOptions: case, whole-word, regex, wrap)
MatchRange    findInRange(DocumentId, needle, range, SearchOptions) const;
void          replaceTarget(DocumentId, MatchRange, std::string_view);

// Bookmarks, marks, URL highlighting
void toggleBookmark(DocumentId, line);   std::vector<std::int64_t> bookmarkedLines(DocumentId) const;
void markRange(DocumentId, MatchRange);  void clearMarks(DocumentId);

// Language & indentation
std::string      colorizerId(DocumentId) const;      // "cpp", "" if none
IndentationStyle indentation(DocumentId) const;      // {useTabs, width}
void             setIndentation(DocumentId, bool useTabs, int width);
void             setWrapIndent(DocumentId, WrapIndentMode); // where wrapped
WrapIndentMode   wrapIndent(DocumentId) const;             // continuation rows
                 // start: LeftMargin / SameAsLine / Indented

// Semantic classification (from the active colorizer's style rules;
// cheap -- no re-lex). CategorySpan = {start, length, SyntaxCategory}.
// The spell checker keeps to Comment/Documentation/String spans this way.
std::vector<CategorySpan> textCategories(DocumentId, start, end) const;

// Clipboard
std::string clipboardText() const;

Espone anche la disposizione dei gruppi di schede della vista divisa in termini neutri rispetto al backend (groupCount(), groupOf(), groupWeights(), moveDocumentToGroup(), setGroupWeights()), così che un plugin possa catturare e ripristinare le disposizioni dei riquadri senza vedere lo splitter di Qt. Vedi PluginApi/IEditorService.h per l'elenco completo: fa fede l'intestazione.

Un documento mostrato in due riquadri è un unico documento con un solo DocumentId, quindi groupOf() può nominare solo una delle sue sedi. Due metodi descrivono e ricostruiscono una divisione:

std::vector<int> groupsOf(DocumentId doc) const;      // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group);  // put it in one more

showDocumentInGroup() attiva una vista già presente in quel riquadro e altrimenti vi crea una seconda vista condivisa — la stessa cosa che fa Visualizza ▸ Dividi il documento a destra — quindi non viene sollevato alcun DocumentOpened ed entrambe le viste modificano un unico documento. I riquadri sono il concetto rivolto ai plugin; le singole viste non lo sono, ed è per questo che la chiamata nomina un riquadro invece di restituire una vista.

Portare l'utente da qualche parte

openFileAt(path, position) è l'unica chiamata dietro «vai all'errore di compilazione», «salta al frame dello stack» o «apri il risultato della ricerca», così arrivano tutti allo stesso modo. Apre il file esattamente come openFile() (la scheda di un file già aperto viene attivata, non aperta una seconda volta), lo rende il documento attivo, posiziona il cursore senza selezione, espande una piegatura che nasconde la riga, scorre la riga fino a renderla visibile — centrata se era fuori dallo schermo, senza scorrere se era già visibile — e dà all'editor il focus della tastiera. La posizione viene limitata, mai rifiutata: una riga oltre la fine finisce sull'ultima riga, una colonna oltre la fine della riga finisce alla sua fine, i valori negativi valgono 0. Restituisce il documento in cui è stato posizionato il cursore, o 0 se il file non si è potuto aprire, e in quel caso non si è mosso nulla.

revealLine(doc, line) mostra un punto senza portarvi l'utente: espande e scorre il riquadro che mostra doc, ma non sposta il cursore né la selezione, non cambia scheda e non prende il focus; lo usa un pannello che mostra l'anteprima di un frame dello stack o segue la propria selezione.

Codifica del documento

La codifica dei caratteri su disco di un documento è stato del nucleo, non di un plugin. L'host la rileva quando il file viene letto e riscrive la stessa codifica a ogni salvataggio, così un file UTF-8 con BOM conserva il suo marcatore e un file UTF-16 resta UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) è un enum POD indipendente dal backend che copre le cinque codifiche Unicode/ANSI principali più le vecchie code page.

setDocumentEncoding() registra una scelta: non cambia alcun byte e non ridecodifica nulla — il buffer è già Unicode e la nuova codifica ha effetto al salvataggio successivo. Un plugin che offre un'azione «converti in» dovrebbe quindi chiamare anche setModified(doc, true); uno che offre «interpreta come» ricarica prima il testo da sé e poi registra la scelta. L'host pubblica DocumentEncodingChanged ogni volta che il valore cambia davvero. Un charset di un .editorconfig di progetto prevale comunque al momento del salvataggio.

Fine riga del documento

Anche il terminatore di riga di un documento è stato del nucleo. L'host lo rileva quando il file viene letto, lo trasmette alla vista così che Invio inserisca quella sequenza, e riscrive i byte del buffer alla lettera — così un file Unix modificato su Windows resta LF e un salvataggio senza modifiche è identico byte per byte. LineEnding (PluginApi/LineEnding.h) è un enum POD indipendente dal backend — Unknown / Lf / CrLf / Cr — che nomina la sequenza di byte anziché una costante del backend. Porta con sé due ausili inline: lineEndingBytes() e platformLineEnding() (ciò con cui parte un nuovo buffer: CRLF su Windows, LF altrove).

setLineEnding() registra la scelta, riaccorda la vista del documento così che il prossimo Invio la inserisca, e decide cosa scriverà il salvataggio successivo. Non converte alcun testo esistente: un plugin che riscrive l'intero buffer (i comandi di Conversione di fine riga) sostituisce prima il testo da sé, così che la conversione resti una sola modifica annullabile. L'host pubblica DocumentLineEndingChanged ogni volta che il valore cambia davvero. Un end_of_line di un .editorconfig di progetto prevale comunque al momento del salvataggio, e con sé porta il documento su quella convenzione.

Thread

Tutte le chiamate al contesto e ai servizi avvengono sul thread dell'interfaccia dell'host. Non chiamare i servizi da un thread in background che hai avviato tu; riporta prima il controllo.

Versionamento

Il contratto ha una versione in due parti, entrambe in PluginApi/PluginInfo.h:

constexpr std::uint32_t MTE_PLUGIN_API_VERSION       = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor

A decidere tra le due è chi implementa l'interfaccia:

Modifica Tipo Versione
Un metodo aggiunto in fondo a un'interfaccia che solo l'editor implementa e che i plugin si limitano a chiamare (IEditorService, IPluginContext, ogni I…Registry, ILineMarkerService, …) compatibile minor + 1
Una nuova interfaccia, una nuova struttura di evento, un nuovo enumeratore in fondo a un'enumerazione compatibile minor + 1
Qualsiasi modifica a una struttura che attraversa il confine (PluginInfo, ContextMenuRequest, DockPanelDescriptor, una struttura di evento) incompatibile major + 1
Una firma modificata, un metodo rimosso o riordinato, un nuovo overload di un nome esistente incompatibile major + 1
Qualsiasi modifica a un'interfaccia che un plugin implementa (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) incompatibile major + 1
Un rilascio compilato con una toolchain la cui interfaccia binaria differisce dalla precedente: un'altra famiglia di compilatori o un'altra libreria standard, una nuova versione principale di Qt incompatibile major + 1

Un toolset più recente della stessa famiglia di compilatori, un diverso livello dello standard C++ e una minor più recente di Qt non sono una rottura di questo tipo e non cambiano nessuno dei due numeri; vedi Requisiti di compilazione per ciò che un plugin nativo deve rispettare.

L'editor carica un plugin solo quando

  • apiVersion in plugin.json e IPlugin::info().apiVersion sono uguali alla sua major, e
  • apiMinorVersion in plugin.json (facoltativo, predefinito 0) e IPlugin::info().apiMinorVersion non superano la sua minor. Il valore in info() vale per impostazione predefinita la minor degli header con cui il plugin è stato compilato; un plugin che sa di non usare nulla di più recente può indicare un numero più basso in plugin.json, e deve comunque superare il secondo controllo.

Un plugin rifiutato resta elencato in Preferenze ▸ Estensioni con il motivo («Incompatibile (versione API diversa)», «Richiede una versione più recente di OctoEdit») e riceve una voce nel log, mai un arresto anomalo. Un plugin Python non ha un info() compilato con gli header, quindi il suo plugin.json è la sua unica dichiarazione: indica la minor della chiamata più recente che usi.

Politica prima del rilascio

Prima del primo rilascio pubblico il contratto può ancora cambiare senza incrementare nessuno dei due numeri; fino ad allora restano entrambi a 1 / 0. Dal primo rilascio ogni modifica viene valutata secondo la tabella qui sopra.