Automação de editor Ace
O Ace renderiza apenas as linhas na tela e recebe pressionamentos de tecla através de uma textarea oculta que fecha automaticamente aspas/chaves e auto-indenta novas linhas. Digitar {, Enter, "a": [1], Enter, } em um editor JSON Ace padrão produz:
{
"a": [1]
}
}
Portanto:
- Leia e escreva através da API do editor com JavaScript executado na página. Não extraia elementos
.ace_lineou capturas de tela para obter o conteúdo, e não digite código tecla por tecla. - Execute no mundo principal da página. Propriedades que a página define em elementos (como
el.env) são invisíveis de mundos isolados, ex. scripts de conteúdo de extensão de navegador. Use a opção de mundo principal da sua ferramenta, ou o fallback no passo 5. - Respeite editores somente leitura. A API ainda pode alterá-los, mas o aplicativo não espera por isso — faça apenas quando o usuário pedir.
- Alterar o editor não é salvar. Confirme com o usuário antes de clicar em Salvar / Enviar / Aplicar.
Os trechos abaixo são JavaScript puro da página cuja última expressão é o resultado. O passo 7 usa await; se sua ferramenta não tiver await de nível superior, envolva o trecho em uma função assíncrona (ex. page.evaluate(async () => { ... }) do Playwright).
1. Encontrar os editores
O renderizador do Ace adiciona a classe ace_editor a todo contêiner de editor, em todas as versões, então o seletor é estável. O que varia é onde os editores estão (documento principal, shadow roots abertos, iframes de mesma origem), quais elementos são editores reais (o popup de autocomplete também é um editor Ace) e se a instância é acessível. Pesquise dinamicamente:
(() => {
const hits = [];
const walk = (root, where) => {
for (const el of root.querySelectorAll('.ace_editor')) hits.push({ el, where });
for (const node of root.querySelectorAll('*')) {
if (node.shadowRoot) walk(node.shadowRoot, `${where} > ${node.localName}::shadow`);
}
for (const frame of root.querySelectorAll('iframe, frame')) {
let doc = null;
try { doc = frame.contentDocument; } catch {}
if (doc) walk(doc, `${where} > iframe${frame.id ? '#' + frame.id : ''}`);
else hits.push({ el: null, where: `${where} > cross-origin iframe ${frame.src}` });
}
};
walk(document, 'document');
// o popup de autocomplete também é um editor Ace: pule-o
const found = hits.filter(h => !h.el?.classList.contains('ace_autocomplete'));
window.__aceEditors = found.map(h => h.el);
return found.map(({ el, where }, i) => {
if (!el) return { i, where, note: 'não acessível desta página: abra a URL do frame diretamente' };
const ed = el.env?.editor;
return {
i,
where,
id: el.id || undefined,
label: el.closest('[aria-label]')?.getAttribute('aria-label') || undefined,
visible: el.getClientRects().length > 0,
handle: !!ed,
mode: ed?.session.getMode().$id,
readOnly: ed?.getReadOnly(),
lines: ed?.session.getLength(),
firstLine: (ed ? ed.session.getLine(0) : el.querySelector('.ace_line')?.textContent ?? '').slice(0, 60),
};
});
})()
- Escolha o alvo por
label,id,mode,firstLine,visible; use seuinos próximos passos (os exemplos usam0). - Execute novamente o localizador após navegação ou re-renderizações. Uma lista vazia em um aplicativo de página única geralmente significa que o editor ainda não foi montado: aguarde e tente novamente.
handle: false→ a página construiu o editor semace.edit()(semel.env) ou você está em um mundo isolado: use o passo 5. Nunca chameace.edit(el)para obter um handle — em um elemento semenv, ele cria um segundo editor sobre o editor da página.
2. Inspecionar e ler
const editor = window.__aceEditors[0].env.editor; // índice do localizador
({
mode: editor.session.getMode().$id, // ex. ace/mode/json (ace/mode/text enquanto um modo ainda está carregando)
readOnly: editor.getReadOnly(),
lines: editor.session.getLength(),
indent: JSON.stringify(editor.session.getTabString()),
cursor: editor.getCursorPosition(), // { row, column }, base 0
selection: editor.getSelectedText(),
text: editor.getValue(),
})
Para documentos muito grandes, leia fatias com editor.session.getLines(firstRow, lastRow) (base 0, inclusivo).
3. Substituir o documento inteiro
const editor = window.__aceEditors[0].env.editor;
const newText = '...'; // o novo conteúdo completo
editor.session.getUndoManager().startNewGroup?.(); // mantenha esta alteração como seu próprio passo de desfazer
editor.setValue(newText, -1); // -1 coloca o cursor no início; dispara 'change' para o aplicativo atualizar seu estado
editor.getValue() === newText
Gere conteúdo estruturado a partir de dados em vez de escrevê-lo manualmente: JSON.stringify(data, null, editor.session.getTabString()) corresponde à indentação do editor.
4. Edições direcionadas
const editor = window.__aceEditors[0].env.editor;
// primeira correspondência; opções: wrap, caseSensitive, wholeWord, regExp, backwards, range
if (editor.find('oldValue', { wrap: true, caseSensitive: true })) editor.replace('newValue');
// toda correspondência, retorna a contagem; com regExp, retroreferências $1 funcionam
const replaced = editor.replaceAll('version-$1', { needle: 'v(\\d+)', regExp: true });
// por posição: linhas e colunas são base 0, colunas além do final da linha são cortadas
editor.session.replace({ start: { row: 2, column: 0 }, end: { row: 2, column: Infinity } }, 'linha 3 inteira nova');
editor.session.insert({ row: editor.session.getLength(), column: 0 }, '\\nlinha anexada');
({ replaced, text: editor.getValue() })
Mais — cursor, seleção, dobramento, rolagem, comandos, eventos: referencias/ace-api.md.
5. Sem handle? Cole na textarea oculta
const el = window.__aceEditors[0]; // funciona com handle: false
const newText = '...';
const input = el.querySelector('textarea.ace_text-input');
const mac = /Mac|iPhone|iPad/.test(navigator.platform);
input.focus();
// selecionar tudo: Ace vincula Ctrl+A, ou Cmd+A em plataformas Apple
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA', keyCode: 65, which: 65, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }));
const data = new DataTransfer();
data.setData('text/plain', newText);
input.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }));
'colado'
- O texto colado é inserido literalmente: sem auto-pareamento, sem auto-indentação. Editores somente leitura o ignoram.
- Alternativa com a textarea focada e tudo selecionado:
document.execCommand('insertText', false, newText). - Sem JavaScript: coloque o texto na área de transferência do SO, clique dentro do editor, pressione
Ctrl+A(Cmd+Ano macOS), depoisCtrl+V(Cmd+V). - Sem um handle, você só pode ler linhas renderizadas (
.ace_text-layer .ace_line, após o próximo quadro). Para o texto completo, selecione tudo e copie via área de transferência do SO.
6. Se você realmente precisar digitar
const editor = window.__aceEditors[0].env.editor;
const assists = ['behavioursEnabled', 'wrapBehavioursEnabled', 'enableAutoIndent', 'enableLiveAutocompletion', 'enableBasicAutocompletion', 'enableSnippets'];
window.__aceSavedOptions = Object.fromEntries(assists.filter(name => editor.$options[name]).map(name => [name, editor.getOption(name)]));
editor.setOptions(Object.fromEntries(Object.keys(window.__aceSavedOptions).map(name => [name, false])));
editor.focus();
window.__aceSavedOptions // restaurar após digitar: editor.setOptions(window.__aceSavedOptions)
Digite, depois restaure as opções. Prefira conteúdo de linha única e pressione Escape antes de começar. Blocos de código multilinha: grave a saída esperada do editor, não a entrada com auto-indentação.
7. Erros de sintaxe
const editor = window.__aceEditors[0].env.editor;
editor.getSession().setAnnotations([
{ row: 0, column: 0, text: 'Exemplo de aviso', type: 'warning' },
]);
(await editor.session.getAnnotations())