// ===== 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()
};