import { create } from "zustand"; import type { FileNode } from "../types"; import { addPaths, getAllFolderPaths, findNode, deleteNode, addNode, renameNode, } from "../helpers/fileTree"; import { scriptsApi } from "../api/scripts.api"; export const initialFiles: FileNode = { name: "my-project", type: "folder", children: [ { name: "src", type: "folder", children: [ { name: "main.py", type: "file", content: 'print("Hello, World!")\n\ndef main():\n print("Welcome!")\n\nif __name__ == "__main__":\n main()', }, { name: "utils.py", type: "file", content: "def helper():\n return 42", }, ], }, { name: "README.md", type: "file", content: "# My Project\n\nWelcome!", }, ], }; interface IDEFileNode extends FileNode { dirty?: boolean; } interface IDEState { // Файловая система files: FileNode | null; openFiles: IDEFileNode[]; activeFile: IDEFileNode | null; expandedFolders: Set; searchQuery: string; showSearch: boolean; isInitialized: boolean; // Диалоги и контекстные меню contextMenu: { x: number; y: number; node: FileNode | null } | null; dialog: { type: "newFile" | "newFolder" | "rename"; node: FileNode | null; } | null; tabContextMenu: { x: number; y: number; file: FileNode } | null; // Действия с файлами selectFile: (node: FileNode) => void; updateFileContent: (content: string) => void; saveActiveFile: () => Promise; closeFile: (file: FileNode) => void; closeAllFiles: () => void; closeOtherFiles: (file: FileNode) => void; // Действия с деревом refreshFiles: (newFiles: FileNode | null, newFile?: FileNode) => void; toggleFolder: (path: string) => void; expandAllFolders: () => void; collapseAllFolders: () => void; autoExpandPaths: (paths: Set) => void; deleteRoot: () => void; createNewProject: () => void; // API методы fetchTree: () => Promise; createScript: (payload: { content: string; interpreter_id: number; path: string; }) => Promise; updateScript: ( id: number, payload: { content: string; interpreter_id: number; path: string }, ) => Promise; deleteScript: (id: number) => Promise; saveActiveFile: () => Promise; // Поиск setSearchQuery: (query: string) => void; toggleSearch: () => void; // Контекстные меню и диалоги setContextMenu: ( menu: { x: number; y: number; node: FileNode | null } | null, ) => void; setDialog: ( dialog: { type: "newFile" | "newFolder" | "rename"; node: FileNode | null; } | null, ) => void; setTabContextMenu: ( menu: { x: number; y: number; file: FileNode } | null, ) => void; // Инициализация initialize: (initialFiles: FileNode) => void; // Диалог подтверждения handleDialogConfirm: (value: string) => Promise; handleDeleteNode: (node: FileNode) => Promise; } export const useIDEStore = create((set, get) => ({ // Начальное состояние files: null, openFiles: [], activeFile: null, expandedFolders: new Set(), searchQuery: "", showSearch: false, isInitialized: false, contextMenu: null, dialog: null, tabContextMenu: null, // Инициализация initialize: (initialFiles: FileNode) => { const filesWithPaths = addPaths(initialFiles); set({ files: filesWithPaths, expandedFolders: new Set([filesWithPaths.path || filesWithPaths.name]), isInitialized: true, }); }, // Выбор файла selectFile: (node: FileNode) => { if (node.type === "file") { const { openFiles, files } = get(); // Берём актуальную версию из дерева файлов const latestFile = files ? findNode(files, node.path || "") : null; const fileToOpen = latestFile && latestFile.type === "file" ? latestFile : node; if (!openFiles.find((f) => f.path === fileToOpen.path)) { set((state) => ({ openFiles: [...state.openFiles, fileToOpen] })); } set({ activeFile: fileToOpen }); } }, // Обновление содержимого файла updateFileContent: (content: string) => { const { activeFile, files } = get(); if (activeFile && files) { const updatedFile = { ...activeFile, content, dirty: true }; set({ activeFile: updatedFile }); set((state) => ({ openFiles: state.openFiles.map((f) => f.path === activeFile.path ? updatedFile : f, ), })); // Обновляем также в дереве файлов const updateFileInTree = (node: FileNode): FileNode => { if (node.path === activeFile.path) { return updatedFile; } if (node.children) { return { ...node, children: node.children.map((child) => updateFileInTree(child)), }; } return node; }; set({ files: updateFileInTree(files) }); } }, // Закрытие файла closeFile: (file: FileNode) => { const { openFiles, activeFile } = get(); const newOpenFiles = openFiles.filter((f) => f.path !== file.path); set({ openFiles: newOpenFiles }); if (activeFile?.path === file.path) { set({ activeFile: newOpenFiles[newOpenFiles.length - 1] || null }); } }, // Закрыть все файлы closeAllFiles: () => { set({ openFiles: [], activeFile: null }); }, // Закрыть другие файлы closeOtherFiles: (file: FileNode) => { set({ openFiles: [file], activeFile: file }); }, // Обновить файловую систему refreshFiles: (newFiles: FileNode | null, newFile?: FileNode) => { const { openFiles, activeFile, selectFile } = get(); set({ files: newFiles }); if (!newFiles) { set({ openFiles: [], activeFile: null }); return; } const updatedOpenFiles = openFiles .map((f) => { const found = findNode(newFiles, f.path || ""); return found && found.type === "file" ? found : null; }) .filter((f): f is FileNode => f !== null); set({ openFiles: updatedOpenFiles }); if (newFile) { selectFile(newFile); } else if (activeFile) { const stillExists = findNode(newFiles, activeFile.path || ""); if (!stillExists) { set({ activeFile: updatedOpenFiles[updatedOpenFiles.length - 1] || null, }); } else if (stillExists.type === "file") { set({ activeFile: stillExists }); } } }, // Переключить папку toggleFolder: (path: string) => { set((state) => { const newSet = new Set(state.expandedFolders); if (newSet.has(path)) { newSet.delete(path); } else { newSet.add(path); } return { expandedFolders: newSet }; }); }, // Раскрыть все папки expandAllFolders: () => { const { files } = get(); if (files) { set({ expandedFolders: new Set(getAllFolderPaths(files)) }); } }, // Свернуть все папки collapseAllFolders: () => { set({ expandedFolders: new Set() }); }, // Автоматически раскрыть пути autoExpandPaths: (paths: Set) => { set((state) => ({ expandedFolders: new Set([...state.expandedFolders, ...paths]), })); }, // Удалить корень deleteRoot: () => { set({ files: null, openFiles: [], activeFile: null, expandedFolders: new Set(), }); }, // Создать новый проект createNewProject: () => { const newProject = addPaths(initialFiles); set({ files: newProject, expandedFolders: new Set([newProject.path || newProject.name]), searchQuery: "", }); }, // API: загрузка дерева с сервера fetchTree: async () => { try { const data = await scriptsApi.getTree(); const convertItem = (item: any): FileNode => { const node: FileNode = { id: item.id, name: item.name, type: item.type === "folder" ? "folder" : "file", content: item.content || "", path: item.name, interpreter_id: item.interpreter_id, }; if (item.type === "folder") { node.children = []; if (item.children && Array.isArray(item.children)) { node.children = item.children.map((child: any) => { const childNode = convertItem(child); childNode.path = `${item.name}/${child.name}`; return childNode; }); } } return node; }; const roots = data.map((item) => convertItem(item)); set({ files: { name: "scripts", type: "folder", children: roots, }, expandedFolders: new Set(), isInitialized: true, }); } catch (e) { console.error("Failed to fetch tree:", e); throw e; } }, // API: создание скрипта createScript: async (payload) => { try { await scriptsApi.createScript(payload); await get().fetchTree(); } catch (e) { console.error("Failed to create script:", e); throw e; } }, // API: обновление скрипта updateScript: async (id, payload) => { try { await scriptsApi.updateScript(id, payload); } catch (e) { console.error("Failed to update script:", e); throw e; } }, // API: удаление скрипта deleteScript: async (id) => { try { await scriptsApi.deleteScript(id); await get().fetchTree(); } catch (e) { console.error("Failed to delete script:", e); throw e; } }, // API: сохранение активного файла saveActiveFile: async () => { const { activeFile } = get(); if (!activeFile || !activeFile.id) return; try { await scriptsApi.updateScript(activeFile.id, { content: activeFile.content || "", interpreter_id: activeFile.interpreter_id || 0, path: activeFile.path || "", }); set((state) => ({ activeFile: state.activeFile ? { ...state.activeFile, dirty: false } : null, openFiles: state.openFiles.map((f) => f.path === state.activeFile?.path ? { ...f, dirty: false } : f, ), })); } catch (e) { console.error("Failed to save file:", e); } }, // Поиск setSearchQuery: (query: string) => { set({ searchQuery: query }); }, toggleSearch: () => { set((state) => ({ showSearch: !state.showSearch })); }, // Контекстные меню и диалоги setContextMenu: (menu) => set({ contextMenu: menu }), setDialog: (dialog) => set({ dialog: dialog }), setTabContextMenu: (menu) => set({ tabContextMenu: menu }), // Подтверждение диалога handleDialogConfirm: async (value: string) => { const { dialog, files, toggleFolder, autoExpandPaths } = get(); if (!dialog) return; if (dialog.type === "rename" && dialog.node) { const parentPath = dialog.node.path?.split("/").slice(0, -1).join("/") || ""; const parentNode = parentPath ? findNode(files!, parentPath) : files; if ( parentNode?.children?.some( (c) => c.name.toLowerCase() === value.toLowerCase() && c.path !== dialog.node?.path, ) ) { alert(`"${value}" already exists.`); return; } const newFiles = renameNode( files!, dialog.node.path || dialog.node.name, value, ); if (newFiles) { set({ files: newFiles }); } set({ dialog: null }); return; } let parentPath: string; if (!dialog.node) { parentPath = ""; } else if (dialog.node.type === "folder") { parentPath = dialog.node.path || dialog.node.name; } else { const pathParts = (dialog.node.path || dialog.node.name).split("/"); pathParts.pop(); parentPath = pathParts.join("/"); } const fullPath = parentPath ? `${parentPath}/${value}` : value; try { const result = await scriptsApi.createScript({ content: "", interpreter_id: 0, path: fullPath, }); await get().fetchTree(); const allParentPaths: string[] = []; let current = parentPath; while (current) { allParentPaths.push(current); const parts = current.split("/"); parts.pop(); current = parts.join("/"); } allParentPaths.forEach((p) => { if (!get().expandedFolders.has(p)) { toggleFolder(p); } }); autoExpandPaths(new Set(allParentPaths)); if (dialog.type === "newFile") { const createdNode: FileNode = { id: result.id, name: value, type: "file", content: result.content, path: result.path, interpreter_id: result.interpreter_id, }; get().selectFile(createdNode); } } catch (e) { console.error("Failed to create:", e); } set({ dialog: null }); }, // Удаление узла handleDeleteNode: async (node: FileNode) => { const { files } = get(); const isRootNode = node.path === files?.path; if (isRootNode) { get().deleteRoot(); } else if (window.confirm(`Delete "${node.name}"?`)) { if (node.id) { try { await get().deleteScript(node.id); } catch (e) { console.error("Failed to delete:", e); } } } }, }));