| import { useCallback, useMemo, useRef, useState } from "react"; |
|
|
| export interface VirtualFile { |
| path: string; |
| content: string; |
| updatedAt: number; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function useVirtualFS(initial?: VirtualFile[]) { |
| const [files, setFiles] = useState<Map<string, VirtualFile>>(() => { |
| const map = new Map<string, VirtualFile>(); |
| (initial ?? []).forEach((f) => map.set(f.path, f)); |
| return map; |
| }); |
| const [version, setVersion] = useState(0); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const filesRef = useRef(files); |
|
|
| const bump = () => setVersion((v) => v + 1); |
|
|
| const commit = useCallback((next: Map<string, VirtualFile>) => { |
| filesRef.current = next; |
| setFiles(next); |
| bump(); |
| }, []); |
|
|
| const writeFile = useCallback( |
| (path: string, content: string) => { |
| const next = new Map(filesRef.current); |
| next.set(path, { path, content, updatedAt: Date.now() }); |
| commit(next); |
| }, |
| [commit], |
| ); |
|
|
| const editFile = useCallback( |
| ( |
| path: string, |
| oldString: string, |
| newString: string, |
| ): { ok: boolean; error?: string } => { |
| const current = filesRef.current.get(path); |
| if (!current) { |
| return { ok: false, error: `File "${path}" does not exist (call write_file first).` }; |
| } |
| const count = current.content.split(oldString).length - 1; |
| if (count === 0) { |
| return { ok: false, error: `old_string not found in "${path}".` }; |
| } |
| if (count > 1) { |
| return { |
| ok: false, |
| error: `old_string is not unique in "${path}" (${count} occurrences). Add more surrounding context.`, |
| }; |
| } |
| const nextContent = current.content.replace(oldString, newString); |
| const next = new Map(filesRef.current); |
| next.set(path, { path, content: nextContent, updatedAt: Date.now() }); |
| commit(next); |
| return { ok: true }; |
| }, |
| [commit], |
| ); |
|
|
| const deleteFile = useCallback( |
| (path: string): { ok: boolean; error?: string } => { |
| if (!filesRef.current.has(path)) { |
| return { ok: false, error: `File "${path}" does not exist.` }; |
| } |
| const next = new Map(filesRef.current); |
| next.delete(path); |
| commit(next); |
| return { ok: true }; |
| }, |
| [commit], |
| ); |
|
|
| const readFile = useCallback((path: string): VirtualFile | null => { |
| return filesRef.current.get(path) ?? null; |
| }, []); |
|
|
| const listFiles = useCallback((): VirtualFile[] => { |
| return Array.from(filesRef.current.values()).sort((a, b) => |
| a.path.localeCompare(b.path), |
| ); |
| }, []); |
|
|
| const clear = useCallback(() => { |
| commit(new Map()); |
| }, [commit]); |
|
|
| |
| |
| |
| |
| |
| |
| const sortedFiles = useMemo( |
| () => |
| Array.from(files.values()).sort((a, b) => a.path.localeCompare(b.path)), |
| [files], |
| ); |
|
|
| return { |
| files: sortedFiles, |
| filesMap: files, |
| version, |
| writeFile, |
| editFile, |
| deleteFile, |
| readFile, |
| listFiles, |
| clear, |
| getAll: () => Array.from(filesRef.current.values()), |
| }; |
| } |
|
|
| export type VirtualFSHandle = ReturnType<typeof useVirtualFS>; |
|
|