dope2-webapp/src/App.tsx

181 lines
5.1 KiB
TypeScript

import { useEffect, useState } from 'react';
import './App.css';
import { CommandContext } from './CommandContext';
import { Editor, type EditorState } from './Editor';
import { extendedEnv } from './EnvContext';
import { biggerExample, initialEditorState, lambda2Params, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
import { evalEditorBlock } from "./eval";
const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ],
["eval" , ['e','Tab','Enter'], "eval" ],
["transform", ['t', '.' ], "transform" ],
["let" , ['l', '=' ], "let … in …" ],
["lambda" , ['a' ], "λx: …" ],
];
const examples: [string, EditorState][] = [
["empty editor", initialEditorState],
["push to list", nonEmptyEditorState],
["function w/ 4 params", tripleFunctionCallEditorState],
["bigger example", biggerExample],
["lambda 2 params", lambda2Params],
];
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() {
// load from localStorage
const [appState, setAppState] = useState(loadFromLocalStorage());
const [syntacticSugar, setSyntacticSugar] = useState(true);
useEffect(() => {
// persist accross reloads
localStorage["appState"] = JSON.stringify(appState);
}, [appState]);
const factoryReset = () => {
setAppState(_ => defaultState);
}
const pushHistory = (callback: (p: EditorState) => EditorState) => {
setAppState(({history}) => {
const newState = callback(history.at(-1)!);
return {
history: history.concat([newState]),
future: [],
};
});
};
const onUndo = () => {
setAppState(({history, future}) => ({
history: history.slice(0,-1),
future: future.concat(history.at(-1)!),
}));
};
const onRedo = () => {
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 (appState.future.length > 0) {
onRedo();
}
}
else {
if (appState.history.length > 1) {
onUndo();
}
}
e.preventDefault();
}
};
useEffect(() => {
window['APP_STATE'] = appState.history; // useful for debugging
}, [appState.history]);
useEffect(() => {
window.onkeydown = onKeyDown;
}, []);
const [highlighted, setHighlighted] = useState(
commands.map(() => false));
const doHighlight = Object.fromEntries(commands.map(([id], i) => {
return [id, () => {
setHighlighted(h => h.with(i, true));
setTimeout(() => setHighlighted(h => h.with(i, false)), 100);
}];
}));
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
// @ts-ignore
if (e.target.value >= 0) {
// @ts-ignore
// @ts-ignore
pushHistory(_ => examples[e.target.value][1]);
}
}
return (
<>
<header>
<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') : '')}>
{keys.map((key, j) => <kbd key={j}>{key}</kbd>)}
{descr}
</span>)
}
<select onChange={onSelectExample} value={-1}>
<option value={-1}>load example...</option>
{
examples.map(([name], i) => {
return <option key={i} value={i}>{name}</option>;
})
}
</select>
<button className="factoryReset" onClick={factoryReset}>
FACTORY RESET
</button>
<label>
<input type="checkbox"
checked={syntacticSugar}
onChange={e => setSyntacticSugar(e.target.checked)}/>
syntactic sugar
</label>
</header>
<main onKeyDown={onKeyDown}>
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight, syntacticSugar}}>
<Editor
state={appState.history.at(-1)!}
setState={pushHistory}
onCancel={() => {}}
suggestionPriority={() => 1}
/>
</CommandContext>
</main>
<footer>
<a href="https://deemz.org/git/joeri/dope2-webapp">Source code</a>
</footer>
</>
)
}