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 stessiideapiVersiondiplugin.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; restituiscifalsesolo 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; eseguireset()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 vistaTokenIdnon distruttiva, per le API che indirizzano l'elemento senza prenderne possesso (per esempioIMenuRegistry::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
apiVersioninplugin.jsoneIPlugin::info().apiVersionsono uguali alla sua major, eapiMinorVersioninplugin.json(facoltativo, predefinito0) eIPlugin::info().apiMinorVersionnon superano la sua minor. Il valore ininfo()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 inplugin.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.