// fileStore.js const STORAGE_KEY = "devmate_ide_tree_v2"; /** * Tree node structure: * { * type: "folder" | "file", * name: "src" or "main.py", * path: "src" or "src/main.py", * children: [ ... ] // only for folders * content: "..." // only for files * } */ const defaultTree = { type: "folder", name: "root", path: "", children: [ { type: "file", name: "main.py", path: "main.py", content: "# Python\nprint('Hello from IDE')", }, { type: "folder", name: "src", path: "src", children: [ { type: "file", name: "script.js", path: "src/script.js", content: "console.log('Hello from src');", }, ], }, ], }; // ---------- persistence ---------- export function loadTree() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : defaultTree; } catch (e) { console.warn("loadTree failed:", e); return defaultTree; } } export function saveTree(tree) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(tree)); } catch (e) { console.warn("saveTree failed:", e); } } // ---------- traversal helpers ---------- function clone(obj) { return JSON.parse(JSON.stringify(obj)); } // find node and parent by path export function findNodeAndParent(root, path) { if (path === "" || path == null) return { node: root, parent: null }; const parts = path.split("/").filter(Boolean); let node = root; let parent = null; for (let i = 0; i < parts.length; i++) { const part = parts[i]; parent = node; if (!parent.children) return { node: null, parent: null }; node = parent.children.find((c) => c.name === part); if (!node) return { node: null, parent: null }; } return { node, parent }; } export function getNodeByPath(root, path) { const r = findNodeAndParent(root, path); return r.node || null; } // build child path function joinPath(parentPath, name) { if (!parentPath) return name; if (!name) return parentPath; return parentPath + "/" + name; } // ---------- CRUD operations ---------- // Add file under parentPath (string). If parentPath points to file, use its parent. export function addFile(tree, filename, parentPath = "") { const newTree = clone(tree); // find parent const { node: parent } = findNodeAndParent(newTree, parentPath); const target = parent?.type === "folder" ? parent : newTree; // ensure unique name let name = filename; const exists = (name) => target.children && target.children.some((c) => c.name === name); let idx = 1; const base = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name; const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : ""; while (exists(name)) { name = `${base}_${idx++}${ext}`; } const path = joinPath(target.path, name); const newNode = { type: "file", name, path, content: `// ${name}\n` }; target.children = target.children || []; target.children.push(newNode); return newTree; } // Add folder under parentPath export function addFolder(tree, folderName, parentPath = "") { const newTree = clone(tree); const { node: parent } = findNodeAndParent(newTree, parentPath); const target = parent?.type === "folder" ? parent : newTree; let name = folderName; const exists = (name) => target.children && target.children.some((c) => c.name === name && c.type === "folder"); let idx = 1; while (exists(name)) { name = `${folderName}_${idx++}`; } const path = joinPath(target.path, name); const newNode = { type: "folder", name, path, children: [] }; target.children = target.children || []; target.children.push(newNode); return newTree; } // Delete node (file or folder) at given path (recursive for folders) export function deleteNode(tree, path) { const newTree = clone(tree); if (!path) return newTree; // cannot delete root const parts = path.split("/").filter(Boolean); const nameToDelete = parts.pop(); const parentPath = parts.join("/"); const { node: parent } = findNodeAndParent(newTree, parentPath); if (!parent || !parent.children) return newTree; parent.children = parent.children.filter((c) => c.name !== nameToDelete); return newTree; } // Rename node: change name and update paths recursively for children export function renameNode(tree, path, newName) { const newTree = clone(tree); const { node, parent } = findNodeAndParent(newTree, path); if (!node || !parent) { // special case root rename not allowed if (node && !parent) return newTree; return newTree; } // ensure no duplicate under parent if (parent.children.some((c) => c.name === newName && c.path !== node.path)) { // collision: abort return newTree; } const oldPath = node.path; node.name = newName; const newPath = joinPath(parent.path, newName); // update path recursively function updatePaths(n, currPath) { n.path = currPath; if (n.children) { for (let child of n.children) { const childNewPath = currPath ? currPath + "/" + child.name : child.name; updatePaths(child, childNewPath); } } } updatePaths(node, newPath); return newTree; } // Update file content export function updateFileContent(tree, path, content) { const newTree = clone(tree); const { node } = findNodeAndParent(newTree, path); if (node && node.type === "file") { node.content = content; } return newTree; } // Search within files (returns array of {path, file, excerpt}) export function searchTree(node, term, results = []) { if (!node) return results; if (node.type === "file" && node.content && node.content.includes(term)) { results.push({ path: node.path, file: node.name, excerpt: node.content .split("\n") .filter((l) => l.includes(term)) .slice(0, 5) .join("\n"), }); } if (node.children) { for (const c of node.children) searchTree(c, term, results); } return results; }