Aceエディタの自動操作
Aceは画面上の行のみをレンダリングし、隠し textarea を介してキー入力を受け取ります。このtextareaは引用符/括弧を自動的に閉じ、新しい行を自動インデントします。デフォルトのAce JSONエディタで {、Enter、"a": [1]、Enter、} と入力すると、以下のようになります:
{
"a": [1]
}
}
そのため:
- エディタAPIを介して読み書きする には、ページ内でJavaScriptを実行します。
.ace_line要素やスクリーンショットからコンテンツをスクレイピングしたり、コードをキー入力で1つずつ入力したりしないでください。 - ページのメインワールドで実行します。 ページが要素に設定したプロパティ(例:
el.env)は、ブラウザ拡張機能のコンテンツスクリプトなどの分離されたワールドからは見えません。ツールのメインワールドオプション、またはステップ5のフォールバックを使用してください。 - 読み取り専用エディタを尊重します。 APIは引き続き変更できますが、アプリはそれを想定していません。ユーザーが要求した場合にのみ行ってください。
- エディタを変更しても保存されません。 [保存] / [送信] / [適用] をクリックする前にユーザーに確認してください。
以下のスニペットはプレーンなページのJavaScriptで、最後の式が結果となります。ステップ7では await を使用します。ツールにトップレベルの await がない場合は、スニペットを非同期関数でラップしてください(例: Playwrightの page.evaluate(async () => { ... }))。
1. エディタを見つける
Aceのレンダラーは、すべてのバージョンですべてのエディタコンテナに ace_editor クラスを追加するため、セレクタは安定しています。異なるのは、エディタがどこにあるか(メインドキュメント、開いているシャドウルート、同一オリジンの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: 'このページからはアクセスできません: フレームのURLを直接開いてください' };
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つ目のエディタを作成してしまいます。
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 } }, '新しい行3全体');
editor.session.insert({ row: editor.session.getLength(), column: 0 }, '\n追加された行');
({ replaced, text: editor.getValue() })
その他 — カーソル、選択、折りたたみ、スクロール、コマンド、イベント: references/ace-api.md を参照してください。
5. ハンドルがない場合? 隠しテキストエリアにペースト
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'
- ペーストされたテキストはそのまま挿入されます: 自動ペアリングも自動インデントも行われません。読み取り専用エディタは無視します。
- テキストエリアにフォーカスがありすべて選択されている場合の代替:
document.execCommand('insertText', false, newText)。 - JavaScriptを使用しない場合: テキストをOSのクリップボードに置き、エディタ内をクリックし、
Ctrl+A(macOSではCmd+A)、次にCtrl+V(Cmd+V)を押します。 - ハンドルがない場合、レンダリングされた行(
.ace_text-layer .ace_line、次のフレーム後)のみを読み取ることができます。全文を取得するには、すべて選択してOSのクリップボードを介してコピーします。
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)
タイピングした後、オプションを復元します。1行のコンテンツを優先し、タイピングの前に Escape を押します。