/**
* 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
if (CodeMirror.fold.brace) foldFunc = CodeMirror.fold.brace;
else if (CodeMirror.fold.indent) foldFunc = 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) {
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
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);
}
}
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*\(/)) {
label = line.substring(0, 30) + "...";
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 = "🎨";
}
}
// HTML
else if (lang === "html") {
if (line.match(/^\s*<([\w-]+)/)) {
const match = line.match(/<([\w-]+)/);
label = "<" + match[1] + ">";
type = "html-tag";
icon = "📄";
} else if (line.match(/^\s*<script/i)) {
label = "<script>";
type = "script";
icon = "⚙️";
} else if (line.match(/^\s*<style/i)) {
label = "<style>";
type = "style";
icon = "🎨";
}
}
return { type, label, icon, lang };
}
function extractBlocksFromCodeMirror() {
const doc = initCodeMirrorDoc();
if (!doc) return fallbackSimpleParsing();
const foldRanges = getFoldableRanges(doc);
const blocks = [];
console.log(`[editor_index.js] Found ${foldRanges.length} foldable ranges`);
foldRanges.forEach((range) => {
const { startRow, endRow } = range;
const blockInfo = detectBlockType(doc, startRow, endRow);
const code = doc.getRange(
{ line: startRow, ch: 0 },
{ line: endRow, ch: doc.getLine(endRow).length }
);
blocks.push({
row: startRow,
endRow: endRow,
label: blockInfo.label,
type: blockInfo.type,
icon: blockInfo.icon,
lang: blockInfo.lang,
code: code
});
});
return blocks;
}
// =========================================================================
// 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.");
})();