129 lines
3.1 KiB
TypeScript
129 lines
3.1 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from "react";
|
|
|
|
const STORAGE_KEY = "forge-editor-state";
|
|
|
|
interface PersistedState {
|
|
openTabs: string[];
|
|
activeTab: string | null;
|
|
}
|
|
|
|
function loadPersistedState(): PersistedState {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
return {
|
|
openTabs: Array.isArray(parsed.openTabs) ? parsed.openTabs : [],
|
|
activeTab:
|
|
typeof parsed.activeTab === "string" ? parsed.activeTab : null,
|
|
};
|
|
}
|
|
} catch {
|
|
// corrupted storage — start fresh
|
|
}
|
|
return { openTabs: [], activeTab: null };
|
|
}
|
|
|
|
function persistState(state: PersistedState) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
} catch {
|
|
// quota exceeded — silently ignore
|
|
}
|
|
}
|
|
|
|
export function useEditorState() {
|
|
const [openTabs, setOpenTabs] = useState<string[]>(
|
|
() => loadPersistedState().openTabs,
|
|
);
|
|
const [activeTab, setActiveTab] = useState<string | null>(
|
|
() => loadPersistedState().activeTab,
|
|
);
|
|
const [dirtyFiles, setDirtyFiles] = useState<Set<string>>(new Set());
|
|
const fileContents = useRef<Map<string, string>>(new Map());
|
|
|
|
useEffect(() => {
|
|
persistState({ openTabs, activeTab });
|
|
}, [openTabs, activeTab]);
|
|
|
|
const openFile = useCallback(
|
|
(path: string, content?: string) => {
|
|
if (content !== undefined) {
|
|
fileContents.current.set(path, content);
|
|
}
|
|
setOpenTabs((prev) => {
|
|
if (prev.includes(path)) return prev;
|
|
return [...prev, path];
|
|
});
|
|
setActiveTab(path);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const closeFile = useCallback(
|
|
(path: string) => {
|
|
setOpenTabs((prev) => {
|
|
const next = prev.filter((p) => p !== path);
|
|
setActiveTab((current) => {
|
|
if (current !== path) return current;
|
|
const idx = prev.indexOf(path);
|
|
return next[Math.min(idx, next.length - 1)] ?? null;
|
|
});
|
|
return next;
|
|
});
|
|
setDirtyFiles((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(path);
|
|
return next;
|
|
});
|
|
fileContents.current.delete(path);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const selectTab = useCallback((path: string) => {
|
|
setActiveTab(path);
|
|
}, []);
|
|
|
|
const markDirty = useCallback((path: string) => {
|
|
setDirtyFiles((prev) => {
|
|
if (prev.has(path)) return prev;
|
|
return new Set(prev).add(path);
|
|
});
|
|
}, []);
|
|
|
|
const markClean = useCallback((path: string) => {
|
|
setDirtyFiles((prev) => {
|
|
if (!prev.has(path)) return prev;
|
|
const next = new Set(prev);
|
|
next.delete(path);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const updateContent = useCallback(
|
|
(path: string, content: string) => {
|
|
fileContents.current.set(path, content);
|
|
markDirty(path);
|
|
},
|
|
[markDirty],
|
|
);
|
|
|
|
const getContent = useCallback((path: string): string | undefined => {
|
|
return fileContents.current.get(path);
|
|
}, []);
|
|
|
|
return {
|
|
openTabs,
|
|
activeTab,
|
|
dirtyFiles,
|
|
openFile,
|
|
closeFile,
|
|
selectTab,
|
|
markDirty,
|
|
markClean,
|
|
updateContent,
|
|
getContent,
|
|
};
|
|
}
|