Community程式設計與開發github.com

omrehbein/ace-editor

用於當自動化瀏覽網頁的代理需要尋找、讀取、取代或驗證 Ace 程式碼編輯器(類別為 `ace_editor` 的元素)中的文字時。適用於內嵌在雲端主控台(如 AWS 政策編輯器)、Cloudscape/awsui、Cloud9 風格的 IDE、react-ace、管理後台中的程式碼/JSON/YAML/SQL 編輯器。也適用於當輸入網頁程式碼編輯器的文字變得混亂(多出或缺少引號/括號、縮排加倍)、當僅有部分文件在 DOM 或螢幕截圖中可見時,或當需要編輯器的語法錯誤時。

ace-editor 是什麼?

此技能專門用於自動化網頁上的 Ace 程式碼編輯器。Ace 編輯器僅渲染螢幕上的行數,並透過一個隱藏的 `textarea` 接收鍵盤輸入,該 textarea 會自動閉合引號/括號並自動縮排新行。直接逐字輸入程式碼常會導致格式錯誤(例如多餘的引號或括號、縮排加倍)。為了解決這個問題,此技能提供了一套完整的 JavaScript 腳本,用於直接透過編輯器的 API 進行操作,從而繞過鍵盤輸入的問題,確保文本的完整性。該技能支援所有主要平台,包括 AWS 主控台、Cloud9、react-ace 等,並能處理跨來源的 iframe 和 Shadow DOM。主要功能包括尋找編輯器、檢查編輯器屬性(如模式、唯讀狀態)、讀取完整或部分內容、取代整個文件、執行目標性編輯、在不使用 API 時透過貼上內容到隱藏的 textarea 中進行操作,以及在必要時暫時停用自動格式化功能來模擬鍵盤輸入。此技能也特別強調尊重唯讀編輯器,並提醒使用者變更編輯器內容不等於儲存。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/omrehbein/agent-skills/tree/main/skills/ace-editor

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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 鍵以隱藏自動完成彈出視窗。

相關技能