greatly simplify state + cleanup code

This commit is contained in:
Joeri Exelmans 2025-05-14 08:09:35 +02:00
parent 2d0deca127
commit 5964510036
11 changed files with 268 additions and 321 deletions

View file

@ -1,11 +1,10 @@
import { useEffect, useState } from 'react';
import './App.css'
import { Editor, type EditorState } from './Editor'
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
import './App.css';
import { CommandContext } from './CommandContext';
import { deserialize, serialize } from './types';
import { Editor, type EditorState } from './Editor';
import { extendedEnv } from './EnvContext';
import { useEffectBetter } from './util/use_effect_better';
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
import { evalEditorBlock } from "./eval";
const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ],
@ -19,54 +18,77 @@ const examples: [string, EditorState][] = [
["push to list", nonEmptyEditorState],
["function w/ 4 params", tripleFunctionCallEditorState]];
type AppState = {
history: EditorState[],
future: EditorState[],
}
const defaultState = {
history: [initialEditorState],
future: [],
};
function loadFromLocalStorage(): AppState {
if (localStorage["appState"]) {
try {
const appState = JSON.parse(localStorage["appState"]); // may throw
// if our state is corrupt, discover it eagerly:
evalEditorBlock(appState.history.at(-1), extendedEnv);
return appState; // all good
}
catch (e) {
console.log('error recovering state from localStorage (resetting):', e);
}
}
return defaultState;
}
export function App() {
// const [history, setHistory] = useState([initialEditorState]);
// const [history, setHistory] = useState([nonEmptyEditorState]);
// const [history, setHistory] = useState([tripleFunctionCallEditorState]);
// const [future, setFuture] = useState<EditorState[]>([]);
// load from localStorage
const [history, setHistory] = useState<EditorState[]>(
localStorage["history"]
? JSON.parse(localStorage["history"]).map(s => deserialize(s, extendedEnv))
: [initialEditorState]
);
const [future, setFuture] = useState<EditorState[]>(
localStorage["future"]
? JSON.parse(localStorage["future"]).map(s => deserialize(s, extendedEnv))
: []
);
const [appState, setAppState] = useState(loadFromLocalStorage());
useEffectBetter(() => {
useEffect(() => {
// persist accross reloads
localStorage["history"] = JSON.stringify(history.map(serialize));
localStorage["future"] = JSON.stringify(future.map(serialize));
}, [history, future]);
localStorage["appState"] = JSON.stringify(appState);
}, [appState]);
const factoryReset = () => {
setAppState(_ => defaultState);
}
const pushHistory = (callback: (p: EditorState) => EditorState) => {
const newState = callback(history.at(-1)!);
setHistory(history.concat([newState]));
setFuture([]);
setAppState(({history}) => {
const newState = callback(history.at(-1)!);
return {
history: history.concat([newState]),
future: [],
};
});
};
const onUndo = () => {
setFuture(future.concat(history.at(-1)!)); // add
setHistory(history.slice(0,-1)); // remove
setAppState(({history, future}) => ({
history: history.slice(0,-1),
future: future.concat(history.at(-1)!),
}));
};
const onRedo = () => {
setHistory(history.concat(future.at(-1)!)); // add
setFuture(future.slice(0,-1)); // remove
setAppState(({history, future}) => ({
history: history.concat(future.at(-1)!),
future: future.slice(0,-1),
}));
};
const onKeyDown = (e) => {
if (e.key === "Z" && e.ctrlKey) {
if (e.shiftKey) {
if (future.length > 0) {
if (appState.future.length > 0) {
onRedo();
}
}
else {
if (history.length > 1) {
if (appState.history.length > 1) {
onUndo();
}
}
@ -75,8 +97,8 @@ export function App() {
};
useEffect(() => {
window['APP_STATE'] = history; // useful for debugging
}, [history]);
window['APP_STATE'] = appState.history; // useful for debugging
}, [appState.history]);
useEffect(() => {
window.onkeydown = onKeyDown;
@ -95,14 +117,17 @@ export function App() {
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
// @ts-ignore
pushHistory(_ => examples[e.target.value][1]);
if (e.target.value) {
// @ts-ignore
pushHistory(_ => examples[e.target.value][1]);
}
}
return (
<>
<header>
<button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button>
<button disabled={appState.history.length===1} onClick={onUndo}>Undo ({appState.history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
<button disabled={appState.future.length===0} onClick={onRedo}>Redo ({appState.future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button>
{
commands.map(([_, keys, descr], i) =>
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
@ -111,22 +136,22 @@ export function App() {
</span>)
}
<select onClick={onSelectExample}>
<option>load example...</option>
{
examples.map(([name], i) => {
return <option key={i} value={i}>{name}</option>;
})
}
</select>
<button className="factoryReset" onClick={() => {
setHistory(_ => [initialEditorState]);
setFuture(_ => []);
}}>FACTORY RESET</button>
<button className="factoryReset" onClick={factoryReset}>
FACTORY RESET
</button>
</header>
<main onKeyDown={onKeyDown}>
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight}}>
<Editor
state={history.at(-1)!}
state={appState.history.at(-1)!}
setState={pushHistory}
onCancel={() => {}}
filter={() => true}