Compare commits
No commits in common. "35d1034c6774ae6ae05ea2dbf392f19a0750258b" and "897824e07d939df8bc3fb4527c255fad0669529b" have entirely different histories.
35d1034c67
...
897824e07d
11 changed files with 222 additions and 214 deletions
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -10,7 +10,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
dope2:
|
dope2:
|
||||||
specifier: git+https://deemz.org/git/joeri/dope2.git
|
specifier: git+https://deemz.org/git/joeri/dope2.git
|
||||||
version: git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994
|
version: git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
|
|
@ -633,8 +633,8 @@ packages:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
|
dope2@git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890:
|
||||||
resolution: {commit: 443a13998dc3eccab26c27bee4fa056cdbc8f994, repo: https://deemz.org/git/joeri/dope2.git, type: git}
|
resolution: {commit: e631f11aa52b2adda8809c1b0b41cc991fbe8890, repo: https://deemz.org/git/joeri/dope2.git, type: git}
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
|
|
@ -1762,7 +1762,7 @@ snapshots:
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
|
dope2@git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890:
|
||||||
dependencies:
|
dependencies:
|
||||||
functional-red-black-tree: 1.0.1
|
functional-red-black-tree: 1.0.1
|
||||||
|
|
||||||
|
|
|
||||||
25
src/App.tsx
25
src/App.tsx
|
|
@ -6,15 +6,14 @@ import { CommandContext } from './CommandContext';
|
||||||
import { EnvContext } from './EnvContext';
|
import { EnvContext } from './EnvContext';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
// const [history, setHistory] = useState([initialEditorState]);
|
const [history, setHistory] = useState([initialEditorState]);
|
||||||
// const [history, setHistory] = useState([nonEmptyEditorState]);
|
// const [history, setHistory] = useState([nonEmptyEditorState]);
|
||||||
const [history, setHistory] = useState([tripleFunctionCallEditorState]);
|
// const [history, setHistory] = useState([tripleFunctionCallEditorState]);
|
||||||
|
|
||||||
const [future, setFuture] = useState<EditorState[]>([]);
|
const [future, setFuture] = useState<EditorState[]>([]);
|
||||||
|
|
||||||
const pushHistory = (callback: (p: EditorState) => EditorState) => {
|
const pushHistory = (s: EditorState) => {
|
||||||
const newState = callback(history.at(-1)!);
|
setHistory(history.concat([s]));
|
||||||
setHistory(history.concat([newState]));
|
|
||||||
setFuture([]);
|
setFuture([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,11 +50,11 @@ export function App() {
|
||||||
window.onkeydown = onKeyDown;
|
window.onkeydown = onKeyDown;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const commands: [string, string[], string][] = [
|
const commands = [
|
||||||
["call" , ['c' ], "call" ],
|
["call" , "[c] call" ],
|
||||||
["eval" , ['e','Tab','Enter' ], "eval" ],
|
["eval" , "[e] [Tab] [Enter] eval"],
|
||||||
["transform", ['t', '.' ], "transform" ],
|
["transform", "[t] [.] transform" ],
|
||||||
["let" , ['l', '=', 'a' ], "let ... in ..."],
|
["let" , "[l] [=] let ... in ..." ],
|
||||||
];
|
];
|
||||||
|
|
||||||
const [highlighted, setHighlighted] = useState(
|
const [highlighted, setHighlighted] = useState(
|
||||||
|
|
@ -75,9 +74,8 @@ export function App() {
|
||||||
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) [Ctrl+Shift+Z]</button>
|
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) [Ctrl+Shift+Z]</button>
|
||||||
Commands:
|
Commands:
|
||||||
{
|
{
|
||||||
commands.map(([_, keys, descr], i) =>
|
commands.map(([_, descr], i) =>
|
||||||
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
|
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
|
||||||
{keys.map((key, j) => <kbd key={j}>{key}</kbd>)}
|
|
||||||
{descr}
|
{descr}
|
||||||
</span>)
|
</span>)
|
||||||
}
|
}
|
||||||
|
|
@ -88,12 +86,11 @@ export function App() {
|
||||||
<Editor
|
<Editor
|
||||||
state={history.at(-1)!}
|
state={history.at(-1)!}
|
||||||
setState={pushHistory}
|
setState={pushHistory}
|
||||||
|
onResolve={() => {}}
|
||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
filter={() => true}
|
filter={() => true}
|
||||||
/>
|
/>
|
||||||
</CommandContext>
|
</CommandContext>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
||||||
|
|
@ -73,26 +73,12 @@
|
||||||
border-left-color: darkred;
|
border-left-color: darkred;
|
||||||
}
|
}
|
||||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
|
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
|
||||||
/* background-color: rgb(95, 4, 4); */
|
background-color: rgb(95, 4, 4);
|
||||||
background-color: pink;
|
color: white;
|
||||||
/* color: white; */
|
|
||||||
color: black;
|
|
||||||
}
|
}
|
||||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
|
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
|
||||||
/* border-left-color: rgb(95, 4, 4); */
|
border-left-color: rgb(95, 4, 4);
|
||||||
border-left-color: pink;
|
|
||||||
}
|
}
|
||||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam {
|
|
||||||
/* background-color: rgb(95, 4, 4); */
|
|
||||||
background-color: pink;
|
|
||||||
/* color: white; */
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam:after {
|
|
||||||
/* border-left-color: rgb(95, 4, 4); */
|
|
||||||
border-left-color: pink;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.functionBlock.unifyError > .functionParams > .outputParam {
|
.functionBlock.unifyError > .functionParams > .outputParam {
|
||||||
background-color: pink;
|
background-color: pink;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction, UnifyError } from "dope2";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { apply, UnifyError, assignFn, getType, getSymbol, symbolFunction } from "dope2";
|
||||||
|
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
import { Value } from "./Value";
|
import { Value } from "./Value";
|
||||||
import type { Dynamic, SetStateFn, State2Props } from "./util/extra";
|
import type { Dynamic, State2Props } from "./util/extra";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import "./CallBlock.css";
|
import "./CallBlock.css";
|
||||||
|
|
||||||
type ResolvedType = Dynamic | Error | undefined;
|
|
||||||
|
|
||||||
export interface CallBlockState<
|
export interface CallBlockState<
|
||||||
FnState=EditorState,
|
FnState=EditorState,
|
||||||
InputState=EditorState,
|
InputState=EditorState,
|
||||||
|
|
@ -16,74 +15,88 @@ export interface CallBlockState<
|
||||||
kind: "call";
|
kind: "call";
|
||||||
fn: FnState;
|
fn: FnState;
|
||||||
input: InputState;
|
input: InputState;
|
||||||
resolved: ResolvedType;
|
resolved: undefined | Dynamic;
|
||||||
|
// focus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CallBlockProps<
|
interface CallBlockProps<
|
||||||
FnState=EditorState,
|
FnState=EditorState,
|
||||||
InputState=EditorState,
|
InputState=EditorState,
|
||||||
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
|
> extends State2Props<CallBlockState<FnState,InputState>> {
|
||||||
|
onResolve: (resolved: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
|
||||||
function have(resolved: ResolvedType) {
|
const [unifyError, setUnifyError] = useState<typeof UnifyError | undefined>(undefined);
|
||||||
return resolved && !(resolved instanceof Error);
|
const {fn, input } = state;
|
||||||
}
|
const setFn = (fn: EditorState) => {
|
||||||
|
setState({...state, fn});
|
||||||
function headlessCallBlock({state, setState}: CallBlockProps) {
|
|
||||||
const {fn, input} = state;
|
|
||||||
const setFn = (callback: SetStateFn) => {
|
|
||||||
setState(state => ({...state, fn: callback(state.fn)}));
|
|
||||||
}
|
}
|
||||||
const setInput = (callback: SetStateFn) => {
|
const setInput = (input: EditorState) => {
|
||||||
setState(state => ({...state, input: callback(state.input)}));
|
setState({...state, input});
|
||||||
}
|
}
|
||||||
const setResolved = (callback: SetStateFn<ResolvedType>) => {
|
const setResolved = (resolved?: Dynamic) => {
|
||||||
setState(state => ({...state, resolved: callback(state.resolved)}));
|
setState({...state, resolved});
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
const makeTheCall = (input, fn) => {
|
||||||
// Here we do something spooky: we update the state in response to state change...
|
try {
|
||||||
// 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.
|
const outputResolved = apply(input.resolved)(fn.resolved);
|
||||||
if (have(input.resolved) && have(fn.resolved)) {
|
setResolved(outputResolved);
|
||||||
try {
|
onResolve({
|
||||||
const outputResolved = apply(input.resolved)(fn.resolved); // may throw
|
...state, resolved: outputResolved
|
||||||
setResolved(() => outputResolved); // success
|
});
|
||||||
}
|
setUnifyError(undefined);
|
||||||
catch (e) {
|
|
||||||
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
setResolved(() => e as Error); // eval error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (input.resolved instanceof Error) {
|
catch (e) {
|
||||||
setResolved(() => input.resolved); // bubble up the error
|
if (!(e instanceof UnifyError)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
setUnifyError(e as typeof UnifyError);
|
||||||
|
onResolve({
|
||||||
|
...state, resolved: undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
else if (fn.resolved instanceof Error) {
|
};
|
||||||
setResolved(() => fn.resolved); // bubble up the error
|
const onFnResolve = (fnState) => {
|
||||||
|
if (input.resolved) {
|
||||||
|
makeTheCall(input, fnState);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// no errors and at least one is undefined:
|
// setFn(fnState);
|
||||||
setResolved(() => undefined); // chill out
|
setResolved(undefined);
|
||||||
|
onResolve({
|
||||||
|
...state, resolved: undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [input.resolved, fn.resolved]);
|
};
|
||||||
|
const onInputResolve = (inputState) => {
|
||||||
|
if (fn.resolved) {
|
||||||
|
makeTheCall(inputState, fn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setResolved(undefined);
|
||||||
|
onResolve({
|
||||||
|
...state, resolved: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
const onFnCancel = () => {
|
const onFnCancel = () => {
|
||||||
setState(state => state.input); // we become our input
|
setState(input);
|
||||||
}
|
}
|
||||||
const onInputCancel = () => {
|
const onInputCancel = () => {
|
||||||
setState(state => state.fn); // we become our function
|
setState(fn);
|
||||||
}
|
}
|
||||||
return {setFn, setInput, onFnCancel, onInputCancel};
|
return {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallBlock({ state, setState }: CallBlockProps) {
|
export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
|
||||||
const {setFn, setInput, onFnCancel, onInputCancel}
|
const {unifyError, setFn, setInput, onFnResolve, onInputResolve, onInputCancel}
|
||||||
= headlessCallBlock({ state, setState });
|
= headlessCallBlock({ state, setState, onResolve });
|
||||||
return <span className={"functionBlock" + ((state.resolved instanceof Error) ? " unifyError" : "")}>
|
return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
|
||||||
<FunctionHeader
|
<FunctionHeader
|
||||||
fn={state.fn}
|
fn={state.fn}
|
||||||
setFn={setFn}
|
setFn={setFn}
|
||||||
onFnCancel={onFnCancel}
|
onFnResolve={onFnResolve}
|
||||||
input={state.input} />
|
input={state.input} />
|
||||||
<div className="functionParams">
|
<div className="functionParams">
|
||||||
<div className="outputParam">
|
<div className="outputParam">
|
||||||
|
|
@ -91,93 +104,110 @@ export function CallBlock({ state, setState }: CallBlockProps) {
|
||||||
<InputParams
|
<InputParams
|
||||||
fn={state.fn} setFn={setFn}
|
fn={state.fn} setFn={setFn}
|
||||||
input={state.input} setInput={setInput}
|
input={state.input} setInput={setInput}
|
||||||
onInputCancel={onInputCancel} />
|
onFnResolve={onFnResolve}
|
||||||
|
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
|
||||||
|
|
||||||
{/* Output (or Error) */}
|
{/* Output (or Error) */}
|
||||||
{ state.resolved instanceof Error && state.resolved.toString()
|
{state.resolved && <Value dynamic={state.resolved} />}
|
||||||
|| state.resolved && <><Value dynamic={state.resolved} />☑</>}
|
{unifyError && unifyError.toString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterFnInputs(fn: Dynamic|Error|undefined, input: Dynamic|Error|undefined) {
|
function FunctionHeader({ fn, setFn, input, onFnResolve }) {
|
||||||
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 (fn.kind === "call") {
|
||||||
// if the function we're calling is itself the result of a function 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
|
// then we are anonymous, and so we don't draw a function name
|
||||||
|
|
||||||
// recurse:
|
// recurse:
|
||||||
const {
|
const {
|
||||||
setFn : setFnFn,
|
onFnResolve : onFnFnResolve
|
||||||
onFnCancel : onFnFnCancel,
|
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
|
||||||
} = headlessCallBlock({state: fn, setState: setFn});
|
|
||||||
|
|
||||||
return <FunctionHeader
|
return <FunctionHeader
|
||||||
fn={fn.fn}
|
fn={fn.fn}
|
||||||
setFn={setFnFn}
|
setFn={fnFn => setFn({...fn, fn: fnFn})}
|
||||||
onFnCancel={onFnFnCancel}
|
onFnResolve={onFnFnResolve}
|
||||||
input={fn.input} />;
|
input={fn.input} />;
|
||||||
}
|
}
|
||||||
else {
|
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
|
// end of recursion - draw function name
|
||||||
return <span className="functionName">
|
return <div className="functionName">
|
||||||
𝑓𝑛
|
𝑓𝑛
|
||||||
<Editor
|
<Editor
|
||||||
state={fn}
|
state={fn}
|
||||||
setState={setFn}
|
setState={setFn}
|
||||||
onCancel={onFnCancel}
|
onResolve={onFnResolve}
|
||||||
filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} />
|
onCancel={() => {/*todo*/}}
|
||||||
</span>;
|
filter={filterCompatibleFns} />
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputParams({ fn, setFn, input, setInput, onInputCancel }) {
|
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;
|
||||||
|
}
|
||||||
return <div className="inputParam">
|
return <div className="inputParam">
|
||||||
{(fn.kind === "call") &&
|
{(fn.kind === "call") &&
|
||||||
// if the function we're calling is itself the result of a function 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
|
// then we render its input parameter nested in our own input parameter box, which is way more readable
|
||||||
|
|
||||||
// recurse:
|
// Input(s) of the function we're calling:
|
||||||
<NestedParams fn={fn} setFn={setFn}/>
|
<NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} />
|
||||||
}
|
}
|
||||||
{/* Our own input */}
|
{/* Our own input */}
|
||||||
<Editor
|
<Editor
|
||||||
state={input}
|
state={input}
|
||||||
setState={setInput}
|
setState={setInput}
|
||||||
|
onResolve={onInputResolve}
|
||||||
onCancel={onInputCancel}
|
onCancel={onInputCancel}
|
||||||
filter={([_, inputCandidate]) => filterFnInputs(fn.resolved, inputCandidate)}
|
filter={filterCompatibleInputs}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NestedParams({fn, setFn}) {
|
function NestedInputParams({fn, setFn, onFnResolve}) {
|
||||||
const {
|
const {
|
||||||
setFn : setFnFn,
|
onInputResolve: onFnInputResolve,
|
||||||
setInput : setFnInput,
|
onFnResolve : onFnFnResolve,
|
||||||
} = headlessCallBlock({state: fn, setState: setFn});
|
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
|
||||||
return <InputParams
|
return <InputParams
|
||||||
fn={fn.fn}
|
fn={fn.fn}
|
||||||
setFn={setFnFn}
|
setFn={fnFn => setFn({...fn, fn: fnFn})}
|
||||||
input={fn.input}
|
input={fn.input}
|
||||||
setInput={setFnInput}
|
setInput={fnInput => setFn({...fn, input: fnInput})}
|
||||||
|
onFnResolve={onFnFnResolve}
|
||||||
|
onInputResolve={onFnInputResolve}
|
||||||
onInputCancel={() => {/*todo*/}}
|
onInputCancel={() => {/*todo*/}}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandInput {
|
.commandInput {
|
||||||
width: 90px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ export type EditorState =
|
||||||
|
|
||||||
interface EditorProps extends State2Props<EditorState> {
|
interface EditorProps extends State2Props<EditorState> {
|
||||||
filter: (suggestion: [string, Dynamic]) => boolean;
|
filter: (suggestion: [string, Dynamic]) => boolean;
|
||||||
|
onResolve: (state: EditorState) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ function removeFocus(state: EditorState): EditorState {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
export function Editor({state, setState, onResolve, onCancel, filter}: EditorProps) {
|
||||||
const [needCommand, setNeedCommand] = useState(false);
|
const [needCommand, setNeedCommand] = useState(false);
|
||||||
const commandInputRef = useRef<HTMLInputElement>(null);
|
const commandInputRef = useRef<HTMLInputElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -59,27 +60,22 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
commandInputRef.current?.focus();
|
commandInputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [needCommand]);
|
}, [needCommand]);
|
||||||
// const onMyResolve = (editorState: EditorState) => {
|
const onMyResolve = (editorState: EditorState) => {
|
||||||
// setState(editorState);
|
setState(editorState);
|
||||||
// onResolve(editorState);
|
if (editorState.resolved) {
|
||||||
// return;
|
setNeedCommand(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
// if (editorState.resolved) {
|
// unresolved
|
||||||
// setNeedCommand(true);
|
setNeedCommand(false);
|
||||||
// }
|
onResolve(editorState); // pass up the fact that we're unresolved
|
||||||
// else {
|
}
|
||||||
// // unresolved
|
}
|
||||||
// setNeedCommand(false);
|
|
||||||
// onResolve(editorState); // pass up the fact that we're unresolved
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const globalContext = useContext(CommandContext);
|
const globalContext = useContext(CommandContext);
|
||||||
const onCommand = (e: React.KeyboardEvent) => {
|
const onCommand = (e: React.KeyboardEvent) => {
|
||||||
// const type = getType(state.resolved);
|
const type = getType(state.resolved);
|
||||||
// const commands = getCommands(type);
|
const commands = getCommands(type);
|
||||||
const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '=', '.', 'c'];
|
|
||||||
if (!commands.includes(e.key)) {
|
if (!commands.includes(e.key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +83,7 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
setNeedCommand(false);
|
setNeedCommand(false);
|
||||||
// u -> pass Up
|
// u -> pass Up
|
||||||
if (e.key === "e" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) {
|
if (e.key === "e" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) {
|
||||||
// onResolve(state);
|
onResolve(state);
|
||||||
globalContext?.doHighlight.eval();
|
globalContext?.doHighlight.eval();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -98,12 +94,12 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
// c -> Call
|
// c -> Call
|
||||||
if (e.key === "c") {
|
if (e.key === "c") {
|
||||||
// we become CallBlock
|
// we become CallBlock
|
||||||
setState(state => ({
|
setState({
|
||||||
kind: "call",
|
kind: "call",
|
||||||
fn: removeFocus(state),
|
fn: removeFocus(state),
|
||||||
input: initialEditorState,
|
input: initialEditorState,
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
}));
|
});
|
||||||
globalContext?.doHighlight.call();
|
globalContext?.doHighlight.call();
|
||||||
// focusNextElement();
|
// focusNextElement();
|
||||||
return;
|
return;
|
||||||
|
|
@ -111,12 +107,12 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
// t -> Transform
|
// t -> Transform
|
||||||
if (e.key === "t" || e.key === ".") {
|
if (e.key === "t" || e.key === ".") {
|
||||||
// we become CallBlock
|
// we become CallBlock
|
||||||
setState(state => ({
|
setState({
|
||||||
kind: "call",
|
kind: "call",
|
||||||
fn: initialEditorState,
|
fn: initialEditorState,
|
||||||
input: removeFocus(state),
|
input: removeFocus(state),
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
}));
|
});
|
||||||
globalContext?.doHighlight.transform();
|
globalContext?.doHighlight.transform();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -132,13 +128,13 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
// = -> assign to name
|
// = -> assign to name
|
||||||
if (e.key === 'l' || e.key === '=') {
|
if (e.key === 'l' || e.key === '=') {
|
||||||
// we become LetInBlock
|
// we become LetInBlock
|
||||||
setState(state => ({
|
setState({
|
||||||
kind: "let",
|
kind: "let",
|
||||||
inner: removeFocus(initialEditorState),
|
inner: removeFocus(initialEditorState),
|
||||||
name: "",
|
name: "",
|
||||||
value: removeFocus(state),
|
value: removeFocus(state),
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
}));
|
});
|
||||||
globalContext?.doHighlight.let();
|
globalContext?.doHighlight.let();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -149,19 +145,22 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
case "input":
|
case "input":
|
||||||
return <InputBlock
|
return <InputBlock
|
||||||
state={state}
|
state={state}
|
||||||
setState={setState as (callback:(p:InputBlockState)=>EditorState)=>void}
|
setState={setState}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
onResolve={onMyResolve}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
/>;
|
/>;
|
||||||
case "call":
|
case "call":
|
||||||
return <CallBlock
|
return <CallBlock
|
||||||
state={state}
|
state={state}
|
||||||
setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
|
setState={setState}
|
||||||
|
onResolve={onMyResolve}
|
||||||
/>;
|
/>;
|
||||||
case "let":
|
case "let":
|
||||||
return <LetInBlock
|
return <LetInBlock
|
||||||
state={state}
|
state={state}
|
||||||
setState={setState as (callback:(p:LetInBlockState)=>EditorState)=>void}
|
setState={setState}
|
||||||
|
onResolve={() => {}}
|
||||||
/>;
|
/>;
|
||||||
case "lambda":
|
case "lambda":
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
@ -170,19 +169,22 @@ export function Editor({state, setState, onCancel, filter}: EditorProps) {
|
||||||
return <>
|
return <>
|
||||||
{renderBlock()}
|
{renderBlock()}
|
||||||
{
|
{
|
||||||
(state.resolved && !(state.resolved instanceof Error))
|
(state.resolved)
|
||||||
? <div className="typeSignature">
|
? <div className="typeSignature">
|
||||||
:: <Type type={getType(state.resolved)} />
|
:: <Type type={getType(state.resolved)} />
|
||||||
|
{ (needCommand)
|
||||||
|
? <input
|
||||||
|
ref={commandInputRef}
|
||||||
|
spellCheck={false}
|
||||||
|
className="editable commandInput"
|
||||||
|
placeholder={`<command: ${getShortCommands(getType(state.resolved))}>`}
|
||||||
|
onKeyDown={onCommand}
|
||||||
|
value={""}
|
||||||
|
onChange={() => {}} /> /* gets rid of React warning */
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
<input
|
|
||||||
ref={commandInputRef}
|
|
||||||
spellCheck={false}
|
|
||||||
className="editable commandInput"
|
|
||||||
placeholder={`<command>`}
|
|
||||||
onKeyDown={onCommand}
|
|
||||||
value={""}
|
|
||||||
onChange={() => {}} />
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export interface InputBlockState {
|
||||||
|
|
||||||
interface InputBlockProps extends State2Props<InputBlockState> {
|
interface InputBlockProps extends State2Props<InputBlockState> {
|
||||||
filter: (suggestion: [string, Dynamic]) => boolean;
|
filter: (suggestion: [string, Dynamic]) => boolean;
|
||||||
|
onResolve: (state: InputBlockState) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +31,6 @@ const computeSuggestions = (text, env, filter) => {
|
||||||
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
||||||
... trie.suggest(env.name2dyn)(text)(Infinity),
|
... trie.suggest(env.name2dyn)(text)(Infinity),
|
||||||
]
|
]
|
||||||
return ls;
|
|
||||||
return [
|
return [
|
||||||
...ls.filter(filter), // ones that match filter come first
|
...ls.filter(filter), // ones that match filter come first
|
||||||
...ls.filter(s => !filter(s)),
|
...ls.filter(s => !filter(s)),
|
||||||
|
|
@ -38,13 +38,17 @@ const computeSuggestions = (text, env, filter) => {
|
||||||
// .slice(0,30);
|
// .slice(0,30);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBlock({ state, setState, filter, onCancel }: InputBlockProps) {
|
export function InputBlock({ state, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
||||||
const {text, resolved, focus} = state;
|
const {text, resolved, focus} = state;
|
||||||
const env = useContext(EnvContext);
|
const env = useContext(EnvContext);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [i, setI] = useState(0); // selected suggestion idx
|
const [i, setI] = useState(0); // selected suggestion idx
|
||||||
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
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 singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
||||||
const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]);
|
const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]);
|
||||||
|
|
||||||
|
|
@ -56,22 +60,32 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
}
|
}
|
||||||
}, [focus]);
|
}, [focus]);
|
||||||
|
|
||||||
useEffect(() => {
|
const onSelectSuggestion = ([name, dynamic]) => {
|
||||||
if (suggestions.length >= i) {
|
onResolve({
|
||||||
setI(0);
|
kind: "input",
|
||||||
|
text: name,
|
||||||
|
resolved: dynamic,
|
||||||
|
focus: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInput = e => {
|
||||||
|
setText(e.target.value);
|
||||||
|
if (resolved) {
|
||||||
|
// un-resolve
|
||||||
|
onResolve({
|
||||||
|
kind: "input",
|
||||||
|
text: e.target.value,
|
||||||
|
resolved: undefined,
|
||||||
|
focus: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [suggestions.length]);
|
};
|
||||||
|
|
||||||
const getCaretPosition = () => {
|
const getCaretPosition = () => {
|
||||||
return inputRef.current?.selectionStart || -1;
|
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 onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
const fns = {
|
const fns = {
|
||||||
Tab: () => {
|
Tab: () => {
|
||||||
|
|
@ -83,7 +97,7 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
// not shift key
|
// not shift key
|
||||||
if (singleSuggestion.length > 0) {
|
if (singleSuggestion.length > 0) {
|
||||||
const newText = text + singleSuggestion;
|
const newText = text + singleSuggestion;
|
||||||
onTextChange(newText);
|
setText(newText);
|
||||||
setRightMostCaretPosition(inputRef.current);
|
setRightMostCaretPosition(inputRef.current);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +116,7 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
},
|
},
|
||||||
ArrowLeft: () => {
|
ArrowLeft: () => {
|
||||||
if (getCaretPosition() <= 0) {
|
if (getCaretPosition() === 0) {
|
||||||
focusPrevElement();
|
focusPrevElement();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
@ -127,14 +141,6 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
fns[e.key]?.();
|
fns[e.key]?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInput = e => {
|
|
||||||
onTextChange(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelectSuggestion = ([name, dynamic]) => {
|
|
||||||
setState(state => ({...state, text: name, resolved: dynamic}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return <span>
|
return <span>
|
||||||
<span className="">
|
<span className="">
|
||||||
{/* Dropdown suggestions */}
|
{/* Dropdown suggestions */}
|
||||||
|
|
@ -158,7 +164,6 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
||||||
spellCheck={false}/>
|
spellCheck={false}/>
|
||||||
{/* Single 'grey' suggestion */}
|
{/* Single 'grey' suggestion */}
|
||||||
<span className="text-block suggest">{singleSuggestion}</span>
|
<span className="text-block suggest">{singleSuggestion}</span>
|
||||||
{ resolved && <>☑</>}
|
|
||||||
</span>
|
</span>
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,20 @@ export interface LetInBlockState {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LetInBlockProps extends State2Props<LetInBlockState> {
|
interface LetInBlockProps extends State2Props<LetInBlockState> {
|
||||||
|
onResolve: (resolved: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function LetInBlock({state, setState}: LetInBlockProps) {
|
export function LetInBlock({state, setState, onResolve}: LetInBlockProps) {
|
||||||
const {name, value, inner} = state;
|
const {name, value, inner} = state;
|
||||||
const env = useContext(EnvContext);
|
const env = useContext(EnvContext);
|
||||||
const nameRef = useRef<HTMLInputElement>(null);
|
const nameRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const setInner = inner => setState(state => ({...state, inner}));
|
const setInner = inner => setState({...state, inner});
|
||||||
const setValue = value => setState(state => ({...state, value}));
|
const setValue = value => setState({...state, value});
|
||||||
|
|
||||||
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setState(state => ({...state, name: e.target.value}));
|
setState({...state, name: e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -54,6 +55,7 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
|
||||||
state={value}
|
state={value}
|
||||||
setState={setValue}
|
setState={setValue}
|
||||||
filter={() => true}
|
filter={() => true}
|
||||||
|
onResolve={() => {}}
|
||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
/>
|
/>
|
||||||
<span className="keyword">in</span>
|
<span className="keyword">in</span>
|
||||||
|
|
@ -64,6 +66,7 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
|
||||||
state={inner}
|
state={inner}
|
||||||
setState={setInner}
|
setState={setInner}
|
||||||
filter={() => true}
|
filter={() => true}
|
||||||
|
onResolve={onResolve}
|
||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
/>
|
/>
|
||||||
</EnvContext>
|
</EnvContext>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool} from "dope2";
|
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict} from "dope2";
|
||||||
|
|
||||||
import "./Value.css";
|
import "./Value.css";
|
||||||
|
|
||||||
|
|
@ -11,9 +11,6 @@ export function Value({dynamic}) {
|
||||||
if (eqType(type)(Int)) {
|
if (eqType(type)(Int)) {
|
||||||
return <ValueInt val={inst}/>;
|
return <ValueInt val={inst}/>;
|
||||||
}
|
}
|
||||||
if (eqType(type)(Bool)) {
|
|
||||||
return <ValueBool val={inst}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const symbol = getSymbol(type);
|
const symbol = getSymbol(type);
|
||||||
switch (symbol) {
|
switch (symbol) {
|
||||||
|
|
@ -51,9 +48,6 @@ function ValueInt({val}) {
|
||||||
function ValueFunction() {
|
function ValueFunction() {
|
||||||
return <>𝑓𝑛 </>;
|
return <>𝑓𝑛 </>;
|
||||||
}
|
}
|
||||||
function ValueBool({val}) {
|
|
||||||
return <span className="valuePrimitive">{val.toString()}</span>;
|
|
||||||
}
|
|
||||||
// function Sum({val, elemType}) {
|
// function Sum({val, elemType}) {
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,3 @@ body {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-variation-settings: "wdth" 100;
|
font-variation-settings: "wdth" 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd {
|
|
||||||
border: 2px darkgrey;
|
|
||||||
color: rgb(63, 63, 63);
|
|
||||||
border-style: outset;
|
|
||||||
background-color: whitesmoke;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 0 2px 0 2px;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ export interface Dynamic {
|
||||||
t: any;
|
t: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
|
export interface State2Props<T> {
|
||||||
|
state: T;
|
||||||
export interface State2Props<InType,OutType=InType> {
|
// setState: (callback: (state: T) => T) => void;
|
||||||
state: InType;
|
// setState: (state: T) => void;
|
||||||
setState: (callback: SetStateFn<InType,OutType>) => void;
|
setState: (state: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue