import type { AppState, HistorySnapshot } from "@editor/state"; import type { CommandContext, CommandHistoryPolicy } from "./command"; import type { CommandId, CommandPayloads } from "./payloads"; import type { CommandRegistry } from "./registry"; export type Dispatch = (commandId: TCommandId, payload: CommandPayloads[TCommandId]) => AppState; export type CommandDispatcher = { dispatch: Dispatch; }; export function createCommandDispatcher(options: { registry: CommandRegistry; getState: () => AppState; setState: (state: AppState) => void; }): CommandDispatcher { let deferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined; return { dispatch(commandId, payload) { const command = options.registry.get(commandId); if (!command) { throw new Error(`Unknown command: ${commandId}`); } const currentState = options.getState(); const historyPolicy = command.history ?? defaultHistoryPolicy; const deferredSnapshot = historyPolicy.mode === "deferred" && historyPolicy.phase === "begin" ? snapshot(currentState) : undefined; const context: CommandContext = { state: currentState }; const executedState = command.execute(context, payload); if (executedState === currentState) { if (historyPolicy.mode === "deferred" && historyPolicy.phase === "commit") deferredHistory = undefined; return currentState; } const nextState = applyHistoryPolicy({ currentState, nextState: executedState, historyPolicy, deferredSnapshot, getDeferredHistory: () => deferredHistory, setDeferredHistory: (nextDeferredHistory) => { deferredHistory = nextDeferredHistory; }, }); options.setState(nextState); return nextState; }, }; } const defaultHistoryPolicy: CommandHistoryPolicy = { mode: "auto" }; function applyHistoryPolicy(options: { currentState: AppState; nextState: AppState; historyPolicy: CommandHistoryPolicy; deferredSnapshot?: HistorySnapshot; getDeferredHistory: () => { snapshot: HistorySnapshot; changed: boolean } | undefined; setDeferredHistory: (nextDeferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined) => void; }): AppState { const { historyPolicy } = options; if (historyPolicy.mode === "ignore") { options.setDeferredHistory(undefined); return options.nextState; } if (historyPolicy.mode === "deferred") { return applyDeferredHistoryPolicy({ ...options, historyPolicy }); } return shouldRecordHistory(options.currentState, options.nextState) ? recordHistory(snapshot(options.currentState), options.nextState) : options.nextState; } function applyDeferredHistoryPolicy(options: { currentState: AppState; nextState: AppState; historyPolicy: Extract; deferredSnapshot?: HistorySnapshot; getDeferredHistory: () => { snapshot: HistorySnapshot; changed: boolean } | undefined; setDeferredHistory: (nextDeferredHistory: { snapshot: HistorySnapshot; changed: boolean } | undefined) => void; }): AppState { switch (options.historyPolicy.phase) { case "begin": options.setDeferredHistory(options.deferredSnapshot ? { snapshot: options.deferredSnapshot, changed: false } : undefined); return options.nextState; case "update": { const deferredHistory = options.getDeferredHistory(); if (deferredHistory && options.currentState.document !== options.nextState.document) { options.setDeferredHistory({ ...deferredHistory, changed: true }); } return options.nextState; } case "commit": { const deferredHistory = options.getDeferredHistory(); options.setDeferredHistory(undefined); return deferredHistory?.changed ? recordHistory(deferredHistory.snapshot, options.nextState) : options.nextState; } } } function shouldRecordHistory(currentState: AppState, nextState: AppState) { return currentState.document !== nextState.document; } function recordHistory(historySnapshot: HistorySnapshot, nextState: AppState): AppState { return { ...nextState, history: { past: [...nextState.history.past, historySnapshot].slice(-100), future: [], }, }; } function snapshot(state: AppState): HistorySnapshot { return { document: state.document, editor: { ...state.editor, brushPreview: undefined, brushStrokePreview: undefined, commandPalette: { open: false, query: "", selectedIndex: 0, }, }, }; }