// Editor that syncs with localStorage for SFTP active files - ACE VERSION with Smart Markers
(function() {
const ACTIVE_FILES_KEY = 'sftp_active_files';
// === LANGUAGE HANDLING ===
const LANG_DEFS = {
html: {
mode: "ace/mode/html",
exts: ["html", "htm"],
comment: { open: "<!--", close: "-->" }
},
css: {
mode: "ace/mode/css",
exts: ["css"],
comment: { open: "/*", close: "*/" }
},
javascript: {
mode: "ace/mode/javascript",
exts: ["js", "mjs", "cjs"],
comment: { open: "//", close: "" }
},
php: {
mode: "ace/mode/php",
exts: ["php", "phtml"],
comment: { open: "//", close: "" }
},
json: {
mode: "ace/mode/json",
exts: ["json"],
comment: { open: "//", close: "" }
},
markdown: {
mode: "ace/mode/markdown",
exts: ["md", "markdown"],
comment: { open: "<!--", close: "-->" }
},
python: {
mode: "ace/mode/python",
exts: ["py"],
comment: { open: "#", close: "" }
},
text: {
mode: "ace/mode/text",
exts: ["txt"],
comment: { open: "//", close: "" }
}
};
// Choose Ace mode based on file extension
function modeFromFileName(fileName = "") {
const ext = (fileName.split(".").pop() || "").toLowerCase();
for (const [lang, def] of Object.entries(LANG_DEFS)) {
if (def.exts.includes(ext)) return def.mode;
}
return LANG_DEFS.html.mode;
}
// Detect which sublanguage is active under the cursor
function detectSubLanguage(editor) {
const pos = editor.getCursorPosition();
const row = pos.row;
const col = pos.column;
// Get tokens on current line
const tokens = editor.session.getTokens(row);
// Find which token we're in
let currentCol = 0;
let activeToken = null;
for (const token of tokens) {
const tokenEnd = currentCol + token.value.length;
if (col >= currentCol && col <= tokenEnd) {
activeToken = token;
break;
}
currentCol = tokenEnd;
}
if (!activeToken) {
activeToken = tokens[tokens.length - 1] || { type: 'text' };
}
const type = (activeToken.type || '').toLowerCase();
const currentMode = editor.session.getMode().$id || "";
// Check for JavaScript
if (type.includes('javascript') ||
type.includes('js') ||
type.includes('script') ||
type === 'storage.type.js' ||
type === 'keyword.operator.js' ||
type.startsWith('source.js')) {
return "javascript";
}
// Check for CSS
if (type.includes('css') ||
type.includes('style') ||
type === 'source.css' ||
type.startsWith('entity.name.tag.css') ||
type.startsWith('support.type.property-name')) {
return "css";
}
// Check for PHP
if (type.includes('php') ||
type.includes('meta.tag.php') ||
type === 'source.php' ||
type.startsWith('keyword.control.php') ||
type.startsWith('support.function.php')) {
return "php";
}
// For HTML/PHP files, check context
if (currentMode.includes("html") || currentMode.includes("php")) {
let scriptDepth = 0;
let styleDepth = 0;
let phpDepth = 0;
for (let i = 0; i <= row; i++) {
const line = editor.session.getLine(i);
scriptDepth += (line.match(/<script[^>]*>/gi) || []).length;
scriptDepth -= (line.match(/<\/script>/gi) || []).length;
styleDepth += (line.match(/<style[^>]*>/gi) || []).length;
styleDepth -= (line.match(/<\/style>/gi) || []).length;
phpDepth += (line.match(/<\?php/gi) || []).length;
phpDepth -= (line.match(/\?>/gi) || []).length;
}
if (scriptDepth > 0) return "javascript";
if (styleDepth > 0) return "css";
if (phpDepth > 0) return "php";
if (type.includes('tag') || type.includes('attr') || type.includes('entity.name.tag')) {
return "html";
}
}
// Fallback to mode
if (currentMode.includes("javascript")) return "javascript";
if (currentMode.includes("css")) return "css";
if (currentMode.includes("python")) return "python";
if (currentMode.includes("markdown")) return "markdown";
if (currentMode.includes("json")) return "json";
if (currentMode.includes("php")) return "php";
if (currentMode.includes("html")) return "html";
return "text";
}
// Return correct comment delimiters for the detected language
function getCommentStyleFor(langKey) {
return LANG_DEFS[langKey]?.comment || { open: "//", close: "" };
}
// Get language name from mode
function getLangNameFromMode(mode) {
for (const [lang, def] of Object.entries(LANG_DEFS)) {
if (def.mode === mode || mode.includes(lang)) return lang;
}
return 'text';
}
// Detect language context for a line range
function detectLanguageContext(lines, startLine, endLine) {
let inScript = false;
let inStyle = false;
let inPhp = false;
// Check context from beginning of file to start of scope
for (let i = 0; i <= startLine; i++) {
const line = lines[i];
// Track script tags
if (/<script[^>]*>/i.test(line)) inScript = true;
if (/<\/script>/i.test(line)) inScript = false;
// Track style tags
if (/<style[^>]*>/i.test(line)) inStyle = true;
if (/<\/style>/i.test(line)) inStyle = false;
// Track PHP tags
if (/<\?php/i.test(line)) inPhp = true;
if (/\?>/i.test(line)) inPhp = false;
}
// Determine language based on context
if (inScript) return 'javascript';
if (inStyle) return 'css';
if (inPhp) return 'php';
// Check the actual content of the scope
const scopeContent = lines.slice(startLine, endLine + 1).join('\n');
if (/(<\?php|<\?=)/i.test(scopeContent)) return 'php';
if (/<script[^>]*>/i.test(scopeContent)) return 'javascript';
if (/<style[^>]*>/i.test(scopeContent)) return 'css';
if (/<[a-z]+[^>]*>/i.test(scopeContent)) return 'html';
return 'text';
}
// Parse scopes/markers from content and group by name
function parseScopes(content) {
if (!content) return [];
const scopeInstances = [];
const lines = content.split('\n');
// Patterns to match opening markers in different comment styles
const patterns = [
{ regex: /\/\/\s*([A-Z_]+)<\s*$/, style: 'js' },
{ regex: /\/\*\s*([A-Z_]+)<\s*\*\//, style: 'css' },
{ regex: /<!--\s*([A-Z_]+)<\s*-->/, style: 'html' },
{ regex: /#\s*([A-Z_]+)<\s*$/, style: 'python' }
];
const scopeStack = [];
lines.forEach((line, idx) => {
// Check for opening marker
for (const pattern of patterns) {
const match = line.match(pattern.regex);
if (match) {
const newScope = {
name: match[1],
startLine: idx,
preview: '',
depth: scopeStack.length
};
scopeStack.push(newScope);
break;
}
}
// Check for closing marker
if (scopeStack.length > 0) {
const currentScope = scopeStack[scopeStack.length - 1];
const closingPatterns = [
new RegExp(`\\/\\/\\s*${currentScope.name}>\\s*$`),
new RegExp(`\\/\\*\\s*${currentScope.name}>\\s*\\*\\/`),
new RegExp(`<!--\\s*${currentScope.name}>\\s*-->`),
new RegExp(`#\\s*${currentScope.name}>\\s*$`)
];
for (const pattern of closingPatterns) {
if (pattern.test(line)) {
currentScope.endLine = idx;
currentScope.lineCount = currentScope.endLine - currentScope.startLine + 1;
// Detect language context
currentScope.language = detectLanguageContext(lines, currentScope.startLine, currentScope.endLine);
scopeInstances.push(currentScope);
scopeStack.pop();
break;
}
}
// Build preview (first non-marker line)
if (currentScope && !currentScope.preview && idx > currentScope.startLine) {
const trimmed = line.trim();
if (trimmed && !patterns.some(p => p.regex.test(trimmed))) {
currentScope.preview = trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '');
}
}
}
});
// Close any unclosed scopes
while (scopeStack.length > 0) {
const currentScope = scopeStack.pop();
currentScope.endLine = lines.length - 1;
currentScope.lineCount = currentScope.endLine - currentScope.startLine + 1;
currentScope.language = detectLanguageContext(lines, currentScope.startLine, currentScope.endLine);
scopeInstances.push(currentScope);
}
// Group by name
const groupedScopes = {};
scopeInstances.forEach(scope => {
if (!groupedScopes[scope.name]) {
groupedScopes[scope.name] = {
name: scope.name,
instances: []
};
}
groupedScopes[scope.name].instances.push(scope);
});
// Convert to array and sort by first occurrence
return Object.values(groupedScopes).sort((a, b) => {
return a.instances[0].startLine - b.instances[0].startLine;
});
}
// Wrap selection with smart marker
function wrapSelectionWithSmartMarker(editor, markerName) {
if (!editor) {
console.error('❌ No editor instance');
return;
}
const selected = editor.getSelectedText();
if (!selected) {
alert("⚠️ Select some text first!");
return;
}
const range = editor.getSelectionRange();
const subLang = detectSubLanguage(editor);
const { open, close } = getCommentStyleFor(subLang);
let wrapped;
if (close) {
wrapped = `${open}${markerName}<${close}\n${selected}\n${open}${markerName}>${close}`;
} else {
wrapped = `${open}${markerName}<\n${selected}\n${open}${markerName}>`;
}
const wasReadOnly = editor.getReadOnly();
editor.setReadOnly(false);
editor.session.replace(range, wrapped);
editor.setReadOnly(wasReadOnly);
if (typeof showToast === "function") {
showToast(`✅ Marked: ${markerName} (${subLang})`, "success");
}
}
// Wait for ACE to be available
function waitForAce(callback, attempts = 0) {
if (typeof ace !== 'undefined') {
console.log('✅ ACE Editor is available');
callback();
} else if (attempts < 50) {
setTimeout(() => waitForAce(callback, attempts + 1), 100);
} else {
console.error('❌ ACE Editor failed to load after 5 seconds');
callback();
}
}
// Load active files from localStorage
function loadActiveFiles() {
try {
const saved = localStorage.getItem(ACTIVE_FILES_KEY);
return saved ? JSON.parse(saved) : [];
} catch (e) {
console.error('Failed to load active files:', e);
return [];
}
}
// Save active files to localStorage
function saveActiveFiles(files) {
try {
localStorage.setItem(ACTIVE_FILES_KEY, JSON.stringify(files));
return true;
} catch (e) {
console.error('Failed to save active files:', e);
return false;
}
}
// Get the currently active file
function getActiveFile() {
const files = loadActiveFiles();
return files.find(f => f.active) || null;
}
// Update the active file content
function updateActiveFileContent(content) {
const files = loadActiveFiles();
const activeIndex = files.findIndex(f => f.active);
if (activeIndex !== -1) {
files[activeIndex].content = content;
files[activeIndex].lastModified = new Date().toISOString();
saveActiveFiles(files);
return true;
}
return false;
}
// Create the editor interface
function createEditorHTML() {
return `
<div style="
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
">
<!-- File Info Bar -->
<div id="editorFileInfo" style="
background: #252525;
padding: 8px 16px;
border-bottom: 1px solid #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
color: #9aa4b2;
font-size: 14px;
flex-shrink: 0;
">
<div style="display: flex; align-items: center; gap: 12px;">
<div>
<span id="editorFileName" style="color: #e6edf3; font-weight: 600;">No file selected</span>
<span id="editorFilePath" style="margin-left: 12px; color: #64748b; font-size: 12px;"></span>
</div>
<button id="openIndexBtn" class="editor-toolbar-btn" style="
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #fff;
padding: 6px 16px;
border-radius: 0;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
">🎯 Scopes</button>
<button id="addMarkerBtn" class="editor-toolbar-btn" style="
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #fff;
padding: 6px 16px;
border-radius: 0;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
">➕ Mark</button>
</div>
<div id="editorStatus" style="color: #10b981;">
● Auto-save enabled
</div>
</div>
<!-- ACE Editor Container -->
<div id="aceEditorContainer" style="
flex:1;
position:relative;
width:100%;
min-height:600px;
"></div>
<!-- Status Bar -->
<div style="
background: #252525;
padding: 6px 16px;
border-top: 1px solid #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #64748b;
flex-shrink: 0;
">
<div id="editorStats">
Lines: <span id="lineCount">0</span> |
Characters: <span id="charCount">0</span> |
Mode: <span id="editorMode">text</span> |
Cursor: <span id="cursorLang" style="color: #3b82f6; font-weight: 600;">-</span>
</div>
<div id="lastSaved">Last saved: <span id="lastSavedTime">Never</span></div>
</div>
</div>
`;
}
// Get language color and icon
function getLanguageStyle(lang) {
const styles = {
javascript: { color: '#f7df1e', bg: 'rgba(247, 223, 30, 0.1)', icon: '🟨' },
css: { color: '#264de4', bg: 'rgba(38, 77, 228, 0.1)', icon: '🟦' },
php: { color: '#8892bf', bg: 'rgba(136, 146, 191, 0.1)', icon: '🟪' },
html: { color: '#e34c26', bg: 'rgba(227, 76, 38, 0.1)', icon: '🟧' },
python: { color: '#3776ab', bg: 'rgba(55, 118, 171, 0.1)', icon: '🐍' },
text: { color: '#64748b', bg: 'rgba(100, 116, 139, 0.1)', icon: '📄' }
};
return styles[lang] || styles.text;
}
// Render scope group with all instances
function renderScopeGroup(group) {
const instances = group.instances;
const totalLines = instances.reduce((sum, inst) => sum + inst.lineCount, 0);
// Get language distribution
const langCounts = {};
instances.forEach(inst => {
langCounts[inst.language] = (langCounts[inst.language] || 0) + 1;
});
// Primary language is the most common
const primaryLang = Object.entries(langCounts).sort((a, b) => b[1] - a[1])[0][0];
const langStyle = getLanguageStyle(primaryLang);
let html = `
<div class="scope-group" style="
background: ${langStyle.bg};
border-left: 4px solid ${langStyle.color};
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s;
">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 20px;">${langStyle.icon}</span>
<div>
<div style="color: ${langStyle.color}; font-weight: 700; font-size: 17px; font-family: monospace;">
${escapeHtml(group.name)}
</div>
<div style="color: #64748b; font-size: 11px; margin-top: 2px;">
${instances.length} instance${instances.length !== 1 ? 's' : ''} • ${totalLines} total lines
</div>
</div>
</div>
<div style="display: flex; gap: 4px;">
`;
// Show language badges
Object.entries(langCounts).forEach(([lang, count]) => {
const style = getLanguageStyle(lang);
html += `
<span style="
background: ${style.bg};
border: 1px solid ${style.color};
color: ${style.color};
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
">${lang} ${count}</span>
`;
});
html += `
</div>
</div>
`;
// Render each instance
instances.forEach((instance, idx) => {
const instStyle = getLanguageStyle(instance.language);
html += `
<div class="scope-instance"
data-start-line="${instance.startLine}"
data-end-line="${instance.endLine}"
style="
background: rgba(0, 0, 0, 0.2);
border-left: 3px solid ${instStyle.color};
border-radius: 4px;
padding: 8px 10px;
margin-bottom: ${idx < instances.length - 1 ? '6px' : '0'};
transition: all 0.2s;
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<div style="display: flex; align-items: center; gap: 6px;">
<span style="
background: ${instStyle.color};
color: #000;
font-weight: 900;
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
">#${idx + 1}</span>
<span style="color: ${instStyle.color}; font-size: 11px; text-transform: uppercase; font-weight: 600;">
${instance.language}
</span>
</div>
<div style="color: #64748b; font-size: 11px;">
Lines ${instance.startLine + 1}-${instance.endLine + 1} (${instance.lineCount})
</div>
</div>
${instance.preview ? `
<div style="
color: #9aa4b2;
font-size: 11px;
font-family: monospace;
background: rgba(0,0,0,0.3);
padding: 4px 6px;
border-radius: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
">${escapeHtml(instance.preview)}</div>
` : ''}
</div>
`;
});
html += '</div>';
return html;
}
// Create the Scopes Index overlay HTML
function createScopesHTML() {
const activeFile = getActiveFile();
let html = `
<div style="padding: 20px; height: 100%; overflow-y: auto; background: #000;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2 style="margin: 0; color: #e6edf3; font-size: 18px;">📍 Code Scopes</h2>
`;
if (!activeFile) {
html += `
</div>
<div style="
text-align: center;
padding: 60px 20px;
color: #64748b;
">
<div style="font-size: 48px; margin-bottom: 16px;">📄</div>
<p style="margin: 0; font-size: 16px;">No file open</p>
<p style="margin: 8px 0 0 0; font-size: 14px;">Open a file to see its scopes</p>
</div>
</div>
`;
return html;
}
const scopeGroups = parseScopes(activeFile.content);
// Count total instances and languages
let totalInstances = 0;
const allLangCounts = {};
scopeGroups.forEach(group => {
totalInstances += group.instances.length;
group.instances.forEach(inst => {
allLangCounts[inst.language] = (allLangCounts[inst.language] || 0) + 1;
});
});
// Language filter buttons
html += '<div style="display: flex; gap: 6px; flex-wrap: wrap;">';
const languages = Object.keys(allLangCounts).sort();
languages.forEach(lang => {
const style = getLanguageStyle(lang);
html += `
<button class="lang-filter" data-lang="${lang}" style="
background: ${style.bg};
border: 2px solid ${style.color};
color: ${style.color};
padding: 4px 12px;
border-radius: 16px;
cursor: pointer;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
transition: all 0.2s;
">${style.icon} ${lang} (${allLangCounts[lang]})</button>
`;
});
html += `
<button class="lang-filter active-filter" data-lang="all" style="
background: rgba(59, 130, 246, 0.2);
border: 2px solid #3b82f6;
color: #3b82f6;
padding: 4px 12px;
border-radius: 16px;
cursor: pointer;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
transition: all 0.2s;
opacity: 1;
">ALL</button>
</div>
</div>
`;
if (scopeGroups.length === 0) {
html += `
<div style="
text-align: center;
padding: 60px 20px;
color: #64748b;
">
<div style="font-size: 48px; margin-bottom: 16px;">🎯</div>
<p style="margin: 0; font-size: 16px;">No scopes found</p>
<p style="margin: 8px 0 0 0; font-size: 14px;">Use the "➕ Mark" button to add markers</p>
</div>
`;
} else {
html += `<div style="margin: 12px 0; color: #64748b; font-size: 13px;">
Found <strong style="color: #e6edf3;">${scopeGroups.length}</strong> unique scope${scopeGroups.length !== 1 ? 's' : ''}
with <strong style="color: #10b981;">${totalInstances}</strong> total instance${totalInstances !== 1 ? 's' : ''}
in <strong style="color: #e6edf3;">${escapeHtml(activeFile.name)}</strong>
</div>`;
html += '<div id="scopesList">';
scopeGroups.forEach(group => {
html += renderScopeGroup(group);
});
html += '</div>';
}
html += '</div>';
return html;
}
// Setup Scopes overlay interactions
function setupScopesInteractions(containerEl) {
const activeFile = getActiveFile();
const scopeGroups = activeFile ? parseScopes(activeFile.content) : [];
// Flatten all instances for easier lookup
const allInstances = [];
scopeGroups.forEach(group => {
group.instances.forEach(inst => {
allInstances.push(inst);
});
});
// Language filter functionality
let activeFilter = 'all';
const filterButtons = containerEl.querySelectorAll('.lang-filter');
const scopeInstances = containerEl.querySelectorAll('.scope-instance');
function applyFilter(lang) {
activeFilter = lang;
// Update button styles
filterButtons.forEach(btn => {
if (btn.dataset.lang === lang) {
btn.style.opacity = '1';
btn.style.transform = 'scale(1.05)';
btn.classList.add('active-filter');
} else {
btn.style.opacity = '0.5';
btn.style.transform = 'scale(1)';
btn.classList.remove('active-filter');
}
});
// Filter scope instances
allInstances.forEach((instance, index) => {
const item = scopeInstances[index];
if (!item) return;
if (lang === 'all' || instance.language === lang) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
// Show/hide groups based on whether they have visible instances
const scopeGroups = containerEl.querySelectorAll('.scope-group');
scopeGroups.forEach(groupEl => {
const visibleInstances = Array.from(groupEl.querySelectorAll('.scope-instance'))
.filter(inst => inst.style.display !== 'none');
if (visibleInstances.length > 0) {
groupEl.style.display = 'block';
} else {
groupEl.style.display = 'none';
}
});
}
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
applyFilter(btn.dataset.lang);
});
});
// Initialize with 'all' filter active
applyFilter('all');
// Scope instance interactions
scopeInstances.forEach((item, index) => {
const instance = allInstances[index];
if (!instance) return;
const langStyle = getLanguageStyle(instance.language);
item.addEventListener('mouseenter', () => {
item.style.borderLeftColor = '#10b981';
item.style.borderLeftWidth = '4px';
item.style.background = 'rgba(16, 185, 129, 0.15)';
item.style.transform = 'translateX(4px)';
});
item.addEventListener('mouseleave', () => {
item.style.borderLeftColor = langStyle.color;
item.style.borderLeftWidth = '3px';
item.style.background = 'rgba(0, 0, 0, 0.2)';
item.style.transform = 'translateX(0)';
});
item.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent group click
// Close overlay first
if (window.AppOverlay && typeof AppOverlay.close === 'function') {
AppOverlay.close();
}
// Then select the scope in editor
if (window._globalEditorInstance && instance) {
const editor = window._globalEditorInstance;
// Navigate to the start line
editor.gotoLine(instance.startLine + 1, 0, true);
// Select from start to end line
const Range = ace.require('ace/range').Range;
const selection = new Range(
instance.startLine,
0,
instance.endLine,
editor.session.getLine(instance.endLine).length
);
editor.selection.setRange(selection);
editor.focus();
// Optional: Show confirmation
if (typeof showToast === "function") {
showToast(`✅ Selected ${instance.language} scope: ${instance.name} (${instance.lineCount} lines)`, "success");
}
}
});
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize ACE editor functionality
function initializeEditor(containerEl) {
console.log('🔧 Initializing ACE editor...');
if (typeof ace === 'undefined') {
console.error('❌ ACE Editor is not available');
containerEl.innerHTML = `
<div style="padding: 40px; text-align: center; color: #ef4444;">
<h2>⚠️ ACE Editor Not Loaded</h2>
<p>ACE Editor library is not available.</p>
<button onclick="location.reload()" style="
margin-top: 20px;
padding: 10px 20px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
">🔄 Reload Page</button>
</div>
`;
return;
}
const editorContainer = containerEl.querySelector('#aceEditorContainer');
const fileNameEl = containerEl.querySelector('#editorFileName');
const filePathEl = containerEl.querySelector('#editorFilePath');
const statusEl = containerEl.querySelector('#editorStatus');
const lineCountEl = containerEl.querySelector('#lineCount');
const charCountEl = containerEl.querySelector('#charCount');
const modeEl = containerEl.querySelector('#editorMode');
const cursorLangEl = containerEl.querySelector('#cursorLang');
const lastSavedTimeEl = containerEl.querySelector('#lastSavedTime');
const openIndexBtn = containerEl.querySelector('#openIndexBtn');
const addMarkerBtn = containerEl.querySelector('#addMarkerBtn');
console.log('🎨 Creating ACE editor instance...');
const editor = ace.edit(editorContainer);
console.log('✅ ACE editor instance created');
editor.setTheme('ace/theme/monokai');
editor.session.setMode('ace/mode/text');
editor.setOptions({
fontSize: '14px',
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
showPrintMargin: false,
highlightActiveLine: true,
tabSize: 2,
useSoftTabs: true,
wrap: true
});
let saveTimeout = null;
let isInitialLoad = true;
// Update cursor language indicator
function updateCursorLang() {
const lang = detectSubLanguage(editor);
if (cursorLangEl) {
cursorLangEl.textContent = lang;
}
}
// Listen for cursor position changes
editor.selection.on('changeCursor', updateCursorLang);
// Toolbar button hover effects
const toolbarBtns = containerEl.querySelectorAll('.editor-toolbar-btn');
toolbarBtns.forEach(btn => {
btn.addEventListener('mouseenter', () => {
btn.style.background = '#2a2a2a';
btn.style.borderColor = '#3a3a3a';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = '#1a1a1a';
btn.style.borderColor = '#2a2a2a';
});
});
// Scopes button handler
if (openIndexBtn) {
openIndexBtn.addEventListener('click', () => {
if (window.AppOverlay && typeof AppOverlay.open === 'function') {
AppOverlay.open([{
title: '🎯 Code Scopes',
html: createScopesHTML(),
onRender: setupScopesInteractions
}]);
}
});
}
// Mark button handler
if (addMarkerBtn) {
addMarkerBtn.addEventListener('click', () => {
const selected = editor.getSelectedText();
if (!selected) {
alert("⚠️ Select some text first!");
return;
}
const markerName = prompt("Enter marker name (e.g., SCOPE, TARGET, KEEP, NOTE):");
if (markerName && markerName.trim()) {
wrapSelectionWithSmartMarker(editor, markerName.trim().toUpperCase());
}
});
}
// Load active file
function loadFile() {
const activeFile = getActiveFile();
if (activeFile) {
editor.setValue(activeFile.content || '', -1);
fileNameEl.textContent = activeFile.name;
filePathEl.textContent = activeFile.path;
const mode = modeFromFileName(activeFile.name);
editor.session.setMode(mode);
modeEl.textContent = getLangNameFromMode(mode);
if (activeFile.lastModified) {
const lastMod = new Date(activeFile.lastModified);
lastSavedTimeEl.textContent = lastMod.toLocaleTimeString();
}
} else {
editor.setValue('', -1);
fileNameEl.textContent = 'No file selected';
filePathEl.textContent = '';
lastSavedTimeEl.textContent = 'Never';
editor.session.setMode('ace/mode/text');
modeEl.textContent = 'text';
}
updateStats();
updateCursorLang();
setTimeout(() => {
isInitialLoad = false;
}, 500);
}
// Update statistics
function updateStats() {
const content = editor.getValue();
const lines = editor.session.getLength();
const chars = content.length;
lineCountEl.textContent = lines;
charCountEl.textContent = chars;
}
// Auto-save function
function autoSave() {
if (isInitialLoad) return;
const content = editor.getValue();
const success = updateActiveFileContent(content);
if (success) {
statusEl.style.color = '#10b981';
statusEl.textContent = '● Saved';
lastSavedTimeEl.textContent = new Date().toLocaleTimeString();
window.dispatchEvent(new CustomEvent('activeFileUpdated', {
detail: { content }
}));
setTimeout(() => {
statusEl.textContent = '● Auto-save enabled';
}, 2000);
} else {
statusEl.style.color = '#ef4444';
statusEl.textContent = '● Save failed';
}
}
// Listen for changes
editor.session.on('change', () => {
if (isInitialLoad) return;
updateStats();
statusEl.style.color = '#f59e0b';
statusEl.textContent = '● Unsaved changes';
clearTimeout(saveTimeout);
saveTimeout = setTimeout(autoSave, 500);
});
// Listen for external file changes
window.addEventListener('activeFilesUpdated', () => {
loadFile();
});
// Initial load
loadFile();
// Focus editor and force resize
setTimeout(() => {
editor.resize(true);
editor.focus();
}, 100);
// Resize on window resize
window.addEventListener('resize', () => {
editor.resize();
});
// Store editor instance for external access
containerEl._aceEditor = editor;
window._globalEditorInstance = editor;
console.log('✅ ACE editor fully initialized');
}
// Wait for ACE before initializing
waitForAce(() => {
console.log('🚀 Storage Editor initializing...');
if (window.AppItems) {
window.AppItems.push({
title: 'Storage Editor',
html: createEditorHTML(),
onRender: initializeEditor
});
}
// Export API
window.StorageEditor = {
open: () => {
if (window.AppOverlay && typeof AppOverlay.open === 'function') {
AppOverlay.open([{
title: '📝 Storage Editor',
html: createEditorHTML(),
onRender: initializeEditor
}]);
}
},
openScopes: () => {
if (window.AppOverlay && typeof AppOverlay.open === 'function') {
AppOverlay.open([{
title: '🎯 Code Scopes',
html: createScopesHTML(),
onRender: setupScopesInteractions
}]);
}
},
mark: (markerName) => {
if (window._globalEditorInstance) {
wrapSelectionWithSmartMarker(window._globalEditorInstance, markerName || 'MARKER');
}
},
getActiveFile,
loadActiveFiles,
saveActiveFiles
};
console.log('✅ Storage Editor initialized');
});
})();