feat: integrate Monaco Editor with tabs, NWScript syntax, and session persistence
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user