📜
editor.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// ===== Editor Core ===== const editor = ace.edit("editor"); // Disable workers to avoid security errors in sandboxed environments editor.session.setUseWorker(false); editor.session.setMode("ace/mode/html"); editor.setTheme("ace/theme/monokai"); editor.setOptions({ tabSize: 2, useSoftTabs: true, showPrintMargin: false, wrap: false, enableBasicAutocompletion: true, enableLiveAutocompletion: true, enableSnippets: true, fontSize: "14px", }); // Demo content const demo = `<!doctype html> <html> <head> <meta charset="utf-8"> <title>Hello</title> <style> body { font-family: system-ui; margin: 2rem } .hi { font-size: 1.5rem } </style> </head> <body> <h1>Hello, world! 👋</h1> <p class="hi">This was inserted as an example HTML+JS snippet.</p> <button id="btn">Click me</button> <script> document.getElementById('btn').addEventListener('click', () => { alert('Hello from JavaScript!'); console.log('Button clicked at', new Date().toISOString()); }); <\/script> </body> </html>`; editor.setValue(demo, -1); // ===== Helpers ===== const $ = (id) => document.getElementById(id); // ===== Save Dropdown (Placeholders) ===== const saveBtn = $("saveBtn"); const saveMenu = $("saveMenu"); saveBtn.addEventListener("click", (e) => { const open = saveMenu.classList.toggle("open"); saveBtn.setAttribute("aria-expanded", String(open)); e.stopPropagation(); }); document.addEventListener("click", (e) => { if (!saveMenu.contains(e.target) && !saveBtn.contains(e.target)) { saveMenu.classList.remove("open"); saveBtn.setAttribute("aria-expanded", "false"); } }); // Placeholder save actions $("doSave").addEventListener("click", () => { // TODO: Implement actual save functionality console.log("Save clicked"); saveMenu.classList.remove("open"); }); $("doArtifact").addEventListener("click", () => { // TODO: Implement artifact save functionality console.log("Save Artifact clicked"); saveMenu.classList.remove("open"); }); // ===== Settings Panel & Tabs ===== const panel = $("panel"); const settingsBtn = $("settingsBtn"); settingsBtn.addEventListener("click", (e) => { panel.classList.toggle("open"); e.stopPropagation(); }); document.addEventListener("click", (e) => { if (!panel.contains(e.target) && !settingsBtn.contains(e.target)) { panel.classList.remove("open"); } }); // Tab management const tabEditor = $("tab-editor"); const tabAI = $("tab-ai"); const panelEditor = $("panel-editor"); const panelAI = $("panel-ai"); function selectTab(which) { const isEditor = which === "editor"; tabEditor.setAttribute("aria-selected", String(isEditor)); tabAI.setAttribute("aria-selected", String(!isEditor)); panelEditor.classList.toggle("active", isEditor); panelAI.classList.toggle("active", !isEditor); } tabEditor.addEventListener("click", () => selectTab("editor")); tabAI.addEventListener("click", () => selectTab("ai")); // ===== Settings & Preferences ===== const themeSel = $("theme"); const wrapChk = $("wrap"); const fontRange = $("fontsize"); const modeSel = $("mode"); const aiProvider = $("ai-provider"); const aiModel = $("ai-model"); const aiTemp = $("ai-temp"); const aiKey = $("ai-key"); // Safe localStorage access with fallback function getPrefs() { try { return JSON.parse(localStorage?.getItem?.("ace_min_prefs") || "{}"); } catch (e) { console.warn("localStorage not available, using defaults"); return {}; } } function savePrefs() { try { localStorage?.setItem?.("ace_min_prefs", JSON.stringify({ theme: editor.getTheme(), wrap: editor.session.getUseWrapMode(), fontSize: editor.getFontSize(), mode: editor.session.getMode().$id, ai: { provider: aiProvider.value, model: aiModel.value, temperature: parseFloat(aiTemp.value), key: aiKey.value } })); } catch (e) { console.warn("Could not save preferences:", e.message); } } // Load saved preferences const prefs = getPrefs(); if (prefs.theme) { editor.setTheme(prefs.theme); themeSel.value = prefs.theme; } if (prefs.wrap != null) { editor.session.setUseWrapMode(!!prefs.wrap); wrapChk.checked = !!prefs.wrap; } if (prefs.fontSize) { editor.setFontSize(prefs.fontSize); fontRange.value = parseInt(prefs.fontSize); } if (prefs.mode) { editor.session.setMode(prefs.mode); modeSel.value = prefs.mode; } if (prefs.ai) { aiProvider.value = prefs.ai.provider ?? "none"; aiModel.value = prefs.ai.model ?? ""; aiTemp.value = prefs.ai.temperature ?? 0.4; aiKey.value = prefs.ai.key ?? ""; } // Settings event listeners themeSel.addEventListener("change", (e) => { editor.setTheme(e.target.value); savePrefs(); }); wrapChk.addEventListener("change", (e) => { editor.session.setUseWrapMode(e.target.checked); savePrefs(); }); fontRange.addEventListener("input", (e) => { editor.setFontSize(e.target.value + "px"); }); fontRange.addEventListener("change", savePrefs); modeSel.addEventListener("change", (e) => { editor.session.setMode(e.target.value); savePrefs(); }); // AI settings aiProvider.addEventListener("change", savePrefs); aiModel.addEventListener("change", savePrefs); aiTemp.addEventListener("input", savePrefs); aiKey.addEventListener("change", savePrefs); // ===== Search & Navigation Functionality ===== const searchInput = $("searchInput"); const searchNext = $("searchNext"); const searchPrev = $("searchPrev"); const searchType = $("searchType"); const selectMode = $("selectMode"); // Track current navigation position let currentIndex = -1; let currentItems = []; // Get the complete range for a code block using folding function getBlockRange(row, type) { const session = editor.getSession(); const doc = session.getDocument(); const lines = doc.getAllLines(); // Try to use Ace's fold widget first const foldRange = session.getFoldWidgetRange(row); if (foldRange) { return { start: { row: row, column: 0 }, end: { row: foldRange.end.row, column: lines[foldRange.end.row].length } }; } // Fallback: smart block detection based on type switch (type) { case 'function': return getFunctionRange(row, lines); case 'variable': return getVariableRange(row, lines); case 'tag': return getTagRange(row, lines); case 'css': return getCSSRange(row, lines); default: return getSingleLineRange(row, lines); } } // Get function range (from declaration to closing brace) function getFunctionRange(startRow, lines) { const startLine = lines[startRow]; const startIndent = startLine.match(/^(\s*)/)[1].length; // Find opening brace let braceRow = startRow; let braceFound = false; for (let i = startRow; i < lines.length && i < startRow + 5; i++) { if (lines[i].includes('{')) { braceRow = i; braceFound = true; break; } } if (!braceFound) { return getSingleLineRange(startRow, lines); } // Find matching closing brace let braceCount = 0; let endRow = braceRow; for (let i = braceRow; i < lines.length; i++) { const line = lines[i]; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; if (braceCount === 0 && i > braceRow) { endRow = i; break; } } return { start: { row: startRow, column: 0 }, end: { row: endRow, column: lines[endRow].length } }; } // Get variable range (including initialization) function getVariableRange(startRow, lines) { const startLine = lines[startRow]; let endRow = startRow; // If it's an object/array initialization, find the end if (startLine.includes('{') || startLine.includes('[')) { let braceCount = 0; let bracketCount = 0; for (let i = startRow; i < lines.length; i++) { const line = lines[i]; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; bracketCount += (line.match(/\[/g) || []).length; bracketCount -= (line.match(/]/g) || []).length; if (braceCount === 0 && bracketCount === 0 && (line.includes(';') || line.includes(',') || i === startRow)) { endRow = i; break; } } } return { start: { row: startRow, column: 0 }, end: { row: endRow, column: lines[endRow].length } }; } // Get HTML tag range (opening to closing tag) function getTagRange(startRow, lines) { const startLine = lines[startRow].trim(); // Self-closing tag if (startLine.endsWith('/>') || startLine.includes('/>')) { return getSingleLineRange(startRow, lines); } // Extract tag name const tagMatch = startLine.match(/<(\w+)/); if (!tagMatch) return getSingleLineRange(startRow, lines); const tagName = tagMatch[1]; const closingTag = `</${tagName}>`; // Find closing tag for (let i = startRow + 1; i < lines.length; i++) { if (lines[i].includes(closingTag)) { return { start: { row: startRow, column: 0 }, end: { row: i, column: lines[i].length } }; } } return getSingleLineRange(startRow, lines); } // Get CSS rule range (selector to closing brace) function getCSSRange(startRow, lines) { let endRow = startRow; let braceCount = 0; let foundOpenBrace = false; for (let i = startRow; i < lines.length; i++) { const line = lines[i]; braceCount += (line.match(/{/g) || []).length; braceCount -= (line.match(/}/g) || []).length; if (line.includes('{')) foundOpenBrace = true; if (foundOpenBrace && braceCount === 0) { endRow = i; break; } } return { start: { row: startRow, column: 0 }, end: { row: endRow, column: lines[endRow].length } }; } // Single line range fallback function getSingleLineRange(row, lines) { return { start: { row: row, column: 0 }, end: { row: row, column: lines[row].length } }; } // Select a range in the editor function selectRange(range) { const Range = ace.require("ace/range").Range; const aceRange = new Range(range.start.row, range.start.column, range.end.row, range.end.column); editor.selection.setSelectionRange(aceRange); } // Get specific code elements based on search type function getCodeElements(type) { const session = editor.getSession(); const doc = session.getDocument(); const lines = doc.getAllLines(); const items = []; switch (type) { case "functions": lines.forEach((line, row) => { const trimmed = line.trim(); // Match various function patterns if (trimmed.match(/^(function\s+\w+|const\s+\w+\s*=\s*(?:function|\()|let\s+\w+\s*=\s*(?:function|\()|var\s+\w+\s*=\s*(?:function|\()|.*function\s*\(|\w+\s*:\s*function|\w+\s*\([^)]*\)\s*{)/)) { const name = extractFunctionName(trimmed); items.push({ row: row, text: trimmed, name: name, type: 'function' }); } }); break; case "variables": lines.forEach((line, row) => { const trimmed = line.trim(); // Match variable declarations const varMatch = trimmed.match(/^(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/); if (varMatch && !trimmed.includes('function')) { items.push({ row: row, text: trimmed, name: varMatch[2], type: 'variable' }); } }); break; case "divs": lines.forEach((line, row) => { const trimmed = line.trim(); // Match HTML tags and CSS selectors const tagMatch = trimmed.match(/<(\w+)(?:\s+[^>]*)?(?:\s+id=["']([^"']+)["'])?(?:\s+class=["']([^"']+)["'])?[^>]*>/); const cssMatch = trimmed.match(/^([.#]?[\w-]+)\s*[{,]/); if (tagMatch) { const tag = tagMatch[1]; const id = tagMatch[2]; const className = tagMatch[3]; let displayName = `<${tag}>`; if (id) displayName += `#${id}`; if (className) displayName += `.${className.split(' ')[0]}`; items.push({ row: row, text: trimmed, name: displayName, type: 'tag' }); } else if (cssMatch) { items.push({ row: row, text: trimmed, name: cssMatch[1], type: 'css' }); } }); break; case "normal": return getTopLevelFolds(); } return items; } // Extract function name from line function extractFunctionName(line) { // Try different function patterns const patterns = [ /function\s+(\w+)/, /(const|let|var)\s+(\w+)\s*=/, /(\w+)\s*:\s*function/, /(\w+)\s*\(/ ]; for (const pattern of patterns) { const match = line.match(pattern); if (match) { return match[2] || match[1]; } } return 'anonymous'; } // Get top-level foldable ranges (for normal mode) function getTopLevelFolds() { const session = editor.getSession(); const foldWidgets = session.foldWidgets; const folds = []; if (!foldWidgets) return folds; const doc = session.getDocument(); const lines = doc.getAllLines(); for (let row = 0; row < lines.length; row++) { const foldWidget = foldWidgets[row]; if (foldWidget === "start") { const line = lines[row]; const indent = line.match(/^(\s*)/)[1].length; if (indent <= 4) { const range = session.getFoldWidgetRange(row); if (range) { folds.push({ row: row, range: range, text: line.trim(), indent: indent, type: 'fold' }); } } } } return folds; } // Navigate through filtered items function navigateItems(backwards = false) { const type = searchType.value; currentItems = getCodeElements(type); if (currentItems.length === 0) { searchInput.placeholder = `No ${type} found`; return; } if (backwards) { currentIndex = currentIndex <= 0 ? currentItems.length - 1 : currentIndex - 1; } else { currentIndex = currentIndex >= currentItems.length - 1 ? 0 : currentIndex + 1; } const item = currentItems[currentIndex]; if (selectMode.checked) { // Select the entire block const blockRange = getBlockRange(item.row, item.type); selectRange(blockRange); editor.scrollToRow(item.row); } else { // Just navigate to the line editor.gotoLine(item.row + 1, 0, true); editor.scrollToRow(item.row); highlightLine(item.row); } // Update placeholder const displayText = item.name || item.text.slice(0, 30); const action = selectMode.checked ? "Select" : "Navigate"; searchInput.placeholder = `${action} ${type} (${currentIndex + 1}/${currentItems.length}): ${displayText}${displayText.length > 30 ? '...' : ''}`; } // Highlight a line briefly (only when not selecting) function highlightLine(row) { const Range = ace.require("ace/range").Range; const range = new Range(row, 0, row, 1); const marker = editor.session.addMarker(range, "ace_active-line", "fullLine"); setTimeout(() => { editor.session.removeMarker(marker); }, 1500); } // Main search/navigate function function doFind(backwards = false) { const query = searchInput.value.trim(); if (!query) { // Empty search: navigate through filtered items navigateItems(backwards); editor.focus(); return; } // Reset navigation when actually searching currentIndex = -1; const type = searchType.value; if (type === "normal") { // Regular search searchInput.placeholder = "Search…"; editor.find(query, { backwards, wrap: true, caseSensitive: false, wholeWord: false, regExp: false, preventScroll: false }); } else { // Filtered search within specific elements currentItems = getCodeElements(type); const filteredItems = currentItems.filter(item => item.text.toLowerCase().includes(query.toLowerCase()) || (item.name && item.name.toLowerCase().includes(query.toLowerCase())) ); if (filteredItems.length === 0) { searchInput.placeholder = `No ${type} matching "${query}"`; return; } // Find current position in filtered results const currentRow = editor.getCursorPosition().row; let startIndex = 0; if (backwards) { startIndex = filteredItems.findLastIndex(item => item.row < currentRow); if (startIndex < 0) startIndex = filteredItems.length - 1; } else { startIndex = filteredItems.findIndex(item => item.row > currentRow); if (startIndex < 0) startIndex = 0; } const item = filteredItems[startIndex]; if (selectMode.checked) { // Select the found block const blockRange = getBlockRange(item.row, item.type); selectRange(blockRange); editor.scrollToRow(item.row); } else { // Just navigate editor.gotoLine(item.row + 1, 0, true); highlightLine(item.row); } const displayText = item.name || item.text.slice(0, 30); const action = selectMode.checked ? "Selected" : "Found"; searchInput.placeholder = `${action} ${type}: ${displayText}`; } editor.focus(); } currentItems = getCodeElements(type); const filteredItems = currentItems.filter(item => item.text.toLowerCase().includes(query.toLowerCase()) || (item.name && item.name.toLowerCase().includes(query.toLowerCase())) ); if (filteredItems.length === 0) { searchInput.placeholder = `No ${type} matching "${query}"`; return; } // Find current position in filtered results const currentRow = editor.getCursorPosition().row; let startIndex = 0; if (backwards) { startIndex = filteredItems.findLastIndex(item => item.row < currentRow); if (startIndex < 0) startIndex = filteredItems.length - 1; } else { startIndex = filteredItems.findIndex(item => item.row > currentRow); if (startIndex < 0) startIndex = 0; } const item = filteredItems[startIndex]; if (selectMode.checked) { // Select the found block const blockRange = getBlockRange(item.row, item.type); selectRange(blockRange); editor.scrollToRow(item.row); } else { // Just navigate editor.gotoLine(item.row + 1, 0, true); highlightLine(item.row); } const displayText = item.name || item.text.slice(0, 30); const action = selectMode.checked ? "Selected" : "Found"; searchInput.placeholder = `${action} ${type}: ${displayText}`; } editor.focus(); } // Update placeholder when search type or select mode changes function updatePlaceholder() { const type = searchType.value; currentIndex = -1; // Reset navigation if (searchInput.value.trim() === "") { const action = selectMode.checked ? "select" : "navigate"; searchInput.placeholder = `Search ${type} or ${action} with arrows…`; } } // Event listeners searchType.addEventListener("change", updatePlaceholder); selectMode.addEventListener("change", updatePlaceholder); searchInput.addEventListener("input", () => { if (searchInput.value.trim() === "") { updatePlaceholder(); } else { const type = searchType.value; searchInput.placeholder = `Search ${type}…`; } }); searchNext.addEventListener("click", () => doFind(false)); searchPrev.addEventListener("click", () => doFind(true)); searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); doFind(e.shiftKey); } }); // Initialize placeholder updatePlaceholder(); currentItems = getCodeElements(type); const filteredItems = currentItems.filter(item => item.text.toLowerCase().includes(query.toLowerCase()) || (item.name && item.name.toLowerCase().includes(query.toLowerCase())) ); if (filteredItems.length === 0) { searchInput.placeholder = `No ${type} matching "${query}"`; return; } // Find current position in filtered results const currentRow = editor.getCursorPosition().row; let startIndex = 0; if (backwards) { startIndex = filteredItems.findLastIndex(item => item.row < currentRow); if (startIndex < 0) startIndex = filteredItems.length - 1; } else { startIndex = filteredItems.findIndex(item => item.row > currentRow); if (startIndex < 0) startIndex = 0; } const item = filteredItems[startIndex]; editor.gotoLine(item.row + 1, 0, true); highlightLine(item.row); const displayText = item.name || item.text.slice(0, 30); searchInput.placeholder = `Found ${type}: ${displayText}`; } editor.focus(); } // Update placeholder when search type changes function updatePlaceholder() { const type = searchType.value; currentIndex = -1; // Reset navigation if (searchInput.value.trim() === "") { searchInput.placeholder = `Search ${type} or navigate with arrows…`; } } // Event listeners searchType.addEventListener("change", updatePlaceholder); searchInput.addEventListener("input", () => { if (searchInput.value.trim() === "") { updatePlaceholder(); } else { const type = searchType.value; searchInput.placeholder = `Search ${type}…`; } }); searchNext.addEventListener("click", () => doFind(false)); searchPrev.addEventListener("click", () => doFind(true)); searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); doFind(e.shiftKey); } }); // Initialize placeholder updatePlaceholder(); // ===== Export for AI module ===== window.editorAPI = { editor, getSelectedText: () => editor.getSelectedText(), getValue: () => editor.getValue(), focus: () => editor.focus() };