📜
overlay_editor_copy6.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
/** * Overlay Editor Module * Handles the edit selection overlay functionality for the Ace editor */ (function() { 'use strict'; console.log("[overlay_editor.js] Loading overlay editor module..."); // ========================================================================= // MODULE STATE // ========================================================================= let overlayEditorInstance = null; // Global per-file overlay state cache window._overlayStates = window._overlayStates || {}; let selectionRange = null; let editHistory = []; let currentEditIndex = 0; // ========================================================================= // DEPENDENCIES (injected from main editor) // ========================================================================= let deps = { getGlobalEditor: null, // Function that returns globalEditorInstance getEl: null, // Function that returns el (DOM element) showOverlay: null, // Function to show overlay hideOverlay: null, // Function to hide overlay generateDocumentIndex: null,// Function to generate document index findMarkerEnd: null, // Function to find marker end escapeHtml: null, // Function to escape HTML showToast: null, // Function to show toast notifications onHideOverlay: null // Callback when overlay is hidden }; // ========================================================================= // INITIALIZATION // ========================================================================= function init(dependencies) { deps = { ...deps, ...dependencies }; console.log("[overlay_editor.js] Initialized with dependencies"); } // ========================================================================= // MAIN OVERLAY FUNCTION // ========================================================================= function showEditSelectionOverlay() { const globalEditorInstance = deps.getGlobalEditor(); if (!globalEditorInstance) { console.error("[overlay_editor.js] Global editor instance not available"); return; } // Get current selection or cursor position const selected = globalEditorInstance.getSelectedText(); const hasInitialSelection = selected && selected.trim().length > 0; let initialContent = ''; if (hasInitialSelection) { // Store the selection range selectionRange = globalEditorInstance.getSelectionRange(); // Check if selection overlaps with any index items and expand if needed const expandedContent = expandSelectionToIndexItems(selectionRange); if (expandedContent) { initialContent = expandedContent.content; } else { initialContent = selected; } } else { // No selection - store cursor position const pos = globalEditorInstance.getCursorPosition(); const Range = ace.require('ace/range').Range; selectionRange = new Range(pos.row, pos.column, pos.row, pos.column); initialContent = ''; } // Initialize edit history with current content editHistory = [initialContent]; currentEditIndex = 0; // Create editor container with section info area const editorHtml = ` <div style=" display: flex; flex-direction: column; gap: 12px; height: calc(100vh - 160px); "> <!-- Section info + actions --> <div id="sectionInfoDisplay" style=" display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #1e293b; border-radius: 4px; "> <span style="font-size: 16px;">❓</span> <span style=" flex: 1; color: #888; font-size: 14px; font-family: 'Segoe UI', sans-serif; font-weight: 500; ">Target: Not detected yet</span> <button id="refreshSectionBtn" style=" padding: 6px 10px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 12px; font-family: 'Segoe UI', sans-serif; ">🔄 Detect</button> <button id="applyBtn" style=" padding: 6px 12px; background: #16a34a; border: 1px solid #15803d; border-radius: 4px; color: #fff; cursor: pointer; font-size: 12px; font-family: 'Segoe UI', sans-serif; font-weight: 600; ">✅ Apply</button> </div> <!-- Edit navigation --> <div style=" display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #1e293b; border-radius: 4px; "> <div style="display: flex; gap: 8px; align-items: center;"> <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=" 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> </div> <button id="addEditBtn" style=" padding: 6px 12px; background: #3b82f6; border: 1px solid #2563eb; border-radius: 4px; color: #fff; cursor: pointer; font-size: 13px; font-family: 'Segoe UI', sans-serif; font-weight: 500; ">+ New</button> </div> <!-- Editor --> <div id="overlayEditor" style=" flex: 1; min-height: 55vh; border: 1px solid #555; border-radius: 6px; background: #0f172a; "></div> </div> `; const footerHtml = ` <button id="applyBtn" 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; ">✅ Apply Changes</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> `; deps.showOverlay('Edit Content', editorHtml, null, false); // Initialize Ace editor in overlay // Try restoring overlay state for the current file const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); const fileKey = active ? active.name : 'default'; if (window._overlayStates[fileKey]) { const state = window._overlayStates[fileKey]; editHistory = state.editHistory || ['']; currentEditIndex = state.currentEditIndex || 0; selectionRange = state.selectionRange || selectionRange; console.log(`[overlay_editor.js] Restored overlay state for ${fileKey}`); } setTimeout(() => { initializeOverlayEditor(); setupEditNavigation(); }, 100); } // ========================================================================= // OVERLAY EDITOR INITIALIZATION // ========================================================================= function initializeOverlayEditor() { const el = deps.getEl(); const globalEditorInstance = deps.getGlobalEditor(); const overlayEditorContainer = el.querySelector('#overlayEditor'); if (!overlayEditorContainer) { console.error("[overlay_editor.js] Could not find #overlayEditor container"); 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(); } // ========================================================================= // NAVIGATION SETUP // ========================================================================= function setupEditNavigation() { const el = deps.getEl(); const prevBtn = el.querySelector('#prevEditBtn'); const nextBtn = el.querySelector('#nextEditBtn'); const addBtn = el.querySelector('#addEditBtn'); const applyBtn = el.querySelector('#applyBtn'); const cancelBtn = el.querySelector('#cancelEditBtn'); const refreshBtn = el.querySelector('#refreshSectionBtn'); // Update display updateEditNavigation(); if (refreshBtn) { refreshBtn.addEventListener('click', () => { detectTargetSection(); }); } 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 (applyBtn) { applyBtn.addEventListener('click', handleApplyChanges); } // Try restoring overlay state for the current file const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); const fileKey = active ? active.name : 'default'; if (window._overlayStates[fileKey]) { const state = window._overlayStates[fileKey]; editHistory = state.editHistory || ['']; currentEditIndex = state.currentEditIndex || 0; selectionRange = state.selectionRange || selectionRange; console.log(`[overlay_editor.js] Restored overlay state for ${fileKey}`); } } // ========================================================================= // EDIT MANAGEMENT // ========================================================================= function saveCurrentEdit() { if (overlayEditorInstance) { editHistory[currentEditIndex] = overlayEditorInstance.getValue(); } } function loadCurrentEdit() { if (overlayEditorInstance) { overlayEditorInstance.setValue(editHistory[currentEditIndex], -1); overlayEditorInstance.focus(); } } function updateEditNavigation() { const el = deps.getEl(); 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}`; } } // ========================================================================= // APPLY CHANGES // ========================================================================= function handleApplyChanges() { const globalEditorInstance = deps.getGlobalEditor(); if (!globalEditorInstance || !overlayEditorInstance) 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 (!allContent.trim()) { if (deps.showToast) { deps.showToast('⚠️ No content to apply', 'error'); } return; } // Check if we have a valid target if (!selectionRange) { if (deps.showToast) { deps.showToast('⚠️ No target. Using cursor position.', 'info'); } // Fallback to cursor const pos = globalEditorInstance.getCursorPosition(); const Range = ace.require('ace/range').Range; selectionRange = new Range(pos.row, pos.column, pos.row, pos.column); } // Check if it's an empty range (insertion) or actual selection (replacement) const Range = ace.require('ace/range').Range; const isEmpty = selectionRange.isEmpty(); if (isEmpty) { // Insert at position const contentToInsert = '\n' + allContent + '\n'; globalEditorInstance.session.insert(selectionRange.start, contentToInsert); if (deps.showToast) { deps.showToast('✅ Content inserted', 'success'); } } else { // Replace the range globalEditorInstance.session.replace(selectionRange, allContent); if (deps.showToast) { deps.showToast('✅ Content replaced', 'success'); } } // Clear state for this file after successful apply const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); const fileKey = active ? active.name : 'default'; delete window._overlayStates[fileKey]; cleanup(); deps.hideOverlay(); deps.showOverlay("Edit Content", editorHtml, null, true); globalEditorInstance.focus(); } // ========================================================================= // SELECTION EXPANSION // ========================================================================= function expandSelectionToIndexItems(range) { const globalEditorInstance = deps.getGlobalEditor(); 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*([\w\-\[\]_]+)\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*([\w\-\[\]_]+)\s*>/); if (closeMatch && closeMatch[1].trim() === markerName) { allSections.push({ startRow: row, endRow: closeRow, length: closeRow - row, type: 'marker', icon: '🏷️', label: `[${markerName}]`, 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; const line = session.getLine(foldRange.start.row).trim(); let icon = '📦'; let label = line.substring(0, 40); // Detect type for icon if (line.match(/^\.([a-zA-Z0-9_-]+)/)) { icon = '🎨'; label = line.match(/^(\.([a-zA-Z0-9_-]+))/)[1]; } else if (line.match(/^(body|html|h[1-6])/)) { icon = '🎨'; label = line.match(/^([a-zA-Z0-9]+)/)[1]; } else if (line.match(/function\s+(\w+)/)) { icon = '⚙️'; const match = line.match(/function\s+(\w+)/); label = match[1] + '()'; } allSections.push({ startRow: foldRange.start.row, endRow: foldRange.end.row, length: foldRange.end.row - foldRange.start.row, type: 'fold', icon: icon, label: label, name: line.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 (deps.showToast) { deps.showToast(`📦 Expanded to ${smallest.label}`, 'info', 3000); } return { content: lines.join('\n'), sectionInfo: smallest }; } // ========================================================================= // TARGET DETECTION // ========================================================================= function detectTargetSection() { const globalEditorInstance = deps.getGlobalEditor(); if (!overlayEditorInstance) { if (deps.showToast) { deps.showToast('⚠️ Overlay editor not initialized', 'error'); } return; } const content = overlayEditorInstance.getValue(); if (!content || !content.trim()) { if (deps.showToast) { deps.showToast('⚠️ No content to analyze', 'error'); } return; } // Get index - handle both hierarchical and flat structures const indexResult = deps.generateDocumentIndex(); let flatIndex = []; // Convert hierarchical structure to flat array if needed if (indexResult && typeof indexResult === 'object' && 'components' in indexResult) { // Hierarchical structure from new index const { components, unmarked } = indexResult; // Flatten components for (const [componentName, languages] of Object.entries(components)) { for (const [language, sections] of Object.entries(languages)) { sections.forEach(section => { flatIndex.push({ ...section, isMarker: true }); }); } } // Add unmarked items flatIndex = flatIndex.concat(unmarked); } else if (Array.isArray(indexResult)) { // Flat array from old index flatIndex = indexResult; } if (flatIndex.length === 0) { // No matches - find smart insertion point findSmartInsertionPoint(content); return; } const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; let candidates = []; // Get lines from overlay content const contentLines = content.split('\n').map(l => l.trim()).filter(l => l.length > 0); const firstContentLine = contentLines[0] || ''; // Score each section in the document for (const item of flatIndex) { let itemStartRow, itemEndRow; if (item.isMarker || item.type === 'marker') { itemStartRow = item.row; itemEndRow = item.endRow || deps.findMarkerEnd(item.row, item.label); } else { const foldRange = session.getFoldWidgetRange(item.row); if (foldRange) { itemStartRow = foldRange.start.row; itemEndRow = foldRange.end.row; } else { itemStartRow = itemEndRow = item.row; } } // Get all lines from this section const itemLines = []; for (let row = itemStartRow; row <= itemEndRow; row++) { const line = session.getLine(row).trim(); if (line.length > 0) { itemLines.push(line); } } // Calculate header match score (70%) const headerScore = calculateHeaderScore(item, firstContentLine, itemLines[0] || ''); // Calculate content match score (30%) const contentScore = calculateContentScore(contentLines, itemLines); // Final score: 70% header + 30% content const finalScore = (headerScore * 0.7) + (contentScore.score * 0.3); candidates.push({ ...item, startRow: itemStartRow, endRow: itemEndRow, score: finalScore, headerScore: headerScore, contentScore: contentScore.score, exactMatches: contentScore.exactMatches, partialMatches: contentScore.partialMatches, totalLines: contentLines.length, size: itemEndRow - itemStartRow }); } if (candidates.length === 0) { findSmartInsertionPoint(content); return; } // Sort by score (descending), then by size (ascending for ties) candidates.sort((a, b) => { if (Math.abs(a.score - b.score) < 0.05) { return a.size - b.size; } return b.score - a.score; }); const bestMatch = candidates[0]; // Check match quality thresholds if (bestMatch.score >= 0.9) { // Excellent match (90%+) - just use it selectionRange = new Range( bestMatch.startRow, 0, bestMatch.endRow, session.getLine(bestMatch.endRow).length ); updateSectionDisplay(bestMatch); } else if (bestMatch.score >= 0.6) { // Good match (60-90%) - show options including the match const cursorPos = globalEditorInstance.getCursorPosition(); const options = []; // Option 1: Best match options.push({ type: 'match', row: bestMatch.startRow, endRow: bestMatch.endRow, label: `Replace ${bestMatch.label}`, detail: `${(bestMatch.score * 100).toFixed(0)}% match`, icon: bestMatch.icon, score: bestMatch.score, isReplacement: true, matchData: bestMatch }); // Option 2: After best match options.push({ type: 'after', row: bestMatch.endRow + 1, label: `After ${bestMatch.label}`, detail: 'Insert new section', icon: '📍', score: bestMatch.score * 0.8, isReplacement: false }); // Option 3: At cursor options.push({ type: 'cursor', row: cursorPos.row, label: `At cursor (line ${cursorPos.row + 1})`, detail: 'Current position', icon: '📍', score: 0, isReplacement: false }); // Set default to best option if (options[0].isReplacement) { selectionRange = new Range( options[0].row, 0, options[0].endRow, session.getLine(options[0].endRow).length ); } else { selectionRange = new Range(options[0].row, 0, options[0].row, 0); } updateSectionDisplay(null, null, { type: 'options', options: options, selectedIndex: 0 }); } else { // Poor match (<60%) - use smart insertion findSmartInsertionPoint(content, bestMatch); } } // ========================================================================= // SMART INSERTION POINT // ========================================================================= function findSmartInsertionPoint(content, poorMatch = null) { const globalEditorInstance = deps.getGlobalEditor(); const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; // Analyze content to determine type const contentLines = content.split('\n'); let contentType = detectContentType(contentLines); // Get cursor position for fallback const cursorPos = globalEditorInstance.getCursorPosition(); const cursorRow = cursorPos.row; // Get index to find nearby sections const indexResult = deps.generateDocumentIndex(); let flatIndex = []; // Convert hierarchical structure to flat array if needed if (indexResult && typeof indexResult === 'object' && 'components' in indexResult) { const { components, unmarked } = indexResult; for (const [componentName, languages] of Object.entries(components)) { for (const [language, sections] of Object.entries(languages)) { sections.forEach(section => { flatIndex.push({ ...section, isMarker: true }); }); } } flatIndex = flatIndex.concat(unmarked); } else if (Array.isArray(indexResult)) { flatIndex = indexResult; } // Strategy 1: Find sections of same content type const matchingTypeSections = flatIndex.filter(item => { if (contentType === 'css' && item.icon === '🎨') return true; if (contentType === 'function' && (item.icon === '⚙️' || item.icon === '🔧')) return true; if (contentType === 'html' && item.icon === '📦') return true; return false; }); // Find the last matching section (or closest to cursor) let smartSection = null; let smartRow = null; if (matchingTypeSections.length > 0) { // Use last section of matching type smartSection = matchingTypeSections[matchingTypeSections.length - 1]; smartRow = smartSection.endRow ? smartSection.endRow + 1 : smartSection.row + 1; } // Strategy 2: Find closest section to cursor let nearestSection = null; let nearestDistance = Infinity; flatIndex.forEach(item => { const itemRow = item.row; const distance = Math.abs(itemRow - cursorRow); if (distance < nearestDistance) { nearestDistance = distance; nearestSection = item; } }); // Prepare insertion options const insertionOptions = []; // Option 1: Smart match based on content type if (smartSection) { insertionOptions.push({ type: 'smart', row: smartRow, label: `After ${smartSection.label}`, icon: smartSection.icon, section: smartSection }); } // Option 2: After nearest section if (nearestSection && (!smartSection || nearestSection.label !== smartSection.label)) { const afterRow = nearestSection.endRow ? nearestSection.endRow + 1 : nearestSection.row + 1; insertionOptions.push({ type: 'nearest', row: afterRow, label: `After ${nearestSection.label}`, icon: nearestSection.icon, section: nearestSection }); } // Option 3: At cursor insertionOptions.push({ type: 'cursor', row: cursorRow, label: `At cursor (line ${cursorRow + 1})`, icon: '📍', section: null }); // Default to first option (smart match or cursor) const defaultOption = insertionOptions[0]; selectionRange = new Range(defaultOption.row, 0, defaultOption.row, 0); updateSectionDisplay(null, poorMatch, { type: 'insertion', contentType: contentType, options: insertionOptions, selectedIndex: 0 }); } function detectContentType(lines) { const content = lines.join('\n').trim(); // Check for CSS if (content.match(/^\.([a-zA-Z0-9_-]+)\s*\{/) || content.match(/^(body|html|h[1-6])\s*\{/i)) { return 'css'; } // Check for function if (content.match(/function\s+(\w+)/) || content.match(/const\s+(\w+)\s*=\s*(?:function|\()/)) { return 'function'; } // Check for HTML if (content.match(/^<\w+/)) { return 'html'; } // Check for PHP if (content.includes('<?php') || content.match(/function\s+\w+\s*\(/)) { return 'php'; } return 'unknown'; } function findClosingBrace(startRow) { const globalEditorInstance = deps.getGlobalEditor(); const session = globalEditorInstance.getSession(); const lineCount = session.getLength(); let braceCount = 0; let foundOpening = false; for (let row = startRow; row < lineCount; row++) { const line = session.getLine(row); for (let i = 0; i < line.length; i++) { if (line[i] === '{') { braceCount++; foundOpening = true; } else if (line[i] === '}') { braceCount--; if (foundOpening && braceCount === 0) { return row; } } } } return startRow; } // ========================================================================= // SCORING HELPERS // ========================================================================= function calculateHeaderScore(item, firstContentLine, firstItemLine) { let score = 0; // Extract normalized names const itemLabel = (item.label || '').replace(/[\[\]()]/g, '').trim().toLowerCase(); const contentHeader = (firstContentLine || '').replace(/[\[\]()<>]/g, '').trim().toLowerCase(); // Small helper for edit distance (Levenshtein) function stringDistance(a, b) { const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0)); for (let i = 0; i <= a.length; i++) dp[i][0] = i; for (let j = 0; j <= b.length; j++) dp[0][j] = j; for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; dp[i][j] = Math.min( dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost ); } } return dp[a.length][b.length]; } // Detect content language from first line const contentLanguage = detectContentLanguage(firstContentLine); // Get item language (from marker name if hierarchical) let itemLanguage = null; if (item.parsed && item.parsed.language) { itemLanguage = item.parsed.language.toLowerCase(); } else if (item.icon === '🎨') { itemLanguage = 'css'; } else if (item.icon === '⚙️' || item.icon === '🔧') { itemLanguage = 'js'; // or php, but we'll check syntax } else if (item.icon === '📦') { itemLanguage = 'html'; } // Language mismatch penalty - if we're confident about both languages and they don't match if (itemLanguage && contentLanguage && itemLanguage !== contentLanguage) { // Strong mismatch: CSS content vs JS function, etc. if ((contentLanguage === 'css' && itemLanguage !== 'css') || (contentLanguage === 'js' && itemLanguage === 'css') || (contentLanguage === 'php' && itemLanguage === 'css')) { return 0; // No match - wrong language } } // --- Marker match --- if (item.isMarker || item.type === 'marker') { const distance = stringDistance(itemLabel, contentHeader); const maxLen = Math.max(itemLabel.length, contentHeader.length); const similarity = 1 - distance / maxLen; // Hard cutoff: must be > 0.9 to be "same header" if (similarity > 0.9) { score = 1.0; } else if (similarity > 0.75) { score = 0.6; } else { score = 0; } } // --- Function header --- else if (item.icon === '⚙️' || item.icon === '🔧') { const funcName = item.label.replace('()', '').trim().toLowerCase(); const match = firstContentLine.match(/function\s+(\w+)/i); const arrowMatch = firstContentLine.match(/(?:const|let|var)\s+(\w+)\s*=/); if (match && match[1].toLowerCase() === funcName) { score = 1.0; } else if (arrowMatch && arrowMatch[1].toLowerCase() === funcName) { score = 1.0; } else if (contentHeader.includes(funcName)) { score = 0.5; } } // --- CSS selectors --- else if (item.icon === '🎨') { const sel = item.label.trim(); const selNormalized = sel.toLowerCase(); if (firstContentLine.startsWith(sel + ' {') || firstContentLine.startsWith(sel + '{')) { score = 1.0; } else if (firstContentLine.match(new RegExp(`^${sel.replace('.', '\\.')}\\s*\\{`, 'i'))) { score = 1.0; } else if (contentHeader.includes(selNormalized)) { score = 0.8; } } // --- HTML tags --- else if (item.icon === '📦') { const tagMatch = firstItemLine.match(/<([a-zA-Z0-9-]+)/); const tagContent = firstContentLine.match(/<([a-zA-Z0-9-]+)/); if (tagMatch && tagContent && tagMatch[1].toLowerCase() === tagContent[1].toLowerCase()) { score = 1.0; } } return score; } function detectContentLanguage(line) { const normalized = line.trim().toLowerCase(); // CSS detection - very specific patterns if (normalized.match(/^\.[\w-]+\s*\{/) || normalized.match(/^(body|html|header|footer|main|section|nav|article|aside|h[1-6]|p|div|span|a|button|input|form)\s*\{/i) || normalized.match(/^[\w-]+\s*:\s*[\w-]+;/)) { return 'css'; } // PHP detection if (normalized.includes('<?php') || normalized.match(/function\s+\w+\s*\([^)]*\)\s*\{/) && normalized.includes('$')) { return 'php'; } // JavaScript detection if (normalized.match(/function\s+\w+\s*\(/) || normalized.match(/(?:const|let|var)\s+\w+\s*=/) || normalized.includes('=>')) { return 'js'; } // HTML detection if (normalized.match(/^<[a-z]+/i)) { return 'html'; } return null; // Unknown } function calculateContentScore(contentLines, itemLines) { let exactMatches = 0; let partialMatches = 0; for (const contentLine of contentLines) { let foundExact = false; let foundPartial = false; for (const itemLine of itemLines) { if (contentLine === itemLine) { foundExact = true; break; } if (contentLine.length > 10 && itemLine.length > 10) { if (itemLine.includes(contentLine) || contentLine.includes(itemLine)) { foundPartial = true; } } } if (foundExact) exactMatches++; else if (foundPartial) partialMatches++; } const totalMatches = exactMatches + (partialMatches * 0.5); const score = totalMatches / contentLines.length; return { score, exactMatches, partialMatches }; } // ========================================================================= // UI UPDATES // ========================================================================= function updateSectionDisplay(bestMatch, poorMatch = null, insertionInfo = null) { const el = deps.getEl(); const sectionDisplay = el.querySelector('#sectionInfoDisplay'); if (!sectionDisplay) return; // Preserve existing buttons const detectBtn = sectionDisplay.querySelector('#refreshSectionBtn'); const applyBtn = sectionDisplay.querySelector('#applyBtn'); // Clear everything sectionDisplay.innerHTML = ''; sectionDisplay.style.display = 'flex'; sectionDisplay.style.flexDirection = 'column'; sectionDisplay.style.gap = '6px'; sectionDisplay.style.padding = '8px 12px'; sectionDisplay.style.background = '#1e293b'; sectionDisplay.style.borderRadius = '4px'; // Create the top row (label + buttons) const topRow = document.createElement('div'); topRow.style.display = 'flex'; topRow.style.alignItems = 'center'; topRow.style.gap = '8px'; const labelEl = document.createElement('span'); labelEl.style.flex = '1'; labelEl.style.fontFamily = `'Segoe UI', sans-serif`; labelEl.style.fontSize = '14px'; labelEl.style.fontWeight = '500'; // --- Determine label text & color --- if (!bestMatch && insertionInfo && insertionInfo.type === 'options') { labelEl.innerHTML = '⚠️ Multiple options found:'; labelEl.style.color = '#fbbf24'; } else if (!bestMatch && insertionInfo && insertionInfo.options) { labelEl.innerHTML = '📍 Choose insertion point:'; labelEl.style.color = '#3b82f6'; } else if (!bestMatch) { if (poorMatch) { labelEl.innerHTML = `❌ No match found <span style="color:#888;font-size:11px;">(best: ${deps.escapeHtml( poorMatch.label )} ${(poorMatch.score * 100).toFixed(0)}%)</span>`; } else { labelEl.innerHTML = '❌ No match found'; } labelEl.style.color = '#ef4444'; } else { const matchInfo = `H:${(bestMatch.headerScore * 100).toFixed(0)}% C:${( bestMatch.contentScore * 100 ).toFixed(0)}%`; labelEl.innerHTML = `🎯 Target: ${deps.escapeHtml(bestMatch.label)} <span style="color:#888;font-size:11px;">(${( bestMatch.score * 100 ).toFixed(0)}% • ${matchInfo})</span>`; labelEl.style.color = '#22c55e'; } // Add label + buttons to the top row topRow.appendChild(labelEl); if (detectBtn) { const newDetect = detectBtn.cloneNode(true); newDetect.addEventListener('click', detectTargetSection); topRow.appendChild(newDetect); } if (applyBtn) { const newApply = applyBtn.cloneNode(true); newApply.addEventListener('click', handleApplyChanges); topRow.appendChild(newApply); } sectionDisplay.appendChild(topRow); // --- Render choice options if present --- if ((!bestMatch && insertionInfo && insertionInfo.type === 'options') || (!bestMatch && insertionInfo && insertionInfo.options)) { const options = insertionInfo.options; const selectedIndex = insertionInfo.selectedIndex || 0; const optionsBox = document.createElement('div'); optionsBox.style.display = 'flex'; optionsBox.style.flexDirection = 'column'; optionsBox.style.gap = '4px'; optionsBox.style.marginTop = '4px'; optionsBox.innerHTML = options.map((opt, idx) => { const scoreInfo = opt.score ? ` • ${(opt.score * 100).toFixed(0)}%` : ''; return ` <label class="insertion-option" data-index="${idx}" style=" display:flex; align-items:center; gap:6px; padding:6px 10px; background:${idx === selectedIndex ? '#4a5568' : '#2d2d2d'}; border-radius:4px; cursor:pointer; transition:background 0.15s; font-family:'Segoe UI',sans-serif; font-size:12px; "> <input type="radio" name="insertionChoice" value="${idx}" ${idx === selectedIndex ? 'checked' : ''} style="accent-color:#3b82f6;"> <span style="font-size:14px;">${opt.icon}</span> <span style="color:#e0e0e0;flex:1;"> ${deps.escapeHtml(opt.label)} ${opt.detail ? `<span style="color:#888;font-size:11px;">${opt.detail}${scoreInfo}</span>` : ''} </span> </label> `; }).join(''); sectionDisplay.appendChild(optionsBox); setTimeout(() => { const globalEditorInstance = deps.getGlobalEditor(); const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; const optionElements = optionsBox.querySelectorAll('.insertion-option'); optionElements.forEach((optEl, idx) => { optEl.addEventListener('click', () => { const option = options[idx]; if (option.isReplacement) { selectionRange = new Range(option.row, 0, option.endRow, session.getLine(option.endRow).length); } else { selectionRange = new Range(option.row, 0, option.row, 0); } updateSectionDisplay(null, poorMatch, { ...insertionInfo, selectedIndex: idx }); if (deps.showToast) deps.showToast(`📍 Selected: ${option.label}`, 'info'); }); }); }, 10); } } // ========================================================================= // CLEANUP // ========================================================================= function cleanup() { if (overlayEditorInstance) { overlayEditorInstance.destroy(); overlayEditorInstance = null; } editHistory = []; currentEditIndex = 0; selectionRange = null; } // ========================================================================= // PUBLIC API // ========================================================================= window.OverlayEditor = { init: init, showEditSelectionOverlay: showEditSelectionOverlay, cleanup: cleanup }; console.log("[overlay_editor.js] Module loaded successfully"); })();