125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
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 = <TCommandId extends CommandId>(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 {
|
|
if (options.historyPolicy.mode === "ignore") {
|
|
options.setDeferredHistory(undefined);
|
|
return options.nextState;
|
|
}
|
|
|
|
if (options.historyPolicy.mode === "deferred") {
|
|
return applyDeferredHistoryPolicy(options);
|
|
}
|
|
|
|
return shouldRecordHistory(options.currentState, options.nextState) ? recordHistory(snapshot(options.currentState), options.nextState) : options.nextState;
|
|
}
|
|
|
|
function applyDeferredHistoryPolicy(options: {
|
|
currentState: AppState;
|
|
nextState: AppState;
|
|
historyPolicy: Extract<CommandHistoryPolicy, { mode: "deferred" }>;
|
|
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,
|
|
},
|
|
};
|
|
}
|