Ace 编辑器自动化
Ace 仅渲染屏幕上的行,并通过一个隐藏的 textarea 接收按键,该 textarea 会自动闭合引号/括号并对新行进行自动缩进。在默认的 Ace JSON 编辑器中依次键入 {、Enter、 "a": [1]、Enter、} 会产生如下结果:
{
"a": [1]
}
}
因此:
- 通过运行在页面中的 JavaScript 编辑器 API 进行读写。 不要抓取
.ace_line元素或截图来获取内容,也不要逐键输入代码。 - 在页面的主世界中运行。 页面在元素上设置的属性(例如
el.env)在隔离世界(例如浏览器扩展的内容脚本)中不可见。使用您工具的主世界选项,或使用第 5 步中的回退方案。 - 尊重只读编辑器。 API 仍然可以更改它们,但应用程序并不期望如此——仅在用户要求时才进行更改。
- 更改编辑器不等于保存。 在点击“保存”/“提交”/“应用”之前,请先与用户确认。
以下是纯页面 JavaScript 代码片段,其最后一个表达式即为结果。第 7 步使用了 await;如果您的工具没有顶层 await,请将代码片段包装在异步函数中(例如 Playwright 的 page.evaluate(async () => { ... }))。
1. 查找编辑器
Ace 的渲染器为每个编辑器容器添加了 ace_editor 类,在所有版本中皆如此,因此选择器是稳定的。变化在于编辑器所在的位置(主文档、开放的 Shadow DOM、同源 iframe)、哪些元素是真正的编辑器(自动完成弹出框也是一个 Ace 编辑器),以及实例是否可达。动态搜索:
(() => {
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');
// 自动完成弹出框也是一个 Ace 编辑器:跳过它
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: 'not reachable from this page: open the frame URL directly' };
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),
};
});
})();
- 根据
label、id、mode、firstLine、visible选择目标编辑器;在后续步骤中使用其i(示例中使用0)。 - 在导航或重渲染后重新运行查找器。在单页应用中,空列表通常意味着编辑器尚未挂载:请等待并重试。
handle: false→ 页面构建编辑器时未使用ace.edit()(没有el.env),或者您处于隔离世界:请使用第 5 步。切勿在元素上调用ace.edit(el)来获取句柄——在没有env的元素上,它会在页面编辑器之上创建一个第二个编辑器。
2. 检查并读取
const editor = window.__aceEditors[0].env.editor; // 索引来自查找器
({
mode: editor.session.getMode().$id, // 例如 ace/mode/json(模式仍在加载时为 ace/mode/text)
readOnly: editor.getReadOnly(),
lines: editor.session.getLength(),
indent: JSON.stringify(editor.session.getTabString()),
cursor: editor.getCursorPosition(), // { row, column },从 0 开始
selection: editor.getSelectedText(),
text: editor.getValue(),
})
对于非常大的文档,使用 editor.session.getLines(firstRow, lastRow) 读取切片(从 0 开始,包含两端)。
3. 替换整个文档
const editor = window.__aceEditors[0].env.editor;
const newText = '...'; // 完整的新内容
editor.session.getUndoManager().startNewGroup?.(); // 将此更改保留为独立的撤销步骤
editor.setValue(newText, -1); // -1 将光标置于开头;触发 'change' 事件以便应用更新其状态
editor.getValue() === newText
从数据生成结构化内容,而不是手动编写:JSON.stringify(data, null, editor.session.getTabString()) 可匹配编辑器的缩进。
4. 针对性编辑
const editor = window.__aceEditors[0].env.editor;
// 第一个匹配项;选项: wrap, caseSensitive, wholeWord, regExp, backwards, range
if (editor.find('oldValue', { wrap: true, caseSensitive: true })) editor.replace('newValue');
// 每个匹配项,返回计数;使用 regExp 时,$1 反向引用生效
const replaced = editor.replaceAll('version-$1', { needle: 'v(\\d+)', regExp: true });
// 按位置:行和列从 0 开始,超出行末的列会被裁剪
editor.session.replace({ start: { row: 2, column: 0 }, end: { row: 2, column: Infinity } }, 'whole new line 3');
editor.session.insert({ row: editor.session.getLength(), column: 0 }, '\\nappended line');
({ replaced, text: editor.getValue() })
更多内容——光标、选中、折叠、滚动、命令、事件:请参见 references/ace-api.md。
5. 无句柄?粘贴到隐藏的 textarea 中
const el = window.__aceEditors[0]; // 即使 handle: false 也能工作
const newText = '...';
const input = el.querySelector('textarea.ace_text-input');
const mac = /Mac|iPhone|iPad/.test(navigator.platform);
input.focus();
// 全选:Ace 绑定 Ctrl+A,或 Apple 平台上的 Cmd+A
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 }));
'pasted'
- 粘贴的文本是逐字插入的:无自动配对,无自动缩进。只读编辑器会忽略它。
- 替代方案:聚焦 textarea 并全选后,使用
document.execCommand('insertText', false, newText)。 - 不使用 JavaScript:将文本放入系统剪贴板,在编辑器内点击,按下
Ctrl+A(macOS 上为Cmd+A),然后Ctrl+V(macOS 上为Cmd+V)。 - 没有句柄时,您只能读取渲染出的行(
.ace_text-layer .ace_line,需在下一帧之后)。要获取全文,请全选并通过系统剪贴板复制。
6. 如果确实需要手动输入
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 // 输入后恢复选项:editor.setOptions(window.__aceSavedOptions)
输入,然后恢复选项。首选单行内容,并在按 Enter 键前按下 Escape。