Plugin API — visão geral¶
Todos os tipos do PluginApi ficam no namespace MTE::plugin. Os cabeçalhos
públicos não podem incluir nenhum cabeçalho do Qt, do Scintilla ou interno do MTE —
apenas a biblioteca padrão. A única exceção deliberada é a pasta opcional
PluginApi/Qt/ (PluginQtGlue.h, ISettingsPage.h, ThemeFormat.h), para
plugins que trazem os próprios widgets do Qt.
O mesmo contrato é atendido por dois backends: o carregador nativo do Qt
(bibliotecas compartilhadas) e o backend Python fora de processo
(plugins Python). Esta seção documenta a superfície
de C++; todo serviço aqui que é neutro em relação ao backend tem um equivalente
Python em ctx.
Ciclo de vida de um plugin¶
Um plugin nativo implementa uma 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()precisa devolver o mesmoideapiVersiondoplugin.json; o host confere e se recusa a carregar em caso de divergência.initialize()roda uma vez na inicialização (depois que todo plugin é descoberto). Registre comandos, menus, provedores e painéis aqui; devolvafalseapenas em uma falha real (o host descarrega o plugin e registra isso no log).shutdown()roda na saída (ou quando o plugin está sendo descarregado). Ele não pode lançar exceções; façareset()em todos os tokens e anule os ponteiros de serviço guardados.
Tokens RAII¶
Todo registro devolve um token (CommandToken, MenuToken, DockToken,
SettingsPageToken, Subscription, …) — um BasicToken apenas movível que
possui o registro: destruí-lo ou fazer reset() nele revoga o comando / item
de menu / painel / assinatura. Mantenha cada token como membro e libere-o em
shutdown(). Dois acessores importam:
id()— uma visãoTokenIdnão destrutiva, para APIs que se dirigem ao item sem tomá-lo (por exemplo,IMenuRegistry::setItemChecked/setItemEnabled).release()— desvincula e devolve o id; o token não revoga mais nada. Raramente é o que você quer.
O localizador de serviços¶
initialize() recebe um IPluginContext, um localizador de serviços que pertence
ao host. Não guarde o contexto além do initialize(); guarde as referências
aos serviços individuais de que você precisa (cada serviço sobrevive a todos os
plugins).
| Acessor | Interface | Usado para |
|---|---|---|
ctx.editor() |
IEditorService |
ler/editar buffer, seleção, cursor, arquivos, busca, marcadores, recuo, grupos de visualização dividida |
ctx.commands() |
ICommandRegistry |
registrar / invocar ações do usuário (com atalhos) |
ctx.menus() |
IMenuRegistry |
itens de menu, separadores, alternâncias marcáveis, esmaecer itens |
ctx.docks() |
IDockRegistry |
painéis laterais acopláveis |
ctx.documents() |
IDocumentTypeRegistry |
tipos de aba de primeira classe da janela principal (diff, visualização de log, …) |
ctx.compare() |
ICompareService |
abrir a visualização de comparação de dois painéis |
ctx.diagnostics() |
IDiagnosticsSink |
publicar traços ondulados e glifos de margem |
ctx.events() |
IEventBus |
assinar e publicar eventos do editor |
ctx.completions() |
ICompletionRegistry |
contribuir candidatos de autocompletar |
ctx.contextMenu() |
IContextMenuRegistry |
entradas dinâmicas no menu de contexto do editor |
ctx.snippetSessions() |
ISnippetSessionService |
fornecer snippets expansíveis com Tab (o host conduz a sessão de campos) |
ctx.hover() |
IHoverRegistry |
dicas ao passar o mouse |
ctx.signatureHelp() |
ISignatureHelpRegistry |
dicas de chamada em ( / , |
ctx.symbols() |
ISymbolRegistry |
símbolos de documento / espaço de trabalho (ir para símbolo) |
ctx.languages() |
ILanguageRegistry |
listar linguagens, ler / trocar a linguagem de um documento, contribuir uma definição de linguagem |
ctx.statusBar() |
IStatusBar |
mensagens passageiras e segmentos persistentes |
ctx.settings() |
ISettings |
configuração persistente chave/valor por plugin |
ctx.appSettings() |
const ISettings& |
ler as configurações de todo o aplicativo (AppSettingsKeys.h) |
ctx.documentSettings() |
IDocumentSettings |
configuração por documento em camadas com .editorconfig |
ctx.settingsRegistry() |
ISettingsRegistry |
acrescentar uma página à caixa de diálogo Preferências |
ctx.themePalette() |
IThemePalette |
cores de tema nomeadas, integração com o Editor de temas |
ctx.lineMarkers() |
ILineMarkerService |
glifos na margem e tons de linha que acompanham as edições (pontos de interrupção, faixas de alterações); a reivindicação do clique na margem |
ctx.log() |
ILogger |
trace / debug / info / warning / error |
ctx.pluginDataDir() |
std::filesystem::path |
seu diretório privado gravável |
ctx.diagnosticsDir() |
std::filesystem::path |
diretório compartilhado de artefatos de falha e logs (somente leitura para plugins) |
Estender a API depois significa acrescentar uma nova interface de serviço e o acessor dela — uma adição compatível pela regra de versionamento: os plugins existentes continuam sendo carregados. A cadeia de ferramentas com a qual a compilação de um plugin precisa coincidir (compilador, runtime, versão menor do Qt) está listada em Escrevendo um plugin, "Requisitos de compilação".
IEditorService — o modelo de documento¶
O editor como os plugins o veem — sem Scintilla, sem Qt. Os documentos são
referenciados por um DocumentId opaco (std::uint64_t, 0 = inválido). Os
deslocamentos são deslocamentos em bytes dentro do texto UTF-8; MatchRange é
um intervalo semiaberto {start, end} e noMatch() significa "não encontrado";
EditorPosition é um {line, column} começando em 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;
Ele também expõe o layout de grupos de abas da visualização dividida em termos
neutros em relação ao backend (groupCount(), groupOf(), groupWeights(),
moveDocumentToGroup(), setGroupWeights()), de modo que um plugin possa capturar
e restaurar disposições de painéis sem ver o divisor do Qt. Veja
PluginApi/IEditorService.h para a lista completa — o cabeçalho é a referência que
manda.
Um documento exibido em dois painéis é um documento com um único DocumentId,
então groupOf() só pode nomear um dos assentos dele. Dois métodos descrevem e
reconstroem uma divisão:
std::vector<int> groupsOf(DocumentId doc) const; // every pane showing it
bool showDocumentInGroup(DocumentId doc, int group); // put it in one more
showDocumentInGroup() ativa uma visualização que já está naquele painel e, caso
contrário, cria ali uma segunda visualização compartilhada — a mesma coisa que
Exibir ▸ Dividir documento à direita faz, então nenhum DocumentOpened é
emitido e as duas visualizações editam um único documento. Painéis são o conceito
voltado aos plugins; visualizações individuais não são, e é por isso que a chamada
nomeia um painel em vez de devolver uma visualização.
Levar o usuário a algum lugar¶
openFileAt(path, position) é a única chamada por trás de "ir para o erro de
compilação", "saltar para o quadro da pilha" ou "abrir o resultado da busca",
então todos chegam do mesmo jeito. Ela abre o arquivo exatamente como
openFile() (a aba de um arquivo já aberto é ativada, não aberta uma segunda
vez), torna-o o documento ativo, posiciona o cursor sem seleção, expande uma
dobra que esconda a linha, rola a linha até ficar visível — centralizada
quando estava fora da tela, sem rolar quando já estava visível — e dá o foco
do teclado ao editor. A posição é limitada, nunca recusada: uma linha
além do fim cai na última linha, uma coluna além do fim da linha cai no fim
dela, valores negativos contam como 0. Ela devolve o documento em que o
cursor foi posicionado, ou 0 quando o arquivo não pôde ser aberto — e então
nada se moveu.
revealLine(doc, line) mostra um lugar sem levar o usuário até lá: expande e
rola o painel que mostra doc, mas não move o cursor nem a seleção, não troca
de aba e não toma o foco — um painel que pré-visualiza um quadro da pilha ou
acompanha a própria seleção o usa.
Codificação do documento¶
A codificação de caracteres em disco de um documento é estado do núcleo, não de
um plugin. O host a detecta quando o arquivo é lido e grava a mesma codificação de
volta a cada salvamento, então um arquivo UTF-8-BOM mantém a marca dele e um arquivo
UTF-16 continua UTF-16. DocumentEncoding (PluginApi/DocumentEncoding.h) é um
enum POD independente de backend cobrindo os cinco principais Unicode/ANSI mais as
páginas de código legadas.
setDocumentEncoding() registra uma escolha: ela não altera bytes nem redecodifica
nada — o buffer já é Unicode, e a nova codificação passa a valer no próximo
salvamento. Um plugin que ofereça uma ação de "converter para" deve, portanto,
chamar também setModified(doc, true); um que ofereça "interpretar como" recarrega
o texto por conta própria antes e então registra a escolha. O host publica
DocumentEncodingChanged sempre que o valor realmente muda. Um charset do
.editorconfig do projeto ainda vence na hora de salvar.
Fins de linha do documento¶
O terminador de linha de um documento também é estado do núcleo. O host o
detecta quando o arquivo é lido, o empurra para a visualização de modo que o
Enter insira aquela sequência, e grava os bytes do buffer de volta literalmente —
então um arquivo Unix editado no Windows continua LF e um salvamento sem edições é
idêntico byte a byte. LineEnding (PluginApi/LineEnding.h) é um enum POD
independente de backend — Unknown / Lf / CrLf / Cr — nomeando a sequência de
bytes em vez de uma constante do backend. Ele traz dois auxiliares em linha:
lineEndingBytes() e platformLineEnding() (com o que um buffer novo começa: CRLF
no Windows, LF no resto).
setLineEnding() registra a escolha, reajusta a visualização do documento para que
o próximo Enter a insira, e decide o que o próximo salvamento grava. Ela não
converte nenhum texto existente: um plugin que reescreva o buffer inteiro (os
comandos de Conversão de fim de linha) substitui o texto por conta própria antes,
para que a conversão continue sendo uma única edição reversível. O host publica
DocumentLineEndingChanged sempre que o valor realmente muda. Um end_of_line do
.editorconfig do projeto ainda vence na hora de salvar, e move o documento para
aquela convenção junto.
Threads
Todas as chamadas ao contexto e aos serviços acontecem na thread de interface do host. Não chame os serviços a partir de uma thread em segundo plano que você criou; volte antes para a thread certa.
Versionamento¶
O contrato tem uma versão em duas partes, ambas em PluginApi/PluginInfo.h:
constexpr std::uint32_t MTE_PLUGIN_API_VERSION = 1; // major
constexpr std::uint32_t MTE_PLUGIN_API_MINOR_VERSION = 0; // minor
O que decide entre elas é quem implementa a interface:
| Alteração | Tipo | Versão |
|---|---|---|
Um método acrescentado ao fim de uma interface que só o editor implementa e que os plugins só chamam (IEditorService, IPluginContext, todo I…Registry, ILineMarkerService, …) |
compatível | minor + 1 |
| Uma interface nova, uma estrutura de evento nova, um enumerador novo no fim de uma enumeração | compatível | minor + 1 |
Qualquer alteração em uma estrutura que atravessa a fronteira (PluginInfo, ContextMenuRequest, DockPanelDescriptor, uma estrutura de evento) |
incompatível | major + 1 |
| Uma assinatura alterada, um método removido ou reordenado, uma nova sobrecarga de um nome existente | incompatível | major + 1 |
Qualquer alteração em uma interface que um plugin implementa (IPlugin, IHoverProvider, IContextMenuProvider, IDockPanel, ISettingsPage, …) |
incompatível | major + 1 |
| Uma versão compilada com uma cadeia de ferramentas cuja interface binária difere da anterior — outra família de compiladores ou outra biblioteca padrão, uma nova versão principal do Qt | incompatível | major + 1 |
Um conjunto de ferramentas mais novo da mesma família de compiladores, outro nível do padrão C++ e uma versão menor mais nova do Qt não são uma ruptura desse tipo e não mudam nenhum dos dois números; veja Requisitos de compilação para saber com o que um plugin nativo precisa coincidir.
O editor só carrega um plugin quando
apiVersionnoplugin.jsoneIPlugin::info().apiVersionsão iguais à sua própria versão major, eapiMinorVersionnoplugin.json(opcional, padrão0) eIPlugin::info().apiMinorVersionnão passam da sua própria versão minor. O valor eminfo()tem como padrão a minor dos cabeçalhos com que o plugin foi compilado; um plugin que sabe que não usa nada mais novo pode informar um número menor noplugin.json, e ainda assim precisa passar na segunda verificação.
Um plugin recusado continua listado em Preferências ▸ Plug-ins com o
motivo ("Incompatível (a API não corresponde)", "Precisa de uma versão mais
recente do OctoEdit") e ganha uma entrada no log — nunca uma falha. Um plugin
Python não tem um info() compilado com os cabeçalhos, então o seu
plugin.json é a sua única declaração: informe a minor da chamada mais
recente que você usa.
Política anterior ao lançamento
Antes do primeiro lançamento público, o contrato ainda pode mudar sem elevar
nenhum dos dois números; até lá, ambos ficam em 1 / 0. A partir do
primeiro lançamento, toda alteração é avaliada pela tabela acima.