import { writable, derived, get } from "svelte/store"; import { invoke } from "@tauri-apps/api/core"; import type { EditorState, EditorTab, FileEntry } from "$lib/types/editor"; const defaultState: EditorState = { tabs: [], activeTabId: null, isFileBrowserOpen: true, currentDirectory: "", fileTree: [], isLoadingTree: false, }; function getLanguageFromPath(filePath: string): string { const ext = filePath.split(".").pop()?.toLowerCase() || ""; const languageMap: Record = { js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript", py: "python", rs: "rust", go: "go", java: "java", c: "c", cpp: "cpp", h: "c", hpp: "cpp", cs: "csharp", rb: "ruby", php: "php", swift: "swift", kt: "kotlin", scala: "scala", html: "html", htm: "html", css: "css", scss: "scss", sass: "sass", less: "less", json: "json", xml: "xml", yaml: "yaml", yml: "yaml", toml: "toml", md: "markdown", markdown: "markdown", sql: "sql", sh: "shell", bash: "shell", zsh: "shell", ps1: "powershell", dockerfile: "dockerfile", svelte: "svelte", vue: "vue", graphql: "graphql", gql: "graphql", lua: "lua", r: "r", dart: "dart", elm: "elm", ex: "elixir", exs: "elixir", erl: "erlang", hs: "haskell", clj: "clojure", lisp: "lisp", ml: "ocaml", fs: "fsharp", zig: "zig", nim: "nim", v: "v", wasm: "wasm", wat: "wasm", }; return languageMap[ext] || "plaintext"; } function generateTabId(): string { return `tab-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } function createEditorStore() { const state = writable(defaultState); const isEditorVisible = writable(false); const saveError = writable(null); async function loadDirectory(dirPath: string): Promise { try { const entries = await invoke("list_directory", { path: dirPath }); return entries.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); } catch (error) { console.error("Failed to load directory:", error); return []; } } async function initializeFileTree(rootPath: string) { state.update((s) => ({ ...s, isLoadingTree: true, currentDirectory: rootPath })); try { const entries = await loadDirectory(rootPath); state.update((s) => ({ ...s, fileTree: entries, isLoadingTree: false })); } catch (error) { console.error("Failed to initialize file tree:", error); state.update((s) => ({ ...s, isLoadingTree: false })); } } async function toggleDirectory(entry: FileEntry) { if (!entry.isDirectory) return; state.update((s) => { const updateTree = (entries: FileEntry[]): FileEntry[] => { return entries.map((e) => { if (e.path === entry.path) { return { ...e, isExpanded: !e.isExpanded, isLoading: !e.isExpanded && !e.children }; } if (e.children) { return { ...e, children: updateTree(e.children) }; } return e; }); }; return { ...s, fileTree: updateTree(s.fileTree) }; }); if (!entry.isExpanded && !entry.children) { const children = await loadDirectory(entry.path); state.update((s) => { const updateTree = (entries: FileEntry[]): FileEntry[] => { return entries.map((e) => { if (e.path === entry.path) { return { ...e, children, isLoading: false }; } if (e.children) { return { ...e, children: updateTree(e.children) }; } return e; }); }; return { ...s, fileTree: updateTree(s.fileTree) }; }); } } async function openFile(filePath: string) { const currentState = get(state); const existingTab = currentState.tabs.find((t) => t.filePath === filePath); if (existingTab) { state.update((s) => ({ ...s, activeTabId: existingTab.id })); return; } try { const content = await invoke("read_file_content", { path: filePath }); const fileName = filePath.split(/[/\\]/).pop() || "untitled"; const language = getLanguageFromPath(filePath); const newTab: EditorTab = { id: generateTabId(), filePath, fileName, content, originalContent: content, isDirty: false, language, }; state.update((s) => ({ ...s, tabs: [...s.tabs, newTab], activeTabId: newTab.id, })); } catch (error) { console.error("Failed to open file:", error); saveError.set(`Failed to open file: ${error}`); } } async function saveFile(tabId?: string) { const currentState = get(state); const tab = tabId ? currentState.tabs.find((t) => t.id === tabId) : currentState.tabs.find((t) => t.id === currentState.activeTabId); if (!tab) return; saveError.set(null); try { await invoke("write_file_content", { path: tab.filePath, content: tab.content }); state.update((s) => ({ ...s, tabs: s.tabs.map((t) => t.id === tab.id ? { ...t, originalContent: t.content, isDirty: false } : t ), })); } catch (error) { console.error("Failed to save file:", error); saveError.set(`Failed to save file: ${error}`); throw error; } } function updateTabContent(tabId: string, content: string) { state.update((s) => ({ ...s, tabs: s.tabs.map((t) => t.id === tabId ? { ...t, content, isDirty: content !== t.originalContent } : t ), })); } function closeTab(tabId: string) { state.update((s) => { const tabIndex = s.tabs.findIndex((t) => t.id === tabId); const newTabs = s.tabs.filter((t) => t.id !== tabId); let newActiveId = s.activeTabId; if (s.activeTabId === tabId) { if (newTabs.length === 0) { newActiveId = null; } else if (tabIndex >= newTabs.length) { newActiveId = newTabs[newTabs.length - 1].id; } else { newActiveId = newTabs[tabIndex].id; } } return { ...s, tabs: newTabs, activeTabId: newActiveId }; }); } function setActiveTab(tabId: string) { state.update((s) => ({ ...s, activeTabId: tabId })); } function toggleFileBrowser() { state.update((s) => ({ ...s, isFileBrowserOpen: !s.isFileBrowserOpen })); } function toggleEditor() { isEditorVisible.update((v) => !v); } async function createFile(parentPath: string, fileName: string): Promise { const filePath = `${parentPath}/${fileName}`; try { await invoke("create_file", { path: filePath }); // Refresh the parent directory await refreshDirectory(parentPath); return true; } catch (error) { console.error("Failed to create file:", error); saveError.set(`Failed to create file: ${error}`); return false; } } async function createDirectory(parentPath: string, dirName: string): Promise { const dirPath = `${parentPath}/${dirName}`; try { await invoke("create_directory", { path: dirPath }); // Refresh the parent directory await refreshDirectory(parentPath); return true; } catch (error) { console.error("Failed to create directory:", error); saveError.set(`Failed to create directory: ${error}`); return false; } } async function deleteFile(filePath: string): Promise { try { await invoke("delete_file", { path: filePath }); // Close the tab if it's open const currentState = get(state); const openTab = currentState.tabs.find((t) => t.filePath === filePath); if (openTab) { closeTab(openTab.id); } // Refresh the parent directory const parentPath = filePath.substring(0, filePath.lastIndexOf("/")); await refreshDirectory(parentPath); return true; } catch (error) { console.error("Failed to delete file:", error); saveError.set(`Failed to delete file: ${error}`); return false; } } async function deleteDirectory(dirPath: string): Promise { try { await invoke("delete_directory", { path: dirPath }); // Close any tabs that are in this directory const currentState = get(state); const tabsToClose = currentState.tabs.filter((t) => t.filePath.startsWith(dirPath + "/")); tabsToClose.forEach((tab) => closeTab(tab.id)); // Refresh the parent directory const parentPath = dirPath.substring(0, dirPath.lastIndexOf("/")); await refreshDirectory(parentPath); return true; } catch (error) { console.error("Failed to delete directory:", error); saveError.set(`Failed to delete directory: ${error}`); return false; } } async function refreshDirectory(dirPath: string) { const currentState = get(state); // If refreshing the root directory, reload the entire tree if (dirPath === currentState.currentDirectory) { const entries = await loadDirectory(dirPath); state.update((s) => ({ ...s, fileTree: entries })); return; } // Otherwise, update the specific directory in the tree const children = await loadDirectory(dirPath); state.update((s) => { const updateTree = (entries: FileEntry[]): FileEntry[] => { return entries.map((e) => { if (e.path === dirPath) { return { ...e, children, isExpanded: true }; } if (e.children) { return { ...e, children: updateTree(e.children) }; } return e; }); }; return { ...s, fileTree: updateTree(s.fileTree) }; }); } async function renamePath(oldPath: string, newName: string): Promise { const parentPath = oldPath.substring(0, oldPath.lastIndexOf("/")); const newPath = `${parentPath}/${newName}`; try { await invoke("rename_path", { oldPath, newPath }); // Update any open tabs that reference this path state.update((s) => ({ ...s, tabs: s.tabs.map((t) => { if (t.filePath === oldPath) { // Exact match - this file was renamed return { ...t, filePath: newPath, fileName: newName, }; } if (t.filePath.startsWith(oldPath + "/")) { // File is inside a renamed directory const relativePath = t.filePath.substring(oldPath.length); return { ...t, filePath: newPath + relativePath, }; } return t; }), })); // Refresh the parent directory await refreshDirectory(parentPath); return true; } catch (error) { console.error("Failed to rename:", error); saveError.set(`Failed to rename: ${error}`); return false; } } function showEditor() { isEditorVisible.set(true); } function hideEditor() { isEditorVisible.set(false); } return { state: { subscribe: state.subscribe }, isEditorVisible: { subscribe: isEditorVisible.subscribe }, saveError: { subscribe: saveError.subscribe }, initializeFileTree, toggleDirectory, openFile, saveFile, updateTabContent, closeTab, setActiveTab, toggleFileBrowser, toggleEditor, showEditor, hideEditor, createFile, createDirectory, deleteFile, deleteDirectory, refreshDirectory, renamePath, tabs: derived(state, ($state) => $state.tabs), activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)), activeTabId: derived(state, ($state) => $state.activeTabId), fileTree: derived(state, ($state) => $state.fileTree), isFileBrowserOpen: derived(state, ($state) => $state.isFileBrowserOpen), isLoadingTree: derived(state, ($state) => $state.isLoadingTree), currentDirectory: derived(state, ($state) => $state.currentDirectory), hasDirtyTabs: derived(state, ($state) => $state.tabs.some((t) => t.isDirty)), }; } export const editorStore = createEditorStore();