🐘
index.php
Back
📝 Php ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// editor.js (function () { try { console.log("[editor.js] Loading HTML editor module..."); window.AppItems = window.AppItems || []; // Store editor instance globally so close hook can access it let globalEditorInstance = null; let saveTimeout = null; // Search state let searchState = { matches: [], idx: -1, markers: [] }; // Fold selection state let lastCursorPos = null; let lastFoldIndex = -1; let cachedFolds = []; // Improved save function with proper error handling function saveToLocalStorage(editorInstance) { if (!editorInstance) return; try { const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); if (active) { active.content = editorInstance.getValue(); localStorage.setItem('sftp_active_files', JSON.stringify(files)); console.log(`[editor.js] ✓ Saved ${active.name}`); return true; } } catch (err) { console.error("[editor.js] Failed to save:", err); return false; } } // Debounced save - saves 500ms after last keystroke function debouncedSave(editorInstance) { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { saveToLocalStorage(editorInstance); }, 500); } // === SMART MARKER FUNCTIONS === function detectSubLanguage(editor) { const pos = editor.getCursorPosition(); const token = editor.session.getTokenAt(pos.row, pos.column); if (!token) return "php"; const t = token.type || ""; if (t.includes("php")) return "php"; if (t.includes("js")) return "javascript"; if (t.includes("css")) return "css"; if (t.includes("tag") || t.includes("attr")) return "html"; return "php"; } function getCommentStyleFor(lang) { switch (lang) { case "html": return { open: "<!--", close: "-->" }; case "css": return { open: "/*", close: "*/" }; case "javascript": return { open: "//", close: "" }; case "php": return { open: "/*", close: "*/" }; default: return { open: "//", close: "" }; } } function wrapSelectionWithSmartMarker(markerName) { if (!globalEditorInstance) return; const selected = globalEditorInstance.getSelectedText(); if (!selected) { if (typeof showToast === 'function') { showToast('⚠️ Select some text first!', 'error'); } return; } const range = globalEditorInstance.getSelectionRange(); const subLang = detectSubLanguage(globalEditorInstance); const { open, close } = getCommentStyleFor(subLang); // Build wrapped text with proper comment syntax let wrapped; if (close) { // Block comment style (HTML, CSS, PHP) - each marker closes on same line wrapped = `${open}${markerName}<${close}\n${selected}\n${open}${markerName}>${close}`; } else { // Line comment style (JavaScript, PHP with //) wrapped = `${open}${markerName}<\n${selected}\n${open}${markerName}>`; } // Temporarily enable editing to insert marker const wasReadOnly = globalEditorInstance.getReadOnly(); globalEditorInstance.setReadOnly(false); globalEditorInstance.session.replace(range, wrapped); // Restore read-only state globalEditorInstance.setReadOnly(wasReadOnly); if (typeof showToast === 'function') { showToast(`✅ Wrapped with marker: ${markerName}`, 'success'); } console.log(`[editor.js] Wrapped selection with marker "${markerName}" using ${subLang} syntax`); } const section = { title: "HTML Editor", html: ` <div class="editor-section"> <div class="editor-toolbar" style=" display: flex; gap: 8px; padding: 8px 12px; background: #1e1e1e; border-bottom: 1px solid #333; align-items: center; "> <button id="indexBtn" title="Show document index" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 13px; font-family: 'Segoe UI', sans-serif; " >📑 Index</button> <button id="editSelectionBtn" title="Edit selection in overlay" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 13px; font-family: 'Segoe UI', sans-serif; " >✏️ Edit</button> <input type="text" id="editorSearchInput" placeholder="Find in file... (Ctrl+F)" style=" flex: 1; padding: 6px 10px; background: #2d2d2d; border: 1px solid #444; border-radius: 4px; color: #e0e0e0; font-size: 13px; font-family: 'Segoe UI', sans-serif; " /> <button id="searchPrevBtn" title="Previous match (Shift+Enter)" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 16px; " >↑</button> <button id="searchNextBtn" title="Next match (Enter)" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 16px; " >↓</button> <span id="matchCounter" style=" color: #888; font-size: 13px; font-family: 'Segoe UI', sans-serif; min-width: 60px; "></span> </div> <div class="ace-editor" id="ace-editor-placeholder"></div> </div> <!-- Multi-purpose Overlay --> <div id="multiOverlay" style=" display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 999998; align-items: center; justify-content: center; "> <div style=" background: #2d2d2d; border: 1px solid #555; border-radius: 8px; width: 90%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.5); "> <div style=" padding: 16px 20px; border-bottom: 1px solid #555; display: flex; justify-content: space-between; align-items: center; "> <h3 id="overlayTitle" style="margin: 0; color: #e0e0e0; font-size: 18px; font-family: 'Segoe UI', sans-serif;">Overlay</h3> <button id="closeOverlayBtn" style=" background: none; border: none; color: #888; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; ">&times;</button> </div> <div id="overlayContent" style=" padding: 12px; overflow-y: auto; flex: 1; "></div> <div id="overlayFooter" style=" padding: 12px 20px; border-top: 1px solid #555; display: none; gap: 8px; "></div> </div> </div> `, onRender(el) { console.log("[editor.js] onRender fired"); const container = el.querySelector('.ace-editor'); if (!container) return console.warn("[editor.js] No .ace-editor found"); container.style.minHeight = "calc(70vh - 50px)"; container.style.display = "block"; function loadAce(cb) { if (window.ace) return cb(); const s = document.createElement('script'); s.src = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.3/ace.js"; s.onload = cb; document.head.appendChild(s); } function fitToOverlayBody() { const body = container.closest('.app-dialog')?.querySelector('.app-dialog__body'); if (!body) return; const bodyRect = body.getBoundingClientRect(); const topInBody = container.getBoundingClientRect().top - bodyRect.top; const targetH = Math.max(200, Math.floor(bodyRect.height - topInBody - 6)); container.style.height = targetH + "px"; } // === OVERLAY MANAGEMENT === let overlayEditorInstance = null; let selectionRange = null; let editHistory = []; let currentEditIndex = 0; function showOverlay(title, content, footer = null) { const overlay = el.querySelector('#multiOverlay'); const titleEl = el.querySelector('#overlayTitle'); const contentEl = el.querySelector('#overlayContent'); const footerEl = el.querySelector('#overlayFooter'); if (!overlay || !titleEl || !contentEl || !footerEl) return; titleEl.textContent = title; contentEl.innerHTML = content; if (footer) { footerEl.innerHTML = footer; footerEl.style.display = 'flex'; } else { footerEl.style.display = 'none'; } overlay.style.display = 'flex'; } function hideOverlay() { const overlay = el.querySelector('#multiOverlay'); if (overlay) overlay.style.display = 'none'; // Clean up overlay editor if exists if (overlayEditorInstance) { overlayEditorInstance.destroy(); overlayEditorInstance = null; } // Clear edit history editHistory = []; currentEditIndex = 0; } // === EDIT SELECTION FUNCTIONS === function showEditSelectionOverlay() { if (!globalEditorInstance) return; const selected = globalEditorInstance.getSelectedText(); let hasSelection = selected && selected.trim().length > 0; let finalContent = ''; // Store the selection range (or cursor position) if (hasSelection) { selectionRange = globalEditorInstance.getSelectionRange(); // Check if selection overlaps with any index items const expandedContent = expandSelectionToIndexItems(selectionRange); if (expandedContent) { finalContent = expandedContent; hasSelection = true; } else { finalContent = selected; } } else { // No selection - get cursor position const pos = globalEditorInstance.getCursorPosition(); const Range = ace.require('ace/range').Range; selectionRange = new Range(pos.row, pos.column, pos.row, pos.column); finalContent = ''; } // Initialize edit history with current content editHistory = [finalContent]; currentEditIndex = 0; // Create editor container with navigation const editorHtml = ` <div style="display: flex; gap: 12px; height: 400px;"> <div style="flex: 1; display: flex; flex-direction: column; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px; padding: 8px; background: #1e293b; border-radius: 4px;"> <button id="prevEditBtn" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 16px; display: none; ">←</button> <span id="editIndexDisplay" style=" flex: 1; text-align: center; color: #888; font-size: 13px; font-family: 'Segoe UI', sans-serif; display: none; ">1 / 1</span> <button id="nextEditBtn" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 16px; display: none; ">→</button> <button id="addEditBtn" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 13px; font-family: 'Segoe UI', sans-serif; ">+ New</button> </div> <div id="overlayEditor" style="flex: 1; border: 1px solid #555; border-radius: 4px;"></div> </div> </div> `; const buttonText = hasSelection ? '✅ Replace Selection' : '➕ Add at Cursor'; const footerHtml = ` <button id="replaceBtn" style=" padding: 8px 16px; background: #16a34a; border: 1px solid #15803d; border-radius: 4px; color: #fff; cursor: pointer; font-size: 14px; font-family: 'Segoe UI', sans-serif; font-weight: 600; ">${buttonText}</button> <button id="cancelEditBtn" style=" padding: 8px 16px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 14px; font-family: 'Segoe UI', sans-serif; ">Cancel</button> `; const title = hasSelection ? 'Edit Selection' : 'Add Content'; showOverlay(title, editorHtml, footerHtml); // Initialize Ace editor in overlay setTimeout(() => { initializeOverlayEditor(hasSelection); setupEditNavigation(hasSelection); }, 100); } function expandSelectionToIndexItems(range) { if (!globalEditorInstance) return null; const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; const startRow = range.start.row; const endRow = range.end.row; // Build index of ALL sections (markers + foldable) const allSections = []; // 1. Find all markers const lineCount = session.getLength(); for (let row = 0; row < lineCount; row++) { const line = session.getLine(row); if (line.includes('<') && (line.includes('<!--') || line.includes('/*') || line.includes('//'))) { const openMatch = line.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)</); if (openMatch) { const markerName = openMatch[1].trim(); for (let closeRow = row + 1; closeRow < lineCount; closeRow++) { const closeLine = session.getLine(closeRow); if (closeLine.includes('>')) { const closeMatch = closeLine.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)>/); if (closeMatch && closeMatch[1].trim() === markerName) { allSections.push({ startRow: row, endRow: closeRow, length: closeRow - row, type: 'marker', name: markerName }); break; } } } } } } // 2. Find all foldable sections for (let row = 0; row < lineCount; row++) { const foldWidget = session.getFoldWidget(row); if (!foldWidget || foldWidget === '') continue; const foldRange = session.getFoldWidgetRange(row); if (!foldRange) continue; allSections.push({ startRow: foldRange.start.row, endRow: foldRange.end.row, length: foldRange.end.row - foldRange.start.row, type: 'fold', name: session.getLine(foldRange.start.row).trim().substring(0, 40) }); } // 3. Check which sections have BOTH start AND end inside them const fullyContainingSections = allSections.filter(section => section.startRow <= startRow && section.endRow >= endRow ); if (fullyContainingSections.length === 0) { return null; // No sections contain the selection } // 4. Sort by length (shortest first) and pick the smallest fullyContainingSections.sort((a, b) => a.length - b.length); const smallest = fullyContainingSections[0]; // 5. Expand selection to this section selectionRange = new Range( smallest.startRow, 0, smallest.endRow, session.getLine(smallest.endRow).length ); const lines = []; for (let row = smallest.startRow; row <= smallest.endRow; row++) { lines.push(session.getLine(row)); } if (typeof showToast === 'function') { const label = smallest.type === 'marker' ? `marker: ${smallest.name}` : smallest.name; showToast(`📦 Expanded to ${label}...`, 'info', 3000); } return lines.join('\n'); } function findContainingMarker(startRow, endRow) { // This function is no longer needed, integrated above return null; } function initializeOverlayEditor(hasSelection) { const overlayEditorContainer = el.querySelector('#overlayEditor'); if (!overlayEditorContainer) return; overlayEditorInstance = ace.edit(overlayEditorContainer); overlayEditorInstance.setTheme("ace/theme/monokai"); // Match the main editor's mode const currentMode = globalEditorInstance.getSession().getMode().$id; overlayEditorInstance.session.setMode(currentMode); // Set initial content overlayEditorInstance.setValue(editHistory[currentEditIndex], -1); overlayEditorInstance.setOptions({ fontSize: "14px", wrap: true, showPrintMargin: false, useWorker: false, enableAutoIndent: true }); overlayEditorInstance.focus(); } function setupEditNavigation(hasSelection) { const prevBtn = el.querySelector('#prevEditBtn'); const nextBtn = el.querySelector('#nextEditBtn'); const addBtn = el.querySelector('#addEditBtn'); const indexDisplay = el.querySelector('#editIndexDisplay'); const replaceBtn = el.querySelector('#replaceBtn'); const cancelBtn = el.querySelector('#cancelEditBtn'); // Update display updateEditNavigation(); if (prevBtn) { prevBtn.addEventListener('click', () => { if (currentEditIndex > 0) { saveCurrentEdit(); currentEditIndex--; loadCurrentEdit(); updateEditNavigation(); } }); } if (nextBtn) { nextBtn.addEventListener('click', () => { if (currentEditIndex < editHistory.length - 1) { saveCurrentEdit(); currentEditIndex++; loadCurrentEdit(); updateEditNavigation(); } }); } if (addBtn) { addBtn.addEventListener('click', () => { saveCurrentEdit(); editHistory.push(''); currentEditIndex = editHistory.length - 1; loadCurrentEdit(); updateEditNavigation(); }); } if (replaceBtn) { replaceBtn.addEventListener('click', () => handleReplaceSelection(hasSelection)); } if (cancelBtn) { cancelBtn.addEventListener('click', hideOverlay); } } function saveCurrentEdit() { if (overlayEditorInstance) { editHistory[currentEditIndex] = overlayEditorInstance.getValue(); } } function loadCurrentEdit() { if (overlayEditorInstance) { overlayEditorInstance.setValue(editHistory[currentEditIndex], -1); overlayEditorInstance.focus(); } } function updateEditNavigation() { const prevBtn = el.querySelector('#prevEditBtn'); const nextBtn = el.querySelector('#nextEditBtn'); const indexDisplay = el.querySelector('#editIndexDisplay'); const hasMultiple = editHistory.length > 1; if (prevBtn) { prevBtn.style.display = hasMultiple ? 'block' : 'none'; prevBtn.disabled = currentEditIndex === 0; prevBtn.style.opacity = currentEditIndex === 0 ? '0.5' : '1'; } if (nextBtn) { nextBtn.style.display = hasMultiple ? 'block' : 'none'; nextBtn.disabled = currentEditIndex === editHistory.length - 1; nextBtn.style.opacity = currentEditIndex === editHistory.length - 1 ? '0.5' : '1'; } if (indexDisplay) { indexDisplay.style.display = hasMultiple ? 'block' : 'none'; indexDisplay.textContent = `${currentEditIndex + 1} / ${editHistory.length}`; } } function handleReplaceSelection(hasSelection) { if (!globalEditorInstance || !overlayEditorInstance || !selectionRange) return; // Save current edit before applying saveCurrentEdit(); // Combine all edits with line breaks const allContent = editHistory.filter(e => e.trim().length > 0).join('\n\n'); if (hasSelection) { // Replace the selection globalEditorInstance.session.replace(selectionRange, allContent); if (typeof showToast === 'function') { showToast('✅ Selection replaced', 'success'); } } else { // Insert at cursor with line break before const contentToInsert = '\n' + allContent; globalEditorInstance.session.insert(selectionRange.start, contentToInsert); if (typeof showToast === 'function') { showToast('✅ Content added', 'success'); } } hideOverlay(); globalEditorInstance.focus(); } // === INDEX FUNCTIONS === function generateDocumentIndex() { if (!globalEditorInstance) return []; const session = globalEditorInstance.getSession(); const lineCount = session.getLength(); const index = []; for (let row = 0; row < lineCount; row++) { const line = session.getLine(row); const trimmed = line.trim(); if (!trimmed) continue; let label = ''; let icon = ''; let indent = 0; let match; let isMarker = false; // Opening markers - check for < symbol in comments // Matches: <!--anything<-->, /*anything<*/, //anything<, ///anything< if (trimmed.includes('<') && (trimmed.includes('<!--') || trimmed.includes('/*') || trimmed.includes('//'))) { // Extract text before the < symbol let markerMatch = trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)</); if (markerMatch) { label = `[${markerMatch[1].trim()}]`; icon = '🏷️'; indent = 0; isMarker = true; } } // HTML tags else if ((match = trimmed.match(/^<(div|section|nav|header|footer|main|article|aside|button)[^>]*(?:id=["']([^"']+)["']|class=["']([^"']+)["'])?[^>]*>/i))) { const tag = match[1]; const id = match[2]; const className = match[3]; label = id ? `#${id}` : (className ? `.${className.split(' ')[0]}` : `<${tag}>`); icon = '📦'; indent = 0; } // JavaScript functions (function keyword or arrow functions) else if ((match = trimmed.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\([^)]*\)\s*=>))/))) { const funcName = match[1] || match[2]; label = `${funcName}()`; icon = '⚙️'; indent = 1; } // PHP functions else if ((match = trimmed.match(/(?:public|private|protected|static)?\s*function\s+(\w+)\s*\(/))) { label = `${match[1]}()`; icon = '🔧'; indent = 1; } // CSS classes (with dot) else if ((match = trimmed.match(/^\.([a-zA-Z0-9_-]+)\s*\{/))) { label = `.${match[1]}`; icon = '🎨'; indent = 1; } // CSS element selectors (body, html, h1, etc.) else if ((match = trimmed.match(/^(body|html|header|footer|main|section|nav|article|aside|h[1-6]|p|div|span|a|button|input|form)\s*\{/i))) { label = match[1]; icon = '🎨'; indent = 1; } if (label) { index.push({ row, label, icon, indent, isMarker, preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '') }); } } return index; } function findMarkerEnd(startRow, markerName) { if (!globalEditorInstance) return startRow; const session = globalEditorInstance.getSession(); const lineCount = session.getLength(); // Clean marker name for matching const cleanMarker = markerName.replace(/[\[\]]/g, '').trim(); // Look for closing marker with > symbol for (let row = startRow + 1; row < lineCount; row++) { const line = session.getLine(row); // Check if line contains the marker name followed by > if (line.includes('>')) { const closingMatch = line.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)>/); if (closingMatch && closingMatch[1].trim() === cleanMarker) { return row; } } } // If no closing found, just return start row return startRow; } function showIndexOverlay() { const index = generateDocumentIndex(); if (index.length === 0) { showOverlay('Document Index', '<div style="color: #888; text-align: center; padding: 40px;">No sections found in document</div>'); } else { const indexHtml = index.map(item => ` <div class="index-item" data-row="${item.row}" data-is-marker="${item.isMarker}" data-label="${escapeHtml(item.label)}" style=" padding: 10px 12px; margin: 4px 0; background: #3d3d3d; border-radius: 4px; cursor: pointer; transition: background 0.15s; padding-left: ${12 + (item.indent * 20)}px; font-family: 'Segoe UI', monospace; "> <div style="display: flex; align-items: center; gap: 8px;"> <span style="font-size: 16px;">${item.icon}</span> <div style="flex: 1; min-width: 0;"> <div style="color: #e0e0e0; font-weight: 500; font-size: 14px;">${escapeHtml(item.label)}</div> <div style="color: #888; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Line ${item.row + 1}</div> </div> </div> </div> `).join(''); showOverlay('Document Index', indexHtml); // Add hover effect and click handlers setTimeout(() => { const contentEl = el.querySelector('#overlayContent'); if (contentEl) { contentEl.querySelectorAll('.index-item').forEach(item => { item.addEventListener('mouseenter', () => { item.style.background = '#4a5568'; }); item.addEventListener('mouseleave', () => { item.style.background = '#3d3d3d'; }); item.addEventListener('click', () => { const row = parseInt(item.dataset.row); const isMarker = item.dataset.isMarker === 'true'; const label = item.dataset.label; if (isMarker) { navigateToMarker(row, label); } else { navigateToRow(row); } hideOverlay(); }); }); } }, 50); } } function navigateToMarker(startRow, label) { if (!globalEditorInstance) return; const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; // Find the closing marker const endRow = findMarkerEnd(startRow, label); // Select from start marker to end marker (entire section) const range = new Range( startRow, 0, endRow, session.getLine(endRow).length ); globalEditorInstance.selection.setRange(range, false); globalEditorInstance.scrollToLine(startRow, true, true, () => {}); globalEditorInstance.focus(); } function navigateToRow(row) { if (!globalEditorInstance) return; const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; // Get the fold range for this row const foldRange = session.getFoldWidgetRange(row); if (foldRange) { // Select entire fold const extended = new Range( foldRange.start.row, 0, foldRange.end.row, session.getLine(foldRange.end.row).length ); globalEditorInstance.selection.setRange(extended, false); } else { // Just select the line const line = session.getLine(row); const range = new Range(row, 0, row, line.length); globalEditorInstance.selection.setRange(range, false); } globalEditorInstance.scrollToLine(row, true, true, () => {}); globalEditorInstance.focus(); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // === SEARCH FUNCTIONS === function clearMarkers() { if (!globalEditorInstance) return; const session = globalEditorInstance.getSession(); searchState.markers.forEach(id => { try { session.removeMarker(id); } catch (e) {} }); searchState.markers = []; } function markMatches() { if (!globalEditorInstance) return; clearMarkers(); const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; searchState.matches.forEach((m, i) => { const r = new Range(m.r, m.s, m.r, m.e); const cls = i === searchState.idx ? 'ace_selected-word' : 'ace_selection'; const markerId = session.addMarker(r, cls, 'text'); searchState.markers.push(markerId); }); } function searchInEditor(query) { if (!globalEditorInstance || !query) { clearMarkers(); searchState = { matches: [], idx: -1, markers: [] }; updateMatchCounter(); return; } const session = globalEditorInstance.getSession(); const lines = session.getDocument().getAllLines(); const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); searchState.matches = []; searchState.idx = -1; clearMarkers(); lines.forEach((line, r) => { let m; regex.lastIndex = 0; while ((m = regex.exec(line))) { searchState.matches.push({ r, s: m.index, e: m.index + m[0].length }); } }); if (searchState.matches.length) { searchState.idx = 0; gotoMatch(); } updateMatchCounter(); } function gotoMatch() { if (searchState.idx < 0 || !searchState.matches.length || !globalEditorInstance) return; const m = searchState.matches[searchState.idx]; const Range = ace.require('ace/range').Range; const r = new Range(m.r, m.s, m.r, m.e); globalEditorInstance.selection.setRange(r, false); globalEditorInstance.scrollToLine(m.r, true, true, () => {}); markMatches(); updateMatchCounter(); } function nextMatch() { if (!searchState.matches.length) return; searchState.idx = (searchState.idx + 1) % searchState.matches.length; gotoMatch(); } function prevMatch() { if (!searchState.matches.length) return; searchState.idx = (searchState.idx - 1 + searchState.matches.length) % searchState.matches.length; gotoMatch(); } function updateMatchCounter() { const counter = el.querySelector('#matchCounter'); if (counter) { if (searchState.matches.length > 0) { counter.textContent = `${searchState.idx + 1} / ${searchState.matches.length}`; } else { counter.textContent = ''; } } } // === FOLD/SCOPE SELECTION FUNCTIONS === function getAllFoldsForRowTokenAware(targetRow) { if (!globalEditorInstance) return []; const session = globalEditorInstance.getSession(); const lineCount = session.getLength(); const stack = []; const allPairs = []; const isCodeToken = (type) => !/comment|string|regex/i.test(type); for (let row = 0; row < lineCount; row++) { const tokens = session.getTokens(row); let col = 0; for (const tok of tokens) { const { type, value } = tok; if (isCodeToken(type)) { for (let i = 0; i < value.length; i++) { const ch = value[i]; if (ch === '{') stack.push({ row, col: col + i }); else if (ch === '}') { const open = stack.pop(); if (open) { allPairs.push({ startRow: open.row, startCol: open.col, endRow: row, endCol: col + i, }); } } } } col += value.length; } } const cursor = globalEditorInstance.getCursorPosition(); const containsCursor = (p) => { if (cursor.row < p.startRow || cursor.row > p.endRow) return false; if (cursor.row === p.startRow && cursor.column <= p.startCol) return false; if (cursor.row === p.endRow && cursor.column >= p.endCol) return false; return true; }; const filtered = allPairs.filter(containsCursor); filtered.sort((a, b) => (a.endRow - a.startRow) - (b.endRow - b.startRow)); return filtered.map((p) => ({ start: p.startRow, end: p.endRow })); } function selectFold() { if (!globalEditorInstance) return; const pos = globalEditorInstance.getCursorPosition(); const session = globalEditorInstance.getSession(); const R = ace.require('ace/range').Range; if (!lastCursorPos || lastCursorPos.row !== pos.row) { cachedFolds = getAllFoldsForRowTokenAware(pos.row); lastFoldIndex = -1; lastCursorPos = { row: pos.row, column: pos.column }; } if (lastFoldIndex === -1) { const line = session.getLine(pos.row); const range = new R(pos.row, 0, pos.row, line.length); globalEditorInstance.selection.setRange(range, false); globalEditorInstance.focus(); lastFoldIndex = 0; return; } if (lastFoldIndex < cachedFolds.length) { const fold = cachedFolds[lastFoldIndex]; const range = new R(fold.start, 0, fold.end, session.getLine(fold.end).length); globalEditorInstance.selection.setRange(range, false); globalEditorInstance.scrollToLine(fold.start, true, true, () => {}); globalEditorInstance.focus(); lastFoldIndex++; if (lastFoldIndex >= cachedFolds.length) lastFoldIndex = -1; } } loadAce(() => { console.log("[editor.js] Ace script loaded"); requestAnimationFrame(() => { globalEditorInstance = ace.edit(container); globalEditorInstance.setTheme("ace/theme/monokai"); globalEditorInstance.session.setMode("ace/mode/html"); let fileContent = ''; let fileName = 'Untitled'; let detectedMode = 'html'; try { const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); if (active && typeof active.content === 'string') { fileContent = active.content; fileName = active.name; console.log(`[editor.js] Loaded content for ${active.name}`); } } catch (err) { console.warn("[editor.js] Failed to load saved file content:", err); } if (fileName !== 'Untitled') { const ext = fileName.split('.').pop().toLowerCase(); const modeMap = { php: 'php', html: 'html', htm: 'html', js: 'javascript', css: 'css', json: 'json', py: 'python', md: 'markdown', txt: 'text' }; detectedMode = modeMap[ext] || 'html'; globalEditorInstance.session.setMode(`ace/mode/${detectedMode}`); } globalEditorInstance.setValue( fileContent.trim() !== '' ? fileContent : `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> </head> <body> <h1>Hello!</h1> </body> </html>`, -1 ); globalEditorInstance.setOptions({ fontSize: "14px", wrap: true, showPrintMargin: false, useWorker: false, showFoldWidgets: true, foldStyle: 'markbegin', enableAutoIndent: true, readOnly: true, highlightActiveLine: false, highlightGutterLine: false }); globalEditorInstance.commands.removeCommand('toggleFoldWidget'); globalEditorInstance.commands.removeCommand('toggleParentFoldWidget'); globalEditorInstance.getSession().addFold = () => false; globalEditorInstance.on('guttermousedown', function (e) { const target = e.domEvent.target; if (target.classList.contains('ace_fold-widget')) { const row = e.getDocumentPosition().row; const range = globalEditorInstance.getSession().getFoldWidgetRange(row); e.stop(); e.stopPropagation(); e.domEvent.stopPropagation(); e.domEvent.preventDefault(); if (range) { const Range = ace.require('ace/range').Range; const extended = new Range(range.start.row, 0, range.end.row, globalEditorInstance.getSession().getLine(range.end.row).length); globalEditorInstance.selection.setRange(extended, false); globalEditorInstance.scrollToLine(range.start.row, true, true, () => {}); globalEditorInstance.focus(); } return true; } }); globalEditorInstance.getSession().on('change', () => { cachedFolds = []; lastCursorPos = null; lastFoldIndex = -1; debouncedSave(globalEditorInstance); }); globalEditorInstance.on("blur", () => { clearTimeout(saveTimeout); saveToLocalStorage(globalEditorInstance); }); const searchInput = el.querySelector('#editorSearchInput'); const prevBtn = el.querySelector('#searchPrevBtn'); const nextBtn = el.querySelector('#searchNextBtn'); const indexBtn = el.querySelector('#indexBtn'); const editSelectionBtn = el.querySelector('#editSelectionBtn'); const closeOverlayBtn = el.querySelector('#closeOverlayBtn'); const multiOverlay = el.querySelector('#multiOverlay'); if (indexBtn) { indexBtn.addEventListener('click', showIndexOverlay); } if (editSelectionBtn) { editSelectionBtn.addEventListener('click', showEditSelectionOverlay); } if (closeOverlayBtn) { closeOverlayBtn.addEventListener('click', hideOverlay); } if (multiOverlay) { multiOverlay.addEventListener('click', (e) => { if (e.target === multiOverlay) { hideOverlay(); } }); } if (searchInput) { searchInput.addEventListener('input', (e) => searchInEditor(e.target.value)); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? prevMatch() : nextMatch(); } else if (e.key === 'Escape') { searchInput.value = ''; searchInEditor(''); globalEditorInstance.focus(); } }); } if (prevBtn) prevBtn.addEventListener('click', () => { prevMatch(); globalEditorInstance.focus(); }); if (nextBtn) nextBtn.addEventListener('click', () => { nextMatch(); globalEditorInstance.focus(); }); globalEditorInstance.commands.addCommand({ name: 'focusSearch', bindKey: {win: 'Ctrl-F', mac: 'Command-F'}, exec: () => { searchInput?.focus(); searchInput?.select(); } }); globalEditorInstance.commands.addCommand({ name: 'showIndex', bindKey: {win: 'Ctrl-I', mac: 'Command-I'}, exec: showIndexOverlay }); globalEditorInstance.commands.addCommand({ name: 'selectScopeUp', bindKey: {win: 'Alt-Up', mac: 'Alt-Up'}, exec: selectFold }); globalEditorInstance.commands.addCommand({ name: 'selectScopeDown', bindKey: {win: 'Alt-Down', mac: 'Alt-Down'}, exec: selectFold }); fitToOverlayBody(); globalEditorInstance.resize(true); window.addEventListener("resize", () => { fitToOverlayBody(); globalEditorInstance.resize(true); }); }); }); } }; window.AppItems.push(section); if (!window.AppOverlayMenuItems) window.AppOverlayMenuItems = []; window.AppOverlayMenuItems.push({ label: "Toggle Edit Mode", action: () => { if (!globalEditorInstance) return; const isReadOnly = globalEditorInstance.getReadOnly(); globalEditorInstance.setReadOnly(!isReadOnly); globalEditorInstance.setOptions({ highlightActiveLine: !isReadOnly, highlightGutterLine: !isReadOnly }); if (typeof showToast === 'function') { showToast(isReadOnly ? '✏️ Editor now editable' : '🔒 Editor now read-only', 'success'); } } }); window.AppOverlayMenuItems.push({ label: "Add Marker", action: () => { if (!globalEditorInstance) return; const selected = globalEditorInstance.getSelectedText(); if (!selected) { if (typeof showToast === 'function') showToast('⚠️ Select some text first!', 'error'); return; } const markerName = prompt("Enter marker name:"); if (markerName && markerName.trim()) wrapSelectionWithSmartMarker(markerName.trim()); } }); window.AppOverlayMenuItems.push({ label: "Language", submenu: [ { label: "HTML", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/html'); if (typeof showToast === 'function') showToast('✅ Switched to HTML', 'success'); }}}, { label: "PHP", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/php'); if (typeof showToast === 'function') showToast('✅ Switched to PHP', 'success'); }}}, { label: "JavaScript", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/javascript'); if (typeof showToast === 'function') showToast('✅ Switched to JavaScript', 'success'); }}}, { label: "CSS", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/css'); if (typeof showToast === 'function') showToast('✅ Switched to CSS', 'success'); }}}, { label: "JSON", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/json'); if (typeof showToast === 'function') showToast('✅ Switched to JSON', 'success'); }}}, { label: "Markdown", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/markdown'); if (typeof showToast === 'function') showToast('✅ Switched to Markdown', 'success'); }}}, { label: "Python", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/python'); if (typeof showToast === 'function') showToast('✅ Switched to Python', 'success'); }}}, { label: "Plain Text", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/text'); if (typeof showToast === 'function') showToast('✅ Switched to Plain Text', 'success'); }}} ] }); if (window.AppOverlay && typeof window.AppOverlay.close === "function") { const originalClose = window.AppOverlay.close; window.AppOverlay.close = function(...args) { clearTimeout(saveTimeout); if (globalEditorInstance) { const saved = saveToLocalStorage(globalEditorInstance); if (saved && typeof showToast === "function") { const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); if (active) showToast(`💾 Saved ${active.name}`, "success"); } } return originalClose.apply(this, args); }; } } catch (err) { console.error("[editor.js] Fatal error:", err); } })();