/**
* Editor Index (CodeMirror-Powered Version)
* ------------------------------------------
* - Uses CodeMirror's folding system for accurate block detection
* - Leverages CodeMirror's language-aware parsing
* - Detects functions, classes, blocks properly per language
*/
(function () {
'use strict';
console.log("[editor_index.js] Loading CodeMirror-powered index module...");
const ACTIVE_FILES_KEY = "sftp_active_files";
// =========================================================================
// LOCALSTORAGE HELPERS
// =========================================================================
function getActiveFileContent() {
try {
const files = JSON.parse(localStorage.getItem(ACTIVE_FILES_KEY) || "[]");
const active = files.find(f => f.active);
return {
content: active?.content || "",
name: active?.name || "Untitled",
path: active?.path || ""
};
} catch (err) {
console.error("[editor_index.js] Failed to load file:", err);
return { content: "", name: "Untitled", path: "" };
}
}
function getLines() {
const { content } = getActiveFileContent();
return content.split("\n");
}
function getLine(index) {
const lines = getLines();
return lines[index] || "";
}
function getLineCount() {
return getLines().length;
}
// =========================================================================
// CODEMIRROR SETUP
// =========================================================================
let cmDoc = null;
function initCodeMirrorDoc() {
if (typeof CodeMirror === 'undefined') {
console.warn("[editor_index.js] CodeMirror not loaded, falling back to simple parsing");
return null;
}
const { content, name } = getActiveFileContent();
const mode = detectCodeMirrorMode(name);
// Create a CodeMirror document
cmDoc = CodeMirror.Doc(content, mode);
console.log(`[editor_index.js] CodeMirror doc initialized with mode: ${mode}`);
return cmDoc;
}
function detectCodeMirrorMode(fileName) {
const ext = (fileName.split(".").pop() || "").toLowerCase();
if (["js", "jsx"].includes(ext)) return "javascript";
if (["ts", "tsx"].includes(ext)) return "text/typescript";
if (["html", "htm"].includes(ext)) return "htmlmixed";
if (["php"].includes(ext)) return "php";
if (["css"].includes(ext)) return "css";
if (["json"].includes(ext)) return "application/json";
if (["py"].includes(ext)) return "python";
if (["java"].includes(ext)) return "text/x-java";
if (["cpp", "cc", "cxx"].includes(ext)) return "text/x-c++src";
if (["c"].includes(ext)) return "text/x-csrc";
return "text/plain";
}
function getLanguageFromMode(mode) {
if (!mode) return "text";
if (mode.includes("javascript")) return "javascript";
if (mode.includes("typescript")) return "typescript";
if (mode.includes("html")) return "html";
if (mode.includes("php")) return "php";
if (mode.includes("css")) return "css";
if (mode.includes("json")) return "json";
if (mode.includes("python")) return "python";
if (mode.includes("java")) return "java";
if (mode.includes("c++") || mode.includes("csrc")) return "cpp";
return "text";
}
// =========================================================================
// CODEMIRROR FOLD-BASED BLOCK DETECTION
// =========================================================================
function getFoldableRanges(doc) {
if (!doc || !CodeMirror.fold) {
console.warn("[editor_index.js] CodeMirror fold addon not loaded");
return [];
}
const ranges = [];
const lineCount = doc.lineCount();
// Try to use fold helpers
const mode = doc.getMode();
let foldFunc = null;
// Load appropriate fold function based on mode
if (mode.name === 'xml' || mode.name === 'htmlmixed') {
foldFunc = CodeMirror.fold.xml || CodeMirror.fold.indent;
} else if (mode.name === 'css') {
foldFunc = CodeMirror.fold.brace;
} else {
foldFunc = CodeMirror.fold.brace || CodeMirror.fold.indent;
}
if (!foldFunc) {
console.warn("[editor_index.js] No fold function available");
return [];
}
for (let line = 0; line < lineCount; line++) {
try {
const range = foldFunc(doc, { line, ch: 0 });
if (range && range.from && range.to) {
// Only include ranges that span multiple lines
if (range.to.line > range.from.line) {
ranges.push({
startRow: range.from.line,
endRow: range.to.line,
startCol: range.from.ch,
endCol: range.to.ch
});
}
}
} catch (e) {
// Skip lines that can't fold
}
}
// Remove duplicate ranges and sort by start position
const unique = [];
const seen = new Set();
for (const range of ranges) {
const key = `${range.startRow}-${range.endRow}`;
if (!seen.has(key)) {
seen.add(key);
unique.push(range);
}
}
// Sort by start row
unique.sort((a, b) => a.startRow - b.startRow);
return unique;
}
function detectBlockType(doc, startRow, endRow) {
const line = doc.getLine(startRow).trim();
const mode = doc.getMode();
const lang = getLanguageFromMode(mode.name || mode);
let type = "block";
let label = `Block ${startRow + 1}`;
let icon = "📦";
// JavaScript/TypeScript
if (lang === "javascript" || lang === "typescript") {
if (line.match(/^\s*function\s+(\w+)/)) {
const match = line.match(/function\s+(\w+)/);
label = match[1] + "()";
type = "function";
icon = "⚙️";
} else if (line.match(/^\s*async\s+function\s+(\w+)/)) {
const match = line.match(/function\s+(\w+)/);
label = match[1] + "()";
type = "function";
icon = "⚙️";
} else if (line.match(/^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/)) {
const match = line.match(/(?:const|let|var)\s+(\w+)/);
label = match[1] + "()";
type = "function";
icon = "⚙️";
} else if (line.match(/^\s*(?:class|export\s+class)\s+(\w+)/)) {
const match = line.match(/class\s+(\w+)/);
label = match[1];
type = "class";
icon = "📦";
} else if (line.match(/^\s*(?:if|for|while|switch|try)\s*\(/)) {
const controlMatch = line.match(/^\s*(if|for|while|switch|try)/);
label = controlMatch[1] + " block";
type = "control";
icon = "🔀";
} else if (line.match(/^\s*\{/)) {
label = "{ block }";
type = "block";
icon = "📦";
}
}
// PHP
else if (lang === "php") {
if (line.match(/^\s*function\s+(\w+)/)) {
const match = line.match(/function\s+(\w+)/);
label = match[1] + "()";
type = "function";
icon = "⚙️";
} else if (line.match(/^\s*(?:class|abstract\s+class|final\s+class)\s+(\w+)/)) {
const match = line.match(/class\s+(\w+)/);
label = match[1];
type = "class";
icon = "📦";
}
}
// CSS
else if (lang === "css") {
if (line.match(/^\s*\.([\w-]+)\s*\{/)) {
const match = line.match(/\.([\w-]+)/);
label = "." + match[1];
type = "css-class";
icon = "🎨";
} else if (line.match(/^\s*#([\w-]+)\s*\{/)) {
const match = line.match(/#([\w-]+)/);
label = "#" + match[1];
type = "css-id";
icon = "🎨";
} else if (line.match(/^\s*([\w-]+)\s*\{/)) {
const match = line.match(/([\w-]+)\s*\{/);
label = match[1];
type = "css-tag";
icon = "🎨";
} else if (line.match(/^\s*@media/)) {
label = "@media query";
type = "css-media";
icon = "🎨";
}
}
// HTML
else if (lang === "html") {
if (line.match(/^\s*<([\w-]+)[\s>]/)) {
const match = line.match(/<([\w-]+)/);
const tagName = match[1];
label = `<${tagName}>`;
if (tagName.toLowerCase() === 'script') {
type = "script";
icon = "⚙️";
} else if (tagName.toLowerCase() === 'style') {
type = "style";
icon = "🎨";
} else {
type = "html-tag";
icon = "📄";
}
}
}
return { type, label, icon, lang };
}
function buildHierarchy(ranges, doc, maxDepth = 2) {
// Build tree structure with max depth
const blocks = [];
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
const blockInfo = detectBlockType(doc, range.startRow, range.endRow);
const code = doc.getRange(
{ line: range.startRow, ch: 0 },
{ line: range.endRow, ch: doc.getLine(range.endRow).length }
);
const block = {
row: range.startRow,
endRow: range.endRow,
label: blockInfo.label,
type: blockInfo.type,
icon: blockInfo.icon,
lang: blockInfo.lang,
code: code,
children: [],
depth: 0
};
blocks.push(block);
}
// Build parent-child relationships (max 2 levels)
const roots = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
let parent = null;
// Find the closest parent (smallest range that contains this block)
for (let j = 0; j < blocks.length; j++) {
if (i === j) continue;
const potential = blocks[j];
// Check if potential contains block
if (potential.row < block.row && potential.endRow > block.endRow) {
// Check if this is closer than current parent
if (!parent || (potential.row > parent.row && potential.endRow < parent.endRow)) {
parent = potential;
}
}
}
if (parent && parent.depth < maxDepth - 1) {
// Add as child to parent
block.depth = parent.depth + 1;
parent.children.push(block);
} else {
// This is a root block
block.depth = 0;
roots.push(block);
}
}
// Remove children from root list
const childIds = new Set();
const markChildren = (block) => {
block.children.forEach(child => {
childIds.add(`${child.row}-${child.endRow}`);
markChildren(child);
});
};
roots.forEach(markChildren);
return roots.filter(block => !childIds.has(`${block.row}-${block.endRow}`));
}
function extractBlocksFromCodeMirror() {
const doc = initCodeMirrorDoc();
if (!doc) return fallbackSimpleParsing();
const foldRanges = getFoldableRanges(doc);
console.log(`[editor_index.js] Found ${foldRanges.length} foldable ranges`);
if (foldRanges.length === 0) {
console.warn("[editor_index.js] No foldable ranges found, using fallback");
return fallbackSimpleParsing();
}
// Build hierarchy with max depth of 2
const hierarchicalBlocks = buildHierarchy(foldRanges, doc, 2);
console.log(`[editor_index.js] Built hierarchy with ${hierarchicalBlocks.length} root blocks`);
return hierarchicalBlocks;
}
// =========================================================================
// FALLBACK SIMPLE PARSING
// =========================================================================
function fallbackSimpleParsing() {
console.log("[editor_index.js] Using fallback simple parsing");
const lines = getLines();
const blocks = [];
const CHUNK_SIZE = 30;
for (let i = 0; i < lines.length; i += CHUNK_SIZE) {
const endRow = Math.min(i + CHUNK_SIZE - 1, lines.length - 1);
blocks.push({
row: i,
endRow: endRow,
label: `Lines ${i + 1}-${endRow + 1}`,
type: "chunk",
icon: "📄",
lang: "text",
code: lines.slice(i, endRow + 1).join("\n")
});
}
return blocks;
}
// =========================================================================
// MARKER PARSING
// =========================================================================
function parseMarkerName(name) {
const cleaned = name.replace(/[\[\]]/g, "").trim();
const parts = cleaned.split("_");
if (parts.length === 1)
return { component: parts[0], language: null, number: null, fullName: cleaned };
if (parts.length === 2)
return { component: parts[0], language: parts[1], number: null, fullName: cleaned };
const num = parseInt(parts[2]);
return { component: parts[0], language: parts[1], number: isNaN(num) ? null : num, fullName: cleaned };
}
function findMarkerEnd(startRow, markerName) {
const lineCount = getLineCount();
for (let row = startRow + 1; row < lineCount; row++) {
const line = getLine(row).trim();
if (line.includes(">")) {
const m = line.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*([\w\-\[\]_]+)\s*>/);
if (m && m[1].trim() === markerName) return row;
}
}
return startRow;
}
function extractMarkersAndBlocks() {
const lines = getLines();
const components = {};
const unmarked = [];
const markerRanges = [];
// Find markers
for (let row = 0; row < lines.length; row++) {
const line = lines[row].trim();
const open = line.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*([\w\-\[\]_]+)\s*</);
if (open) {
const markerName = open[1].trim();
const parsed = parseMarkerName(markerName);
const endRow = findMarkerEnd(row, markerName);
const code = lines.slice(row, endRow + 1).join("\n");
const markerItem = {
type: "marker",
row,
endRow,
label: markerName,
parsed,
lang: parsed.language || "text",
code,
children: []
};
markerRanges.push({ startRow: row, endRow, markerItem });
if (parsed.component) {
if (!components[parsed.component]) components[parsed.component] = {};
if (!components[parsed.component][parsed.language || "text"])
components[parsed.component][parsed.language || "text"] = [];
components[parsed.component][parsed.language || "text"].push(markerItem);
} else {
unmarked.push(markerItem);
}
}
}
// Get CodeMirror blocks
const cmBlocks = extractBlocksFromCodeMirror();
// Assign blocks
for (const block of cmBlocks) {
let belongsTo = null;
for (const range of markerRanges) {
if (block.row > range.startRow && block.row < range.endRow) {
belongsTo = range.markerItem;
break;
}
}
if (belongsTo) {
belongsTo.children.push(block);
} else {
unmarked.push(block);
}
}
return { components, unmarked };
}
// =========================================================================
// MAIN INDEX GENERATION
// =========================================================================
function generateDocumentIndex() {
const { components, unmarked } = extractMarkersAndBlocks();
if (Object.keys(components).length === 0 && unmarked.length === 0) {
const fallbackBlocks = fallbackSimpleParsing();
return { components: {}, unmarked: fallbackBlocks };
}
return { components, unmarked };
}
// =========================================================================
// PUBLIC API
// =========================================================================
window.EditorIndex = {
generateDocumentIndex,
initCodeMirrorDoc,
getFoldableRanges,
extractBlocksFromCodeMirror,
parseMarkerName,
getActiveFileContent,
getLines,
getLine,
getLineCount
};
console.log("[editor_index.js] CodeMirror-powered indexer ready.");
})();