Automatización del editor Ace
Ace solo renderiza las filas visibles en pantalla y recibe las pulsaciones de teclado a través de un textarea oculto que cierra automáticamente comillas/corchetes y aplica sangría automática en nuevas líneas. Escribir {, Enter, "a": [1], Enter, } en un editor JSON predeterminado produce:
{
"a": [1]
}
}
Por lo tanto:
- Lee y escribe a través de la API del editor con JavaScript ejecutado en la página. No extraigas contenido de elementos
.ace_lineni de capturas de pantalla, y no escribas código tecla por tecla. - Ejecuta en el ámbito principal de la página. Las propiedades que la página establece en los elementos (como
el.env) son invisibles desde ámbitos aislados, por ejemplo, scripts de contenido de extensiones de navegador. Usa la opción de ámbito principal de tu herramienta, o el método alternativo en el paso 5. - Respeta los editores de solo lectura. La API aún puede modificarlos, pero la aplicación no lo espera; solo hazlo cuando el usuario lo solicite.
- Cambiar el editor no equivale a guardar. Confirma con el usuario antes de hacer clic en Guardar / Enviar / Aplicar.
Los fragmentos a continuación son JavaScript plano de página cuya última expresión es el resultado. El paso 7 usa await; si tu herramienta no tiene await de nivel superior, envuelve el fragmento en una función async (por ejemplo, page.evaluate(async () => { ... }) en Playwright).
1. Encontrar los editores
El renderizador de Ace añade la clase ace_editor a cada contenedor de editor, en todas las versiones, por lo que el selector es estable. Lo que varía es dónde viven los editores (documento principal, shadow roots abiertos, iframes del mismo origen), qué elementos son editores reales (el popup de autocompletado también es un editor Ace) y si la instancia es accesible. Busca dinámicamente:
(() => {
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} > iframe de origen cruzado ${frame.src}` });
}
};
walk(document, 'document');
// el popup de autocompletado también es un editor Ace: saltarlo
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, nota: 'no accesible desde esta página: abre la URL del frame directamente' };
const ed = el.env?.editor;
return {
i,
where,
id: el.id || undefined,
etiqueta: el.closest('[aria-label]')?.getAttribute('aria-label') || undefined,
visible: el.getClientRects().length > 0,
handle: !!ed,
modo: ed?.session.getMode().$id,
soloLectura: ed?.getReadOnly(),
lineas: ed?.session.getLength(),
primeraLinea: (ed ? ed.session.getLine(0) : el.querySelector('.ace_line')?.textContent ?? '').slice(0, 60),
};
});
})()
- Selecciona el objetivo por
etiqueta,id,modo,primeraLinea,visible; usa suien los siguientes pasos (los ejemplos usan0). - Vuelve a ejecutar el buscador después de navegación o re-renderizados. Una lista vacía en una aplicación de una sola página generalmente significa que el editor aún no se ha montado: espera y reintenta.
handle: false→ la página construyó el editor sinace.edit()(sinel.env) o estás en un ámbito aislado: usa el paso 5. Nunca llames aace.edit(el)para obtener un handle — en un elemento sinenvcrea un segundo editor encima del editor de la página.
2. Inspeccionar y leer
const editor = window.__aceEditors[0].env.editor; // índice del buscador
({
modo: editor.session.getMode().$id, // por ejemplo, ace/mode/json (ace/mode/text mientras se carga el modo)
soloLectura: editor.getReadOnly(),
lineas: editor.session.getLength(),
indentacion: JSON.stringify(editor.session.getTabString()),
cursor: editor.getCursorPosition(), // { row, column }, basado en 0
seleccion: editor.getSelectedText(),
texto: editor.getValue(),
})
Para documentos muy grandes, lee fragmentos con editor.session.getLines(primeraFila, ultimaFila) (basado en 0, inclusivo).
3. Reemplazar todo el documento
const editor = window.__aceEditors[0].env.editor;
const nuevoTexto = '...'; // el contenido nuevo completo
editor.session.getUndoManager().startNewGroup?.(); // mantiene este cambio como un paso de deshacer propio
editor.setValue(nuevoTexto, -1); // -1 coloca el cursor al inicio; dispara 'change' para que la aplicación actualice su estado
editor.getValue() === nuevoTexto
Genera contenido estructurado a partir de datos en lugar de escribirlo manualmente: JSON.stringify(datos, null, editor.session.getTabString()) coincide con la indentación del editor.
4. Ediciones dirigidas
const editor = window.__aceEditors[0].env.editor;
// primera coincidencia; opciones: wrap, caseSensitive, wholeWord, regExp, backwards, range
if (editor.find('valorAntiguo', { wrap: true, caseSensitive: true })) editor.replace('nuevoValor');
// cada coincidencia, devuelve el recuento; con regExp, las referencias $1 funcionan
const reemplazadas = editor.replaceAll('version-$1', { needle: 'v(\\d+)', regExp: true });
// por posición: filas y columnas están basadas en 0, las columnas después del final de la línea se recortan
editor.session.replace({ start: { row: 2, column: 0 }, end: { row: 2, column: Infinity } }, 'línea 3 completamente nueva');
editor.session.insert({ row: editor.session.getLength(), column: 0 }, '\nlínea añadida');
({ reemplazadas, texto: editor.getValue() })
Más — cursor, selección, plegado, desplazamiento, comandos, eventos: references/ace-api.md.
5. ¿Sin handle? Pega en el textarea oculto
const el = window.__aceEditors[0]; // funciona con handle: false
const nuevoTexto = '...';
const input = el.querySelector('textarea.ace_text-input');
const mac = /Mac|iPhone|iPad/.test(navigator.platform);
input.focus();
// seleccionar todo: Ace asocia Ctrl+A, o Cmd+A en 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', nuevoTexto);
input.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }));
'pegado'
- El texto pegado se inserta tal cual: sin emparejamiento automático ni sangría automática. Los editores de solo lectura lo ignoran.
- Alternativa con el textarea enfocado y todo seleccionado:
document.execCommand('insertText', false, nuevoTexto). - Sin JavaScript: coloca el texto en el portapapeles del SO, haz clic dentro del editor, presiona
Ctrl+A(Cmd+Aen macOS) y luegoCtrl+V(Cmd+V). - Sin un handle solo puedes leer las filas renderizadas (
.ace_text-layer .ace_line, después del siguiente frame). Para el texto completo, selecciona todo y copia a través del portapapeles del SO.
6. Si realmente debes escribir
const editor = window.__aceEditors[0].env.editor;
const asistencias = ['behavioursEnabled', 'wrapBehavioursEnabled', 'enableAutoIndent', 'enableLiveAutocompletion', 'enableBasicAutocompletion', 'enableSnippets'];
window.__aceOpcionesGuardadas = Object.fromEntries(asistencias.filter(nombre => editor.$options[nombre]).map(nombre => [nombre, editor.getOption(nombre)]));
editor.setOptions(Object.fromEntries(Object.keys(window.__aceOpcionesGuardadas).map(nombre => [nombre, false])));
editor.focus();
window.__aceOpcionesGuardadas // restaurar después de escribir: editor.setOptions(window.__aceOpcionesGuardadas)
Escribe y luego restaura las opciones. Prefiere contenido de una sola línea y presiona Escape antes de continuar para evitar activar el autocompletado.