import { cn } from "@/lib/utils"; import type { EditorTab, Tab } from "@/modules/tabs"; import { useEffect, useRef } from "react"; import { EditorPane, type EditorPaneHandle } from "./EditorPane"; type Props = { tabs: Tab[]; activeId: number; onDirtyChange: (id: number, dirty: boolean) => void; registerHandle: (id: number, handle: EditorPaneHandle | null) => void; onCloseTab: (id: number) => void; }; export function EditorStack({ tabs, activeId, onDirtyChange, registerHandle, onCloseTab, }: Props) { const editors = tabs.filter((t): t is EditorTab => t.kind === "editor"); // Stable per-tab callbacks. Inline arrows in `ref` and `onDirtyChange` // change identity every render, which makes React detach+reattach the ref // callback and re-invoke `onDirtyChange`, triggering setState loops in // the parent. Memoizing per id keeps each callback's identity stable. const registerRef = useRef(registerHandle); const dirtyRef = useRef(onDirtyChange); const closeRef = useRef(onCloseTab); useEffect(() => { registerRef.current = registerHandle; }, [registerHandle]); useEffect(() => { dirtyRef.current = onDirtyChange; }, [onDirtyChange]); useEffect(() => { closeRef.current = onCloseTab; }, [onCloseTab]); const refCallbacks = useRef( new Map void>(), ); const dirtyCallbacks = useRef(new Map void>()); const closeCallbacks = useRef(new Map void>()); const getRefCallback = (id: number) => { let cb = refCallbacks.current.get(id); if (!cb) { cb = (h: EditorPaneHandle | null) => registerRef.current(id, h); refCallbacks.current.set(id, cb); } return cb; }; const getDirtyCallback = (id: number) => { let cb = dirtyCallbacks.current.get(id); if (!cb) { cb = (dirty: boolean) => dirtyRef.current(id, dirty); dirtyCallbacks.current.set(id, cb); } return cb; }; const getCloseCallback = (id: number) => { let cb = closeCallbacks.current.get(id); if (!cb) { cb = () => closeRef.current(id); closeCallbacks.current.set(id, cb); } return cb; }; // Drop callback entries for closed tabs to avoid unbounded growth. useEffect(() => { const live = new Set(editors.map((t) => t.id)); for (const id of refCallbacks.current.keys()) { if (!live.has(id)) refCallbacks.current.delete(id); } for (const id of dirtyCallbacks.current.keys()) { if (!live.has(id)) dirtyCallbacks.current.delete(id); } for (const id of closeCallbacks.current.keys()) { if (!live.has(id)) closeCallbacks.current.delete(id); } }, [editors]); if (editors.length === 0) return null; return (
{editors.map((t) => { const visible = t.id === activeId; return (
); })}
); }