181 lines
5.1 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|