/**
* 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;
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; gap: 12px; height: 400px;">
<div style="flex: 1; display: flex; flex-direction: column; gap: 8px;">
<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: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
</div>
<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 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, footerHtml);
// Initialize Ace editor in overlay
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);
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
cleanup();
deps.hideOverlay();
});
}
}
// =========================================================================
// 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');
}
}
cleanup();
deps.hideOverlay();
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;
if (!bestMatch && insertionInfo && insertionInfo.type === 'options') {
// Show multiple options for 60-90% matches
const options = insertionInfo.options;
const selectedIndex = insertionInfo.selectedIndex || 0;
const selectedOption = options[selectedIndex];
let optionsHtml = options.map((opt, idx) => {
const scoreInfo = opt.score > 0 ? ` • ${(opt.score * 100).toFixed(0)}%` : '';
return `
<label 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;
" class="insertion-option" data-index="${idx}">
<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)}
<span style="color: #888; font-size: 11px;">${opt.detail}${scoreInfo}</span>
</span>
</label>
`;
}).join('');
sectionDisplay.innerHTML = `
<div style="flex: 1; display: flex; flex-direction: column; gap: 4px;">
<div style="
color: #fbbf24;
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
">⚠️ Multiple options found:</div>
${optionsHtml}
</div>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
align-self: flex-start;
">🔄 Detect</button>
`;
// Attach option selection handlers
setTimeout(() => {
const globalEditorInstance = deps.getGlobalEditor();
const session = globalEditorInstance.getSession();
const Range = ace.require('ace/range').Range;
const optionElements = sectionDisplay.querySelectorAll('.insertion-option');
optionElements.forEach((optEl, idx) => {
optEl.addEventListener('mouseenter', () => {
if (idx !== selectedIndex) {
optEl.style.background = '#3d3d3d';
}
});
optEl.addEventListener('mouseleave', () => {
if (idx !== selectedIndex) {
optEl.style.background = '#2d2d2d';
}
});
optEl.addEventListener('click', () => {
const option = options[idx];
// Update selection range based on option type
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);
}
// Re-render with new selection
updateSectionDisplay(null, poorMatch, {
...insertionInfo,
selectedIndex: idx
});
if (deps.showToast) {
deps.showToast(`📍 Selected: ${option.label}`, 'info');
}
});
});
}, 10);
if (deps.showToast && insertionInfo.selectedIndex === undefined) {
deps.showToast(`⚠️ ${(options[0].score * 100).toFixed(0)}% match - choose action`, 'warning');
}
}
else if (!bestMatch && insertionInfo && insertionInfo.options) {
// Smart insertion mode with options (from findSmartInsertionPoint)
const options = insertionInfo.options;
const selectedIndex = insertionInfo.selectedIndex || 0;
const selectedOption = options[selectedIndex];
let optionsHtml = options.map((opt, idx) => `
<label 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;
" class="insertion-option" data-index="${idx}">
<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;">${deps.escapeHtml(opt.label)}</span>
</label>
`).join('');
sectionDisplay.innerHTML = `
<div style="flex: 1; display: flex; flex-direction: column; gap: 4px;">
<div style="
color: #3b82f6;
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
">📍 Choose insertion point:</div>
${optionsHtml}
</div>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
align-self: flex-start;
">🔄 Detect</button>
`;
// Attach option selection handlers
setTimeout(() => {
const optionElements = sectionDisplay.querySelectorAll('.insertion-option');
optionElements.forEach((optEl, idx) => {
optEl.addEventListener('mouseenter', () => {
if (idx !== selectedIndex) {
optEl.style.background = '#3d3d3d';
}
});
optEl.addEventListener('mouseleave', () => {
if (idx !== selectedIndex) {
optEl.style.background = '#2d2d2d';
}
});
optEl.addEventListener('click', () => {
const globalEditorInstance = deps.getGlobalEditor();
const Range = ace.require('ace/range').Range;
// Update selection range
const option = options[idx];
selectionRange = new Range(option.row, 0, option.row, 0);
// Re-render with new selection
updateSectionDisplay(null, poorMatch, {
...insertionInfo,
selectedIndex: idx
});
if (deps.showToast) {
deps.showToast(`📍 Will insert ${option.label}`, 'info');
}
});
});
}, 10);
if (deps.showToast && insertionInfo.selectedIndex === undefined) {
deps.showToast(`📍 Choose where to insert content`, 'info');
}
} else if (!bestMatch && insertionInfo) {
// Legacy fallback (shouldn't hit this anymore)
sectionDisplay.innerHTML = `
<span style="font-size: 16px;">📍</span>
<span style="
flex: 1;
color: #3b82f6;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
">Insert after line ${insertionInfo.row + 1} <span style="color: #888; font-size: 11px;">(${insertionInfo.contentType})</span></span>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
`;
if (deps.showToast) {
deps.showToast(`📍 Will insert after line ${insertionInfo.row + 1}`, 'info');
}
} else if (!bestMatch) {
// No good match
const poorInfo = poorMatch
? `<span style="color: #888; font-size: 11px;">(best: ${deps.escapeHtml(poorMatch.label)} ${(poorMatch.score * 100).toFixed(0)}%)</span>`
: '';
sectionDisplay.innerHTML = `
<span style="font-size: 16px;">❌</span>
<span style="
flex: 1;
color: #ef4444;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
">No match found ${poorInfo}</span>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
`;
if (deps.showToast) {
deps.showToast('❌ No good match found', 'error');
}
} else {
// Good match (90%+)
const matchInfo = `H:${(bestMatch.headerScore * 100).toFixed(0)}% C:${(bestMatch.contentScore * 100).toFixed(0)}%`;
sectionDisplay.innerHTML = `
<span style="font-size: 16px;">${bestMatch.icon}</span>
<span style="
flex: 1;
color: #22c55e;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
">Target: ${deps.escapeHtml(bestMatch.label)} <span style="color: #888; font-size: 11px;">(${(bestMatch.score * 100).toFixed(0)}% • ${matchInfo})</span></span>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
`;
if (deps.showToast) {
deps.showToast(`🎯 Target: ${bestMatch.label} (${(bestMatch.score * 100).toFixed(0)}%)`, 'success');
}
}
// Re-attach event listener for detect button
const refreshBtn = sectionDisplay.querySelector('#refreshSectionBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', detectTargetSection);
}
}
// =========================================================================
// 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");
})();