"use client"; import { type RefObject, useCallback, useEffect, useRef } from "react"; export interface HistoryEntry { readonly value: string; readonly selectionStart: number; readonly selectionEnd: number; } const COALESCE_WINDOW_MS = 400; const COALESCE_MAX_DELTA = 8; const MAX_HISTORY = 200; function countLines(value: string): number { let lines = 1; for (let i = 0; i < value.length; i += 1) { if (value.charCodeAt(i) === 10) lines += 1; } return lines; } export function useEditorHistory( body: string, bodyRef: RefObject, ) { const stackRef = useRef([]); const indexRef = useRef(-1); const lastRecordAtRef = useRef(0); if (stackRef.current.length === 0) { stackRef.current = [{ value: body, selectionStart: 0, selectionEnd: 0 }]; indexRef.current = 0; } useEffect(() => { const top = stackRef.current[indexRef.current]; if (top && top.value === body) return; stackRef.current = [ { value: body, selectionStart: bodyRef.current?.selectionStart ?? 0, selectionEnd: bodyRef.current?.selectionEnd ?? 0, }, ]; indexRef.current = 0; lastRecordAtRef.current = 0; }, [body, bodyRef]); const record = useCallback( (value: string, selectionStart: number, selectionEnd: number, coalesce: boolean) => { const stack = stackRef.current; const top = stack[indexRef.current]; const now = Date.now(); const canCoalesce = coalesce && top !== undefined && now - lastRecordAtRef.current <= COALESCE_WINDOW_MS && Math.abs(value.length - top.value.length) <= COALESCE_MAX_DELTA && countLines(value) === countLines(top.value); if (canCoalesce && top) { stack[indexRef.current] = { value, selectionStart, selectionEnd }; } else { stack.length = indexRef.current + 1; stack.push({ value, selectionStart, selectionEnd }); if (stack.length > MAX_HISTORY) stack.shift(); indexRef.current = stack.length - 1; } lastRecordAtRef.current = now; }, [], ); const undo = useCallback((): HistoryEntry | null => { if (indexRef.current <= 0) return null; indexRef.current -= 1; lastRecordAtRef.current = 0; return stackRef.current[indexRef.current]; }, []); const redo = useCallback((): HistoryEntry | null => { const stack = stackRef.current; if (indexRef.current >= stack.length - 1) return null; indexRef.current += 1; lastRecordAtRef.current = 0; return stack[indexRef.current]; }, []); return { record, undo, redo }; }