diff --git a/src/components/workspace/Tabs/Tabs.tsx b/src/components/workspace/Tabs/Tabs.tsx index 8f14e07..f6d09a3 100644 --- a/src/components/workspace/Tabs/Tabs.tsx +++ b/src/components/workspace/Tabs/Tabs.tsx @@ -1,52 +1,48 @@ import AppIcon from '@/components/ui/icon'; -import { useWorkspaceActions } from '@/hooks/workspace.hooks'; -import { Tree } from '@/interfaces/workspace.interface'; +import { useFileTab } from '@/hooks'; +import { useProject } from '@/hooks/projectV2.hooks'; import { fileTypeFromFileName } from '@/utility/utils'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import s from './Tabs.module.scss'; -interface Props { - projectId: string; -} +const Tabs: FC = () => { + const { fileTab, open, close, syncTabSettings } = useFileTab(); + const { activeProject } = useProject(); -const Tabs: FC = ({ projectId }) => { - const { openedFiles, openFile, closeFile } = useWorkspaceActions(); - const openedFilesList = openedFiles(projectId); - - const updateActiveTab = (node: Tree) => { - openFile(node.id, projectId); - }; - - const closeTab = (e: React.MouseEvent, id: string) => { + const closeTab = (e: React.MouseEvent, filePath: string) => { e.preventDefault(); e.stopPropagation(); - closeFile(id, projectId); + close(filePath); }; - if (openedFilesList.length === 0) { + useEffect(() => { + syncTabSettings(); + }, [activeProject]); + + if (fileTab.items.length === 0) { return <>; } return (
- {openedFilesList.map((item) => ( + {fileTab.items.map((item) => (
{ - updateActiveTab(item); + open(item.name, item.path); }} className={`${s.item} file-icon ${item.name.split('.').pop()}-lang-file-icon ${fileTypeFromFileName(item.name)}-lang-file-icon - ${item.isOpen ? s.isActive : ''} + ${item.path === fileTab.active ? s.isActive : ''} `} - key={item.id} + key={item.path} > {item.name} { - closeTab(e, item.id); + closeTab(e, item.path); }} > diff --git a/src/components/workspace/WorkSpace/WorkSpace.tsx b/src/components/workspace/WorkSpace/WorkSpace.tsx index 28a1d77..144dea6 100644 --- a/src/components/workspace/WorkSpace/WorkSpace.tsx +++ b/src/components/workspace/WorkSpace/WorkSpace.tsx @@ -201,7 +201,7 @@ const WorkSpace: FC = () => { >
- +
diff --git a/src/components/workspace/project/ManageProject/ManageProject.tsx b/src/components/workspace/project/ManageProject/ManageProject.tsx index 8bb1107..26cc473 100644 --- a/src/components/workspace/project/ManageProject/ManageProject.tsx +++ b/src/components/workspace/project/ManageProject/ManageProject.tsx @@ -10,8 +10,13 @@ import s from './ManageProject.module.scss'; const ManageProject: FC = () => { const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false); - const { projects, setActiveProject, deleteProject, activeProject } = - useProject(); + const { + projects, + setActiveProject, + deleteProject, + activeProject, + loadProjectFiles, + } = useProject(); const projectHeader = () => ( <> @@ -85,14 +90,13 @@ const ManageProject: FC = () => { } }; - const openProject = async (id: Project['id']) => { - if (!id) return; - const selectedProject = id as string; + const openProject = async (selectedProject: Project['id']) => { if (!selectedProject) { await message.error('Project not found'); return; } setActiveProject(selectedProject); + await loadProjectFiles(selectedProject); }; useEffect(() => { diff --git a/src/components/workspace/tree/FileTree/TreeNode.tsx b/src/components/workspace/tree/FileTree/TreeNode.tsx index d42931c..a079f45 100644 --- a/src/components/workspace/tree/FileTree/TreeNode.tsx +++ b/src/components/workspace/tree/FileTree/TreeNode.tsx @@ -1,3 +1,4 @@ +import { useFileTab } from '@/hooks'; import { useLogActivity } from '@/hooks/logActivity.hooks'; import { useProject } from '@/hooks/projectV2.hooks'; import { useWorkspaceActions } from '@/hooks/workspace.hooks'; @@ -5,7 +6,6 @@ import { Project, Tree } from '@/interfaces/workspace.interface'; import { fileTypeFromFileName } from '@/utility/utils'; import { NodeModel } from '@minoru/react-dnd-treeview'; import cn from 'clsx'; -import { useRouter } from 'next/router'; import { FC, useState } from 'react'; import s from './FileTree.module.scss'; import ItemAction, { actionsTypes } from './ItemActions'; @@ -30,11 +30,9 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { const [isEditing, setIsEditing] = useState(false); const [newItemAdd, setNewItemAdd] = useState(''); - const router = useRouter(); - const { id: projectId } = router.query; - - const { openFile, isProjectEditable } = useWorkspaceActions(); + const { isProjectEditable } = useWorkspaceActions(); const { deleteProjectFile, renameProjectFile, newFileFolder } = useProject(); + const { open: openTab } = useFileTab(); const { createLog } = useLogActivity(); const disallowedFile = [ @@ -48,7 +46,7 @@ const TreeNode: FC = ({ node, depth, isOpen, onToggle }) => { e.stopPropagation(); onToggle(node.id); if (!node.droppable) { - openFile(node.id as string, projectId as string); + openTab(node.text, node.data?.path as string); } }; diff --git a/src/hooks/fileTabs.hooks.ts b/src/hooks/fileTabs.hooks.ts new file mode 100644 index 0000000..f517064 --- /dev/null +++ b/src/hooks/fileTabs.hooks.ts @@ -0,0 +1,112 @@ +import fileSystem from '@/lib/fs'; +import { IDEContext, IFileTab } from '@/state/IDE.context'; +import { useContext } from 'react'; + +const useFileTab = () => { + const { fileTab, setFileTab, activeProject } = useContext(IDEContext); + + const syncTabSettings = async (updatedTab?: IFileTab) => { + if (!activeProject) return; + + const defaultSetting = { + tab: { + items: [], + active: null, + }, + }; + + try { + const settingPath = `/${activeProject}/.ide/setting.json`; + if (!(await fileSystem.exists(settingPath))) { + await fileSystem.writeFile( + settingPath, + JSON.stringify(defaultSetting, null, 2), + { + overwrite: true, + }, + ); + } + const setting = (await fileSystem.readFile(settingPath)) as string; + + let parsedSetting = setting ? JSON.parse(setting) : defaultSetting; + + if (updatedTab) { + parsedSetting.tab = updatedTab; + } else { + parsedSetting = { + ...defaultSetting, + ...parsedSetting, + }; + setFileTab(parsedSetting.tab); + } + + await fileSystem.writeFile( + settingPath, + JSON.stringify(parsedSetting, null, 2), + { + overwrite: true, + }, + ); + } catch (error) { + console.error('Error syncing tab settings:', error); + } + }; + + const open = (name: string, path: string) => { + if (fileTab.active === name) return; + + const existingTab = fileTab.items.find((item) => item.path === path); + + if (existingTab) { + const updatedTab = { ...fileTab, active: path }; + setFileTab(updatedTab); + syncTabSettings(updatedTab); + } else { + const newTab = { name, path, isDirty: false }; + const updatedTab = { + ...fileTab, + items: [...fileTab.items, newTab], + active: path, + }; + setFileTab(updatedTab); + syncTabSettings(updatedTab); + } + }; + + const close = (filePath: string, closeAll: boolean = false) => { + let updatedTab: IFileTab; + + if (closeAll) { + updatedTab = { items: [], active: null }; + } else { + const updatedItems = fileTab.items.filter( + (item) => item.path !== filePath, + ); + + let newActiveTab = fileTab.active; + if (fileTab.active === filePath) { + const closedTabIndex = fileTab.items.findIndex( + (item) => item.path === filePath, + ); + if (updatedItems.length > 0) { + if (closedTabIndex > 0) { + newActiveTab = updatedItems[closedTabIndex - 1].path; + } else { + newActiveTab = updatedItems[0].path; + } + } else { + newActiveTab = null; // No more tabs open + } + } + + updatedTab = { items: updatedItems, active: newActiveTab }; + } + + setFileTab(updatedTab); + syncTabSettings(updatedTab); + }; + + return { fileTab, open, close, syncTabSettings }; +}; + +export default useFileTab; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..927a7e9 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export { default as useFileTab } from './fileTabs.hooks'; diff --git a/src/hooks/projectV2.hooks.ts b/src/hooks/projectV2.hooks.ts index df1568f..ff61eb0 100644 --- a/src/hooks/projectV2.hooks.ts +++ b/src/hooks/projectV2.hooks.ts @@ -32,16 +32,6 @@ export const useProject = () => { } = useContext(IDEContext); const baseProjectPath = '/'; - useEffect(() => { - if (activeProject) { - loadProjectFiles(activeProject).catch(() => {}); - } - }, [activeProject]); - - useEffect(() => { - loadProjects().catch(() => {}); - }, []); - const loadProjects = async () => { const projectCollection = await fileSystem.readdir(baseProjectPath, { onlyDir: true, @@ -207,6 +197,15 @@ export const useProject = () => { await loadProjectFiles(activeProject); }; + const updateActiveProject = (projectName: string | null) => { + if (activeProject === projectName) return; + setActiveProject(projectName); + }; + + useEffect(() => { + loadProjects(); + }, []); + return { projects, projectFiles, @@ -217,7 +216,8 @@ export const useProject = () => { deleteProjectFile, moveItem, renameProjectFile, - setActiveProject, + setActiveProject: updateActiveProject, + loadProjectFiles, }; }; diff --git a/src/lib/fs.ts b/src/lib/fs.ts index ab13b9d..cacfd09 100644 --- a/src/lib/fs.ts +++ b/src/lib/fs.ts @@ -7,6 +7,9 @@ class FileSystem { } async readFile(path: string) { + if (!(await this.exists(path))) { + throw new Error(`File not found: ${path}`); + } return this.fs.readFile(path, 'utf8'); } @@ -17,8 +20,13 @@ class FileSystem { * @param data - The content to be written to the file. Can be a string or Uint8Array. * @returns A promise that resolves once the file has been written. */ - async writeFile(path: string, data: string | Uint8Array) { - const finalPath = await this.getUniquePath(path); + async writeFile( + path: string, + data: string | Uint8Array, + options?: { overwrite?: boolean }, + ) { + const { overwrite } = options ?? {}; + const finalPath = overwrite ? path : await this.getUniquePath(path); await this.ensureDirectoryExists(finalPath); return this.fs.writeFile(finalPath, data); } diff --git a/src/state/IDE.context.tsx b/src/state/IDE.context.tsx index 0f04712..571d5fc 100644 --- a/src/state/IDE.context.tsx +++ b/src/state/IDE.context.tsx @@ -1,5 +1,16 @@ import { Tree } from '@/interfaces/workspace.interface'; -import { FC, createContext, useEffect, useState } from 'react'; +import { FC, createContext, useEffect, useMemo, useState } from 'react'; + +interface ITabItems { + name: string; + path: string; + isDirty: boolean; +} + +export interface IFileTab { + items: ITabItems[]; + active: string | null; +} interface IDEContextProps { projects: string[]; @@ -8,39 +19,47 @@ interface IDEContextProps { setProjectFiles: (files: Tree[]) => void; activeProject: string | null; setActiveProject: (project: string | null) => void; - tabs: string[]; - setTabs: (tabs: string[]) => void; + fileTab: IFileTab; + setFileTab: (fileTab: IFileTab) => void; } -export const IDEContext = createContext({ +const defaultState = { projects: [], projectFiles: [], setProjectFiles: () => {}, setProjects: () => {}, activeProject: null, setActiveProject: () => {}, - tabs: [], - setTabs: () => {}, -}); + fileTab: { + items: [], + active: null, + }, + setFileTab: () => {}, +}; + +export const IDEContext = createContext(defaultState); export const IDEProvider: FC<{ children: React.ReactNode }> = ({ children, }) => { const [projects, setProjects] = useState([]); const [projectFiles, setProjectFiles] = useState([]); - const [tabs, setTabs] = useState([]); + const [fileTab, setFileTab] = useState(defaultState.fileTab); const [activeProject, setActiveProject] = useState(null); - const value = { - projects, - setProjects, - projectFiles, - setProjectFiles, - activeProject, - setActiveProject, - tabs, - setTabs, - }; + const value = useMemo( + () => ({ + projects, + setProjects, + projectFiles, + setProjectFiles, + activeProject, + setActiveProject, + fileTab, + setFileTab, + }), + [activeProject, projects, projectFiles, fileTab], + ); const onInit = () => { const storedActiveProject = localStorage.getItem('IDE_activeProject');