greatly simplified app
This commit is contained in:
parent
9c0c2dab90
commit
35d1034c67
8 changed files with 156 additions and 204 deletions
|
|
@ -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<CallBlockState<FnState,InputState>> {
|
||||
onResolve: (resolved: EditorState) => void;
|
||||
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
|
||||
}
|
||||
|
||||
function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
|
||||
const [unifyError, setUnifyError] = useState<typeof UnifyError | undefined>(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<ResolvedType>) => {
|
||||
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 <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
|
||||
export function CallBlock({ state, setState }: CallBlockProps) {
|
||||
const {setFn, setInput, onFnCancel, onInputCancel}
|
||||
= headlessCallBlock({ state, setState });
|
||||
return <span className={"functionBlock" + ((state.resolved instanceof Error) ? " unifyError" : "")}>
|
||||
<FunctionHeader
|
||||
fn={state.fn}
|
||||
setFn={setFn}
|
||||
onFnResolve={onFnResolve}
|
||||
onFnCancel={onFnCancel}
|
||||
input={state.input} />
|
||||
<div className="functionParams">
|
||||
|
|
@ -105,113 +91,93 @@ export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
|
|||
<InputParams
|
||||
fn={state.fn} setFn={setFn}
|
||||
input={state.input} setInput={setInput}
|
||||
onFnResolve={onFnResolve}
|
||||
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
|
||||
|
||||
onInputCancel={onInputCancel} />
|
||||
{/* Output (or Error) */}
|
||||
{state.resolved && <Value dynamic={state.resolved} />}
|
||||
{ state.resolved && <>☑</>}
|
||||
{unifyError && unifyError.toString()}
|
||||
{ state.resolved instanceof Error && state.resolved.toString()
|
||||
|| state.resolved && <><Value dynamic={state.resolved} />☑</>}
|
||||
</div>
|
||||
</div>
|
||||
</span>;
|
||||
}
|
||||
|
||||
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 <FunctionHeader
|
||||
fn={fn.fn}
|
||||
setFn={fnFn => 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 <span className="functionName">
|
||||
𝑓𝑛
|
||||
<Editor
|
||||
state={fn}
|
||||
setState={setFn}
|
||||
onResolve={onFnResolve}
|
||||
onCancel={onFnCancel}
|
||||
filter={filterCompatibleFns} />
|
||||
filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} />
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
||||
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 <div className="inputParam">
|
||||
{(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:
|
||||
<NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} />
|
||||
// recurse:
|
||||
<NestedParams fn={fn} setFn={setFn}/>
|
||||
}
|
||||
{/* Our own input */}
|
||||
<Editor
|
||||
state={input}
|
||||
setState={setInput}
|
||||
onResolve={onInputResolve}
|
||||
onCancel={onInputCancel}
|
||||
filter={filterCompatibleInputs}
|
||||
filter={([_, inputCandidate]) => filterFnInputs(fn.resolved, inputCandidate)}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
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 <InputParams
|
||||
fn={fn.fn}
|
||||
setFn={fnFn => setFn({...fn, fn: fnFn})}
|
||||
setFn={setFnFn}
|
||||
input={fn.input}
|
||||
setInput={fnInput => setFn({...fn, input: fnInput})}
|
||||
onFnResolve={onFnFnResolve}
|
||||
onInputResolve={onFnInputResolve}
|
||||
setInput={setFnInput}
|
||||
onInputCancel={() => {/*todo*/}}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue