Pular para conteúdo

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 mesmo id e apiVersion do plugin.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; devolva false apenas 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ça reset() 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ão TokenId nã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

  • apiVersion no plugin.json e IPlugin::info().apiVersion são iguais à sua própria versão major, e
  • apiMinorVersion no plugin.json (opcional, padrão 0) e IPlugin::info().apiMinorVersion não passam da sua própria versão minor. O valor em info() 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 no plugin.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.