import { signal, type Signal } from "@wrnexus/reactive"; import { applyCommand, type BlockCommand, type BlockDocument, type CommandResult, type DocumentKindDefinition, type ReadonlyBlockRegistry, type Result, type SelectionState, } from "@wrnexus/blocks"; export interface EditorSnapshot { document: BlockDocument; revision: number; selection: SelectionState; dirty: boolean; status: "idle" | "saving" | "saved" | "conflict" | "error"; } interface HistoryEntry { forward: BlockCommand; inverse: BlockCommand; } export interface BlockEditorState extends Signal { execute(command: BlockCommand): Result; undo(): boolean; redo(): boolean; select(ids: string[], primary?: string | null): void; markSaving(): void; markSaved(revision: number): void; markConflict(): void; replaceFromServer(document: BlockDocument, revision: number): void; canUndo(): boolean; canRedo(): boolean; } export function createBlockEditorState(options: { document: BlockDocument; revision: number; kind: DocumentKindDefinition; registry: ReadonlyBlockRegistry; historyLimit?: number; }): BlockEditorState { const current = signal({ document: options.document, revision: options.revision, selection: { primary: null, ordered: [] }, dirty: false, status: "idle", }); const past: HistoryEntry[] = []; const future: HistoryEntry[] = []; const limit = Math.max(1, options.historyLimit ?? 100); const apply = (command: BlockCommand, remember: boolean): Result => { const result = applyCommand(current.get().document, command, options.kind, options.registry); if (!result.ok) return result; if (remember) { past.push({ forward: command, inverse: result.value.inverse }); if (past.length > limit) past.shift(); future.length = 0; } current.set({ ...current.get(), document: result.value.document, selection: result.value.selection, dirty: true, status: "idle", }); return result; }; return Object.assign(current, { execute: (command: BlockCommand) => apply(command, true), undo() { const entry = past.pop(); if (!entry) return false; const result = apply(entry.inverse, false); if (!result.ok) { past.push(entry); return false; } future.push(entry); return true; }, redo() { const entry = future.pop(); if (!entry) return false; const result = apply(entry.forward, false); if (!result.ok) { future.push(entry); return false; } past.push(entry); return true; }, select(ids: string[], primary: string | null = ids[0] ?? null) { current.set({ ...current.get(), selection: { primary, ordered: [...new Set(ids)] } }); }, markSaving() { current.set({ ...current.get(), status: "saving" }); }, markSaved(revision: number) { current.set({ ...current.get(), revision, dirty: false, status: "saved" }); }, markConflict() { current.set({ ...current.get(), status: "conflict" }); }, replaceFromServer(document: BlockDocument, revision: number) { past.length = 0; future.length = 0; current.set({ document, revision, selection: { primary: null, ordered: [] }, dirty: false, status: "idle", }); }, canUndo: () => past.length > 0, canRedo: () => future.length > 0, }); }