move everything

This commit is contained in:
Joeri Exelmans 2025-05-23 22:35:47 +02:00
parent 3ff7e76694
commit 9050581a10
25 changed files with 37 additions and 42 deletions

62
src/component/app/App.css Normal file
View file

@ -0,0 +1,62 @@
#root {
display: grid;
grid-template-areas:
"header header header"
"content content content"
"footer footer footer";
grid-template-columns: 100px 1fr 100px;
grid-template-rows: auto 1fr auto;
/* grid-gap: 10px; */
height: 100vh;
}
header {
grid-area: header;
}
nav {
grid-area: nav;
margin-left: 0.5rem;
}
main {
grid-area: content;
overflow: auto;
}
aside {
grid-area: side;
margin-right: 0.5rem;
}
footer {
text-align: right;
grid-area: footer;
background-color: dodgerblue;
color: white;
}
footer a {
color: white;
}
.command {
border: 1px solid black;
border-radius: 5px;
padding: 0 6px 0 6px;
margin: 0 4px 0 4px;
}
.command.highlighted {
background-color: dodgerblue;
color: white;
}
.factoryReset {
background-color: rgb(255, 0, 0);
color: black;
}

181
src/component/app/App.tsx Normal file
View file

@ -0,0 +1,181 @@
import { useEffect, useState } from 'react';
import './App.css';
import { ExprBlock, type ExprBlockState } from '../expr/ExprBlock';
import { GlobalContext } from '../../context/GlobalContext';
import { biggerExample, emptySet, factorial, higherOrder, higherOrder2Params, inc, initialEditorState, lambda2Params, nonEmptyEditorState, pushBool, tripleFunctionCallEditorState } from "./configurations";
import { actionShortcuts } from './actions';
// import { scoreResolved, type ResolvedType } from './eval';
const examples: [string, ExprBlockState][] = [
["empty editor" , initialEditorState ],
["push to list" , nonEmptyEditorState ],
["function w/ 4 params", tripleFunctionCallEditorState],
["bigger example" , biggerExample ],
["lambda 2 params" , lambda2Params ],
["higher order" , higherOrder ],
["higher order 2" , higherOrder2Params ],
["push Bool" , pushBool ],
["inc" , inc ],
["empty set" , emptySet ],
["factorial" , factorial ],
];
type AppState = {
history: ExprBlockState[],
future: ExprBlockState[],
}
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);
}
// factoryReset();
const pushHistory = (callback: (p: ExprBlockState) => ExprBlockState) => {
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(
actionShortcuts.map(() => false));
const doHighlight = Object.fromEntries(actionShortcuts.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>
{
actionShortcuts.map(([_, keys, descr], i) =>
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
{keys.map((key, j) => <kbd key={j}>{key}</kbd>)}
&nbsp;
{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}>
<GlobalContext value={{undo: onUndo, redo: onRedo, doHighlight, syntacticSugar}}>
<ExprBlock
state={appState.history.at(-1)!}
setState={pushHistory}
onCancel={() => {}}
score={() => 0}
/>
</GlobalContext>
</main>
<footer>
<a href="https://deemz.org/git/joeri/dope2-webapp">Source code</a>
</footer>
</>
)
}

View file

@ -0,0 +1,62 @@
import { initialEditorState } from "./configurations";
// import { removeFocus } from "./eval";
const removeFocus = state => state;
export const actionShortcuts: [string, string[], string][] = [
["call" , ['c'], "expr ⌴" ],
["transform", ['t'], "⌴ expr" ],
["assign" , ['a'], "let (⌴ = expr) in ⌴"],
["declare" , ['d'], "let (⌴ = ⌴) in expr"],
["lambda" , ['l'], "λ⌴. expr" ],
];
export function getActions(globalContext, setState) {
return {
c: () => {
setState(state => ({
kind: "call",
fn: removeFocus(state),
input: initialEditorState,
}));
globalContext?.doHighlight.call();
},
t: () => {
setState(state => ({
kind: "call",
fn: initialEditorState,
input: removeFocus(state),
}));
globalContext?.doHighlight.transform();
},
a: () => {
setState(state => ({
kind: "let",
name: "",
focus: true,
value: removeFocus(state),
inner: removeFocus(initialEditorState),
}));
globalContext?.doHighlight.assign();
},
d: () => {
setState(state => ({
kind: "let",
name: "",
focus: true,
value: removeFocus(initialEditorState),
inner: removeFocus(state),
}));
globalContext?.doHighlight.declare();
},
l: () => {
setState(state => ({
kind: "lambda",
paramName: "",
focus: true,
expr: removeFocus(state),
}));
globalContext?.doHighlight.lambda();
},
};
}

View file

@ -0,0 +1,144 @@
import type { ExprBlockState } from "../expr/ExprBlock";
export const initialEditorState: ExprBlockState = {
kind: "input",
text: "",
value: { kind: "text" },
focus: true,
};
export const nonEmptyEditorState: ExprBlockState = {
kind: "call",
fn: {
kind: "call",
fn: {
kind: "input",
text: "list.push",
value: { kind: "name" },
focus: false,
},
input: {
kind: "input",
text: "list.emptyList",
value: { kind: "name" },
focus: false,
},
},
input: {
kind: "input",
text: "42",
value: { kind: "literal", type: "Int" },
focus: false,
},
};
export const tripleFunctionCallEditorState: ExprBlockState = {
kind: "call",
fn: {
kind: "call",
fn: {
kind: "call",
fn: {
kind: "call",
fn: {
kind: "input",
text: "functionWith4Params",
value: { kind: "name" },
focus: false,
},
input: {
kind: "input",
text: "42",
value: { kind: "literal", type: "Int" },
focus: false,
},
},
input: {
kind: "input",
text: "43",
value: { kind: "literal", type: "Int" },
focus: false,
},
},
input: {
kind: "input",
text: "44",
value: { kind: "literal", type: "Int" },
focus: false,
},
},
input: {
kind: "input",
text: "45",
value: { kind: "literal", type: "Int" },
focus: false,
},
};
export const biggerExample: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"input","text":"","value":{"kind":"text"},"focus":false},"name":"myListInc","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"myList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"inc","value":{"kind":"name"},"focus":false}}},"input":{"kind":"input","text":"id","value":{"kind":"name"},"focus":true}}},"name":"myList","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"list.emptyList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"2","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"3","value":{"kind":"literal","type":"Int"},"focus":false}}},"name":"id","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}}},"name":"inc","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":true}}}};
export const lambda2Params: ExprBlockState = {
"kind": "let",
"inner": {
"kind": "input",
"text": "",
"value": {
"kind": "text"
},
"focus": false
},
"name": "myAddInt",
"focus": false,
"value": {
"kind": "lambda",
"paramName": "x",
"focus": false,
"expr": {
"kind": "lambda",
"paramName": "y",
"focus": false,
"expr": {
"kind": "call",
"fn": {
"kind": "call",
"fn": {
"kind": "input",
"text": "addInt",
"value": {
"kind": "name"
},
"focus": false
},
"input": {
"kind": "input",
"text": "x",
"value": {
"kind": "name"
},
"focus": false
}
},
"input": {
"kind": "input",
"text": "y",
"value": {
"kind": "name"
},
"focus": false
}
}
}
}
};
export const higherOrder: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"input","text":"","value":{"kind":"text"},"focus":false},"name":"myBinaryApply","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"lambda","focus":false,"paramName":"fn","expr":{"kind":"call","fn":{"kind":"input","text":"fn","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}}}}};
export const higherOrder2Params: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"call","fn":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"myBinaryApply","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}},"input":{"kind":"input","text":"2","value":{"kind":"literal","type":"Int"},"focus":false}},"input":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":true}},"name":"myBinaryApply","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"lambda","focus":false,"paramName":"y","expr":{"kind":"lambda","focus":false,"paramName":"fn","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"fn","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"y","value":{"kind":"name"},"focus":true}}}}}};
export const pushBool: ExprBlockState = {"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"list.emptyList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"Bool","value":{"kind":"name"},"focus":true}};
export const inc: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"input","text":"","value":{"kind":"name"},"focus":false},"name":"inc","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":true}}}};
export const emptySet: ExprBlockState = {"kind":"call","fn":{"kind":"input","text":"set.emptySet","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"","value":{"kind":"text"},"focus":true}};
export const factorial: ExprBlockState = {"kind":"lambda","paramName":"factorial","focus":true,"expr":{"kind":"lambda","paramName":"n","focus":true,"expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"leqZero","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"n","value":{"kind":"name"},"focus":false}},"input":{"kind":"lambda","paramName":"_","focus":false,"expr":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"lambda","paramName":"_","focus":false,"expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"mulInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"n","value":{"kind":"name"},"focus":true}},"input":{"kind":"call","fn":{"kind":"input","text":"factorial","value":{"kind":"name"},"focus":true},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"n","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"-1","value":{"kind":"literal","type":"Int"},"focus":false}}}}}}}};