Ace 編輯器自動化
Ace 僅會渲染螢幕上可見的行數,並透過一個隱藏的 textarea 接收按鍵輸入,該 textarea 會自動閉合引號/括號,並自動縮排新行。在預設的 Ace JSON 編輯器中輸入 {、Enter、 "a": [1]、Enter、} 會產生:
{
"a": [1]
}
}
因此:
- 透過 Editor API 進行讀寫:使用在頁面中執行的 JavaScript。不要從
.ace_line元素或螢幕截圖中抓取內容,也不要逐個按鍵輸入程式碼。 - 在頁面的主世界中執行。頁面在元素上設定的屬性(例如
el.env)在隔離的世界(例如瀏覽器擴充功能的內容腳本)中是看不見的。使用您工具的 main-world 選項,或使用步驟 5 中的備用方案。 - 尊重唯讀編輯器。API 仍然可以更改它們,但應用程式不預期如此 — 僅在使用者要求時才這樣做。
- 變更編輯器不等於儲存。在點擊儲存 / 提交 / 套用之前,請與使用者確認。
以下程式碼片段是純頁面 JavaScript,其最後一個表達式即為結果。步驟 7 使用了 await;如果您的工具沒有頂層 await,請將該片段包裹在一個非同步函數中(例如 Playwright 的 page.evaluate(async () => { ... }))。
1. 尋找編輯器
Ace 的渲染器會在任何版本的每個編輯器容器上加入 ace_editor 類別,因此這個選取器是穩定的。會變化的是編輯器的位置(主文件、開啟的 Shadow Root、同源的 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)
輸入,然後恢復選項。偏好單行內容,並在繼續自動化之前按下 Escape 鍵以隱藏自動完成彈出視窗。