Spaces:
Running
Running
| // 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; | |
| } | |