@@ -0,0 +1,385 @@
|
||||
import { create } from "zustand";
|
||||
import type { FileNode } from "../types";
|
||||
import {
|
||||
addPaths,
|
||||
getAllFolderPaths,
|
||||
findNode,
|
||||
deleteNode,
|
||||
addNode,
|
||||
renameNode,
|
||||
} from "../helpers/fileTree";
|
||||
|
||||
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 IDEState {
|
||||
// Файловая система
|
||||
files: FileNode | null;
|
||||
openFiles: FileNode[];
|
||||
activeFile: FileNode | null;
|
||||
expandedFolders: Set<string>;
|
||||
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;
|
||||
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<string>) => void;
|
||||
deleteRoot: () => void;
|
||||
createNewProject: () => void;
|
||||
|
||||
// Поиск
|
||||
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) => void;
|
||||
handleDeleteNode: (node: FileNode) => void;
|
||||
}
|
||||
|
||||
export const useIDEStore = create<IDEState>((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 } = get();
|
||||
if (!openFiles.find((f) => f.path === node.path)) {
|
||||
set((state) => ({ openFiles: [...state.openFiles, node] }));
|
||||
}
|
||||
set({ activeFile: node });
|
||||
}
|
||||
},
|
||||
|
||||
// Обновление содержимого файла
|
||||
updateFileContent: (content: string) => {
|
||||
const { activeFile } = get();
|
||||
if (activeFile) {
|
||||
const updatedFile = { ...activeFile, content };
|
||||
set({ activeFile: updatedFile });
|
||||
set((state) => ({
|
||||
openFiles: state.openFiles.map((f) =>
|
||||
f.path === activeFile.path ? updatedFile : f,
|
||||
),
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
// Закрытие файла
|
||||
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<string>) => {
|
||||
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: "",
|
||||
});
|
||||
},
|
||||
|
||||
// Поиск
|
||||
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: (value: string) => {
|
||||
const { dialog, files, refreshFiles, 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) {
|
||||
refreshFiles(newFiles);
|
||||
}
|
||||
set({ dialog: null });
|
||||
return;
|
||||
}
|
||||
|
||||
let parentPath: string;
|
||||
|
||||
if (!dialog.node) {
|
||||
parentPath = files!.path || files!.name;
|
||||
} 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("/") || files!.path || files!.name;
|
||||
}
|
||||
|
||||
const parentNode = findNode(files!, parentPath);
|
||||
if (
|
||||
parentNode?.children?.some(
|
||||
(c) => c.name.toLowerCase() === value.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
alert(`"${value}" already exists in this folder.`);
|
||||
set({ dialog: null });
|
||||
return;
|
||||
}
|
||||
|
||||
let newFiles: FileNode | null = null;
|
||||
let createdNode: FileNode | null = null;
|
||||
|
||||
if (dialog.type === "newFile") {
|
||||
createdNode = { name: value, type: "file", content: "" };
|
||||
newFiles = addNode(files!, parentPath, createdNode);
|
||||
} else if (dialog.type === "newFolder") {
|
||||
createdNode = { name: value, type: "folder", children: [] };
|
||||
newFiles = addNode(files!, parentPath, createdNode);
|
||||
}
|
||||
|
||||
if (newFiles) {
|
||||
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 (createdNode && createdNode.type === "file") {
|
||||
const findAndOpen = (node: FileNode, name: string): FileNode | null => {
|
||||
if (node.name === name && node.type === "file") return node;
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
const found = findAndOpen(child, name);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const openedFile = findAndOpen(newFiles, value);
|
||||
refreshFiles(newFiles, openedFile || undefined);
|
||||
} else {
|
||||
refreshFiles(newFiles);
|
||||
}
|
||||
}
|
||||
set({ dialog: null });
|
||||
},
|
||||
|
||||
// Удаление узла
|
||||
handleDeleteNode: (node: FileNode) => {
|
||||
const { files, refreshFiles } = get();
|
||||
const isRootNode = node.path === files?.path;
|
||||
if (isRootNode) {
|
||||
get().deleteRoot();
|
||||
} else if (window.confirm(`Delete "${node.name}"?`)) {
|
||||
const newFiles = deleteNode(files!, node.path || node.name);
|
||||
if (newFiles) refreshFiles(newFiles);
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user