From 35d1034c6774ae6ae05ea2dbf392f19a0750258b Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 13 May 2025 18:29:37 +0200 Subject: [PATCH] greatly simplified app --- pnpm-lock.yaml | 8 +- src/App.tsx | 10 +-- src/CallBlock.tsx | 206 +++++++++++++++++++-------------------------- src/Editor.tsx | 54 ++++++------ src/InputBlock.tsx | 53 +++++------- src/LetInBlock.tsx | 11 +-- src/Value.tsx | 8 +- src/util/extra.ts | 10 +-- 8 files changed, 156 insertions(+), 204 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 658260d..e7b5fdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: dope2: specifier: git+https://deemz.org/git/joeri/dope2.git - version: git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890 + version: git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994 react: specifier: ^19.1.0 version: 19.1.0 @@ -633,8 +633,8 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - dope2@git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890: - resolution: {commit: e631f11aa52b2adda8809c1b0b41cc991fbe8890, repo: https://deemz.org/git/joeri/dope2.git, type: git} + dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994: + resolution: {commit: 443a13998dc3eccab26c27bee4fa056cdbc8f994, repo: https://deemz.org/git/joeri/dope2.git, type: git} version: 0.0.1 dunder-proto@1.0.1: @@ -1762,7 +1762,7 @@ snapshots: depd@2.0.0: {} - dope2@git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890: + dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994: dependencies: functional-red-black-tree: 1.0.1 diff --git a/src/App.tsx b/src/App.tsx index 42f7b44..05505d0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,14 +6,15 @@ import { CommandContext } from './CommandContext'; import { EnvContext } from './EnvContext'; export function App() { - const [history, setHistory] = useState([initialEditorState]); + // const [history, setHistory] = useState([initialEditorState]); // const [history, setHistory] = useState([nonEmptyEditorState]); - // const [history, setHistory] = useState([tripleFunctionCallEditorState]); + const [history, setHistory] = useState([tripleFunctionCallEditorState]); const [future, setFuture] = useState([]); - const pushHistory = (s: EditorState) => { - setHistory(history.concat([s])); + const pushHistory = (callback: (p: EditorState) => EditorState) => { + const newState = callback(history.at(-1)!); + setHistory(history.concat([newState])); setFuture([]); }; @@ -87,7 +88,6 @@ export function App() { {}} onCancel={() => {}} filter={() => true} /> diff --git a/src/CallBlock.tsx b/src/CallBlock.tsx index 95e7780..6753e30 100644 --- a/src/CallBlock.tsx +++ b/src/CallBlock.tsx @@ -1,13 +1,14 @@ -import { useState } from "react"; - -import { apply, UnifyError, assignFn, getType, getSymbol, symbolFunction } from "dope2"; +import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction, UnifyError } from "dope2"; import { Editor, type EditorState } from "./Editor"; import { Value } from "./Value"; -import type { Dynamic, State2Props } from "./util/extra"; +import type { Dynamic, SetStateFn, State2Props } from "./util/extra"; +import { useEffect } from "react"; import "./CallBlock.css"; +type ResolvedType = Dynamic | Error | undefined; + export interface CallBlockState< FnState=EditorState, InputState=EditorState, @@ -15,88 +16,73 @@ export interface CallBlockState< kind: "call"; fn: FnState; input: InputState; - resolved: undefined | Dynamic; - // focus: boolean; + resolved: ResolvedType; } interface CallBlockProps< FnState=EditorState, InputState=EditorState, -> extends State2Props> { - onResolve: (resolved: EditorState) => void; +> extends State2Props,EditorState> { } -function headlessCallBlock({state, setState, onResolve}: CallBlockProps) { - const [unifyError, setUnifyError] = useState(undefined); - const {fn, input } = state; - const setFn = (fn: EditorState) => { - setState({...state, fn}); + +function have(resolved: ResolvedType) { + return resolved && !(resolved instanceof Error); +} + +function headlessCallBlock({state, setState}: CallBlockProps) { + const {fn, input} = state; + const setFn = (callback: SetStateFn) => { + setState(state => ({...state, fn: callback(state.fn)})); } - const setInput = (input: EditorState) => { - setState({...state, input}); + const setInput = (callback: SetStateFn) => { + setState(state => ({...state, input: callback(state.input)})); } - const setResolved = (resolved?: Dynamic) => { - setState({...state, resolved}); + const setResolved = (callback: SetStateFn) => { + setState(state => ({...state, resolved: callback(state.resolved)})); } - const makeTheCall = (input, fn) => { - try { - const outputResolved = apply(input.resolved)(fn.resolved); - setResolved(outputResolved); - onResolve({ - ...state, resolved: outputResolved - }); - setUnifyError(undefined); - } - catch (e) { - if (!(e instanceof UnifyError)) { - throw e; + useEffect(() => { + // Here we do something spooky: we update the state in response to state change... + // The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle. + if (have(input.resolved) && have(fn.resolved)) { + try { + const outputResolved = apply(input.resolved)(fn.resolved); // may throw + setResolved(() => outputResolved); // success + } + catch (e) { + if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) { + throw e; + } + setResolved(() => e as Error); // eval error } - setUnifyError(e as typeof UnifyError); - onResolve({ - ...state, resolved: undefined - }) } - }; - const onFnResolve = (fnState) => { - if (fnState && input.resolved) { - makeTheCall(input, fnState); + else if (input.resolved instanceof Error) { + setResolved(() => input.resolved); // bubble up the error + } + else if (fn.resolved instanceof Error) { + setResolved(() => fn.resolved); // bubble up the error } else { - // setFn(fnState); - setResolved(undefined); - onResolve({ - ...state, resolved: undefined - }); + // no errors and at least one is undefined: + setResolved(() => undefined); // chill out } - }; - const onInputResolve = (inputState) => { - if (fn.resolved && inputState) { - makeTheCall(inputState, fn); - } - else { - setResolved(undefined); - onResolve({ - ...state, resolved: undefined - }); - } - }; + }, [input.resolved, fn.resolved]); const onFnCancel = () => { - setState(input); + setState(state => state.input); // we become our input } const onInputCancel = () => { - setState(fn); + setState(state => state.fn); // we become our function } - return {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel}; + return {setFn, setInput, onFnCancel, onInputCancel}; } -export function CallBlock({ state, setState, onResolve }: CallBlockProps) { - const {unifyError, setFn, setInput, onFnResolve, onFnCancel, onInputResolve, onInputCancel} - = headlessCallBlock({ state, setState, onResolve }); - return +export function CallBlock({ state, setState }: CallBlockProps) { + const {setFn, setInput, onFnCancel, onInputCancel} + = headlessCallBlock({ state, setState }); + return
@@ -105,113 +91,93 @@ export function CallBlock({ state, setState, onResolve }: CallBlockProps) { - + onInputCancel={onInputCancel} /> {/* Output (or Error) */} - {state.resolved && } - { state.resolved && <>☑} - {unifyError && unifyError.toString()} + { state.resolved instanceof Error && state.resolved.toString() + || state.resolved && <>☑}
; } -function FunctionHeader({ fn, setFn, input, onFnResolve, onFnCancel }) { +function filterFnInputs(fn: Dynamic|Error|undefined, input: Dynamic|Error|undefined) { + if (!have(fn) || !have(input)) { + return false; + } + const fnType = getType(fn); + if (getSymbol(fnType) !== symbolFunction) { + return false; // filter out non-functions already + } + try { + assignFn(fnType, getType(input)); // may throw + return true; + } catch (e) { + if (!(e instanceof UnifyError)) { + throw e; + } + return false; + } +} + +function FunctionHeader({ fn, setFn, input, onFnCancel }) { if (fn.kind === "call") { // if the function we're calling is itself the result of a function call, // then we are anonymous, and so we don't draw a function name // recurse: const { - onFnResolve : onFnFnResolve, + setFn : setFnFn, onFnCancel : onFnFnCancel, - } = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve}); + } = headlessCallBlock({state: fn, setState: setFn}); return setFn({...fn, fn: fnFn})} - onFnResolve={onFnFnResolve} + setFn={setFnFn} onFnCancel={onFnFnCancel} input={fn.input} />; } else { - const filterCompatibleFns = ([_name, dynamic]: [string, Dynamic]) => { - if (input.resolved) { - try { - const type = getType(dynamic); - if (getSymbol(type) !== symbolFunction) { - return false; - } - assignFn(type, getType(input.resolved)); - } catch (e) { - if (!(e instanceof UnifyError)) { - throw e; - } - return false; - } - } - return true; - } - // end of recursion - draw function name return  𝑓𝑛  + filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} /> ; } } -function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) { - const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => { - if (fn.resolved) { - try { - assignFn(getType(fn.resolved), getType(dynamic)); - } catch (e) { - if (!(e instanceof UnifyError)) { - throw e; - } - return false; - } - } - return true; - } +function InputParams({ fn, setFn, input, setInput, onInputCancel }) { return
{(fn.kind === "call") && // if the function we're calling is itself the result of a function call, // then we render its input parameter nested in our own input parameter box, which is way more readable - // Input(s) of the function we're calling: - + // recurse: + } {/* Our own input */} filterFnInputs(fn.resolved, inputCandidate)} />
; } -function NestedInputParams({fn, setFn, onFnResolve}) { +function NestedParams({fn, setFn}) { const { - onInputResolve: onFnInputResolve, - onFnResolve : onFnFnResolve, - } = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve}); + setFn : setFnFn, + setInput : setFnInput, + } = headlessCallBlock({state: fn, setState: setFn}); return setFn({...fn, fn: fnFn})} + setFn={setFnFn} input={fn.input} - setInput={fnInput => setFn({...fn, input: fnInput})} - onFnResolve={onFnFnResolve} - onInputResolve={onFnInputResolve} + setInput={setFnInput} onInputCancel={() => {/*todo*/}} />; -} +} \ No newline at end of file diff --git a/src/Editor.tsx b/src/Editor.tsx index c66b00a..4421961 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -21,7 +21,6 @@ export type EditorState = interface EditorProps extends State2Props { filter: (suggestion: [string, Dynamic]) => boolean; - onResolve: (state: EditorState) => void; onCancel: () => void; } @@ -52,7 +51,7 @@ function removeFocus(state: EditorState): EditorState { return state; } -export function Editor({state, setState, onResolve, onCancel, filter}: EditorProps) { +export function Editor({state, setState, onCancel, filter}: EditorProps) { const [needCommand, setNeedCommand] = useState(false); const commandInputRef = useRef(null); useEffect(() => { @@ -60,21 +59,21 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro commandInputRef.current?.focus(); } }, [needCommand]); - const onMyResolve = (editorState: EditorState) => { - setState(editorState); - onResolve(editorState); - return; + // const onMyResolve = (editorState: EditorState) => { + // setState(editorState); + // onResolve(editorState); + // return; - if (editorState.resolved) { - setNeedCommand(true); - } - else { - // unresolved - setNeedCommand(false); - onResolve(editorState); // pass up the fact that we're unresolved - } - } + // if (editorState.resolved) { + // setNeedCommand(true); + // } + // else { + // // unresolved + // setNeedCommand(false); + // onResolve(editorState); // pass up the fact that we're unresolved + // } + // } const globalContext = useContext(CommandContext); const onCommand = (e: React.KeyboardEvent) => { @@ -88,7 +87,7 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro setNeedCommand(false); // u -> pass Up if (e.key === "e" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) { - onResolve(state); + // onResolve(state); globalContext?.doHighlight.eval(); return; } @@ -99,12 +98,12 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro // c -> Call if (e.key === "c") { // we become CallBlock - setState({ + setState(state => ({ kind: "call", fn: removeFocus(state), input: initialEditorState, resolved: undefined, - }); + })); globalContext?.doHighlight.call(); // focusNextElement(); return; @@ -112,12 +111,12 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro // t -> Transform if (e.key === "t" || e.key === ".") { // we become CallBlock - setState({ + setState(state => ({ kind: "call", fn: initialEditorState, input: removeFocus(state), resolved: undefined, - }); + })); globalContext?.doHighlight.transform(); return; } @@ -133,13 +132,13 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro // = -> assign to name if (e.key === 'l' || e.key === '=') { // we become LetInBlock - setState({ + setState(state => ({ kind: "let", inner: removeFocus(initialEditorState), name: "", value: removeFocus(state), resolved: undefined, - }); + })); globalContext?.doHighlight.let(); return; } @@ -150,22 +149,19 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro case "input": return EditorState)=>void} filter={filter} - onResolve={onMyResolve} onCancel={onCancel} />; case "call": return EditorState)=>void} />; case "let": return {}} + setState={setState as (callback:(p:LetInBlockState)=>EditorState)=>void} />; case "lambda": return <>; @@ -174,7 +170,7 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro return <> {renderBlock()} { - (state.resolved) + (state.resolved && !(state.resolved instanceof Error)) ?
::
diff --git a/src/InputBlock.tsx b/src/InputBlock.tsx index 7f0323f..dc3f882 100644 --- a/src/InputBlock.tsx +++ b/src/InputBlock.tsx @@ -18,7 +18,6 @@ export interface InputBlockState { interface InputBlockProps extends State2Props { filter: (suggestion: [string, Dynamic]) => boolean; - onResolve: (state: InputBlockState) => void; onCancel: () => void; } @@ -31,6 +30,7 @@ const computeSuggestions = (text, env, filter) => { ... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []), ... trie.suggest(env.name2dyn)(text)(Infinity), ] + return ls; return [ ...ls.filter(filter), // ones that match filter come first ...ls.filter(s => !filter(s)), @@ -38,17 +38,13 @@ const computeSuggestions = (text, env, filter) => { // .slice(0,30); } -export function InputBlock({ state, setState, filter, onResolve, onCancel }: InputBlockProps) { +export function InputBlock({ state, setState, filter, onCancel }: InputBlockProps) { const {text, resolved, focus} = state; const env = useContext(EnvContext); const inputRef = useRef(null); const [i, setI] = useState(0); // selected suggestion idx const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not - const setText = (text: string) => { - setState({...state, text}); - } - const singleSuggestion = trie.growPrefix(env.name2dyn)(text); const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]); @@ -60,20 +56,21 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp } }, [focus]); - const onSelectSuggestion = ([name, dynamic]) => { - console.log('resolving input block', text, '->', name); - - onResolve({ - kind: "input", - text: name, - resolved: dynamic, - focus: false, - }); - }; + useEffect(() => { + if (suggestions.length >= i) { + setI(0); + } + }, [suggestions.length]); const getCaretPosition = () => { return inputRef.current?.selectionStart || -1; } + + const onTextChange = newText => { + const found = trie.get(env.name2dyn)(newText); + setState(state => ({...state, text: newText, resolved: found})); + } + // fired before onInput const onKeyDown = (e: React.KeyboardEvent) => { const fns = { @@ -86,7 +83,7 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp // not shift key if (singleSuggestion.length > 0) { const newText = text + singleSuggestion; - setText(newText); + onTextChange(newText); setRightMostCaretPosition(inputRef.current); e.preventDefault(); } @@ -105,7 +102,7 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp e.preventDefault(); }, ArrowLeft: () => { - if (getCaretPosition() === 0) { + if (getCaretPosition() <= 0) { focusPrevElement(); e.preventDefault(); } @@ -131,21 +128,11 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp }; const onInput = e => { - const found = trie.get(env.name2dyn)(e.target.value); - if (found) { - console.log('resolving input block..', e.target.value); - onResolve({...state, text: e.target.value, resolved: found}); - } - else { - if (resolved) { - // un-resolve - console.log('un-resolving input block..', e.target.value); - onResolve({...state, text: e.target.value, resolved: undefined}); - } - else { - setText(e.target.value); - } - } + onTextChange(e.target.value); + }; + + const onSelectSuggestion = ([name, dynamic]) => { + setState(state => ({...state, text: name, resolved: dynamic})); }; return diff --git a/src/LetInBlock.tsx b/src/LetInBlock.tsx index 84e38e8..3d5733a 100644 --- a/src/LetInBlock.tsx +++ b/src/LetInBlock.tsx @@ -16,20 +16,19 @@ export interface LetInBlockState { } interface LetInBlockProps extends State2Props { - onResolve: (resolved: EditorState) => void; } -export function LetInBlock({state, setState, onResolve}: LetInBlockProps) { +export function LetInBlock({state, setState}: LetInBlockProps) { const {name, value, inner} = state; const env = useContext(EnvContext); const nameRef = useRef(null); - const setInner = inner => setState({...state, inner}); - const setValue = value => setState({...state, value}); + const setInner = inner => setState(state => ({...state, inner})); + const setValue = value => setState(state => ({...state, value})); const onChangeName = (e: React.ChangeEvent) => { - setState({...state, name: e.target.value}); + setState(state => ({...state, name: e.target.value})); } useEffect(() => { @@ -55,7 +54,6 @@ export function LetInBlock({state, setState, onResolve}: LetInBlockProps) { state={value} setState={setValue} filter={() => true} - onResolve={() => {}} onCancel={() => {}} />  in @@ -66,7 +64,6 @@ export function LetInBlock({state, setState, onResolve}: LetInBlockProps) { state={inner} setState={setInner} filter={() => true} - onResolve={onResolve} onCancel={() => {}} /> diff --git a/src/Value.tsx b/src/Value.tsx index 89eb9f7..b9e0be0 100644 --- a/src/Value.tsx +++ b/src/Value.tsx @@ -1,4 +1,4 @@ -import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict} from "dope2"; +import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool} from "dope2"; import "./Value.css"; @@ -11,6 +11,9 @@ export function Value({dynamic}) { if (eqType(type)(Int)) { return ; } + if (eqType(type)(Bool)) { + return ; + } const symbol = getSymbol(type); switch (symbol) { @@ -48,6 +51,9 @@ function ValueInt({val}) { function ValueFunction() { return <>𝑓𝑛 ; } +function ValueBool({val}) { + return {val.toString()}; +} // function Sum({val, elemType}) { // return // } diff --git a/src/util/extra.ts b/src/util/extra.ts index f2f0dde..a00e315 100644 --- a/src/util/extra.ts +++ b/src/util/extra.ts @@ -5,9 +5,9 @@ export interface Dynamic { t: any; } -export interface State2Props { - state: T; - // setState: (callback: (state: T) => T) => void; - // setState: (state: T) => void; - setState: (state: EditorState) => void; +export type SetStateFn = (state: InType) => OutType; + +export interface State2Props { + state: InType; + setState: (callback: SetStateFn) => void; }