better-looking parameters
This commit is contained in:
parent
9afaa41fbb
commit
95eb8aef84
10 changed files with 306 additions and 229 deletions
98
src/App.tsx
98
src/App.tsx
|
|
@ -1,94 +1,16 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { Editor, initialEditorState, type EditorState } from './Editor'
|
import { Editor, type EditorState } from './Editor'
|
||||||
import { trie, apply, Int } from "dope2";
|
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
|
||||||
|
|
||||||
const listPush = trie.get(initialEditorState.env.name2dyn)("list.push");
|
|
||||||
const listEmptyList = trie.get(initialEditorState.env.name2dyn)("list.emptyList");
|
|
||||||
const fourtyTwo = {i: 42n, t: Int};
|
|
||||||
|
|
||||||
const nonEmptyEditorState: EditorState = {
|
|
||||||
kind: "call",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
fn: {
|
|
||||||
kind: "call",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
fn: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "list.push",
|
|
||||||
resolved: listPush,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "list.emptyList",
|
|
||||||
resolved: listEmptyList,
|
|
||||||
},
|
|
||||||
resolved: apply(listEmptyList)(listPush),
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "42",
|
|
||||||
resolved: fourtyTwo,
|
|
||||||
},
|
|
||||||
resolved: apply(fourtyTwo)(apply(listEmptyList)(listPush)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const functionWith3Params = trie.get(initialEditorState.env.name2dyn)("functionWith3Params");
|
|
||||||
const fourtyThree = {i: 43n, t: Int};
|
|
||||||
const fourtyFour = {i: 44n, t: Int};
|
|
||||||
|
|
||||||
const tripleFunctionCallEditorState: EditorState = {
|
|
||||||
kind: "call",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
fn: {
|
|
||||||
kind: "call",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
fn: {
|
|
||||||
kind: "call",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
fn: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "functionWith3Params",
|
|
||||||
resolved: functionWith3Params,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "42",
|
|
||||||
resolved: fourtyTwo,
|
|
||||||
},
|
|
||||||
resolved: apply(fourtyTwo)(functionWith3Params),
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "43",
|
|
||||||
resolved: fourtyThree,
|
|
||||||
},
|
|
||||||
resolved: apply(fourtyThree)(apply(fourtyTwo)(functionWith3Params)),
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
kind: "input",
|
|
||||||
env: initialEditorState.env,
|
|
||||||
text: "44",
|
|
||||||
resolved: fourtyFour,
|
|
||||||
},
|
|
||||||
resolved: apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith3Params))),
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = (s: EditorState) => {
|
const pushHistory = (s: EditorState) => {
|
||||||
console.log('pushHistory');
|
|
||||||
setHistory(history.concat([s]));
|
setHistory(history.concat([s]));
|
||||||
setFuture([]);
|
setFuture([]);
|
||||||
};
|
};
|
||||||
|
|
@ -118,10 +40,8 @@ export function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window['APP_STATE'] = history;
|
window['APP_STATE'] = history; // useful for debugging
|
||||||
// console.log("EDITOR STATE:", state);
|
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -139,8 +59,10 @@ export function App() {
|
||||||
<Editor
|
<Editor
|
||||||
state={history.at(-1)!}
|
state={history.at(-1)!}
|
||||||
setState={pushHistory}
|
setState={pushHistory}
|
||||||
onResolve={() => {console.log("toplevel resolved")}}
|
onResolve={() => {}}
|
||||||
onCancel={() => {console.log("toplevel canceled")}}
|
onCancel={() => {}}
|
||||||
|
filter={() => true}
|
||||||
|
focus={true}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
.functionBlock {
|
.functionBlock {
|
||||||
border: solid 1px darkgray;
|
border: solid 1px darkgray;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 1px;
|
margin: 4px;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.functionBlock.unifyError {
|
.functionBlock.unifyError {
|
||||||
|
|
@ -18,37 +19,43 @@
|
||||||
/* background-color: pink; */
|
/* background-color: pink; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.inputParam:after {
|
.inputParam:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: solid 10px transparent;
|
border: solid transparent;
|
||||||
border-left-color: rgb(242, 253, 146);
|
border-width: 10px;
|
||||||
margin-left: 0px;
|
right: -19px;
|
||||||
/* z-index: 1; */
|
top: 0;
|
||||||
|
bottom:0;
|
||||||
}
|
}
|
||||||
.inputParam {
|
.inputParam {
|
||||||
/* height: 20px; */
|
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative; /* to ensure the :after (which is absolute) is relative to ourselves */
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Count nested level AFTER .outputParam (resets the depth) */
|
||||||
|
.outputParam > .inputParam:after {
|
||||||
|
border-left-color: rgb(242, 253, 146);
|
||||||
|
}
|
||||||
|
.outputParam > .inputParam {
|
||||||
background-color: rgb(242, 253, 146);
|
background-color: rgb(242, 253, 146);
|
||||||
}
|
}
|
||||||
|
.outputParam > .inputParam > .inputParam {
|
||||||
|
|
||||||
.inputParam .inputParam {
|
|
||||||
background-color: rgb(180, 248, 214);
|
background-color: rgb(180, 248, 214);
|
||||||
}
|
}
|
||||||
.inputParam .inputParam:after {
|
.outputParam > .inputParam > .inputParam:after {
|
||||||
border-left-color: rgb(180, 248, 214);
|
border-left-color: rgb(180, 248, 214);
|
||||||
}
|
}
|
||||||
.inputParam .inputParam .inputParam {
|
.outputParam > .inputParam > .inputParam > .inputParam {
|
||||||
background-color: rgb(153, 212, 214);
|
background-color: rgb(153, 212, 214);
|
||||||
}
|
}
|
||||||
.inputParam .inputParam .inputParam:after {
|
.outputParam > .inputParam > .inputParam > .inputParam:after {
|
||||||
border-left-color: rgb(153, 212, 214);
|
border-left-color: rgb(153, 212, 214);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.typeAnnot {
|
.typeAnnot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
@ -67,14 +74,21 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.functionBlock.unifyError .inputParam {
|
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam {
|
||||||
background-color: darkred;
|
background-color: darkred;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.functionBlock.unifyError .inputParam:after {
|
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam:after {
|
||||||
border-left-color: darkred;
|
border-left-color: darkred;
|
||||||
}
|
}
|
||||||
|
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
|
||||||
|
background-color: rgb(95, 4, 4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
|
||||||
|
border-left-color: rgb(95, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
.functionBlock.unifyError .outputParam {
|
.functionBlock.unifyError > .functionParams > .outputParam {
|
||||||
background-color: pink;
|
background-color: pink;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { apply, UnifyError } from "dope2";
|
import { apply, UnifyError, assignFn, getType } from "dope2";
|
||||||
|
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
import { Value } from "./Value";
|
import { Value } from "./Value";
|
||||||
|
|
@ -17,6 +17,7 @@ export interface CallBlockState<
|
||||||
fn: FnState;
|
fn: FnState;
|
||||||
input: InputState;
|
input: InputState;
|
||||||
resolved: undefined | Dynamic;
|
resolved: undefined | Dynamic;
|
||||||
|
// focus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CallBlockProps<
|
interface CallBlockProps<
|
||||||
|
|
@ -26,25 +27,24 @@ interface CallBlockProps<
|
||||||
onResolve: (resolved: EditorState) => void;
|
onResolve: (resolved: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function headlessCallBlock({state: {kind, env, fn, input, resolved }, setState, onResolve}: CallBlockProps) {
|
function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
|
||||||
const [unifyError, setUnifyError] = useState<UnifyError | undefined>(undefined);
|
const [unifyError, setUnifyError] = useState<UnifyError | undefined>(undefined);
|
||||||
|
const {fn, input } = state;
|
||||||
const setFn = (fn: EditorState) => {
|
const setFn = (fn: EditorState) => {
|
||||||
setState({kind, env, fn, input, resolved});
|
setState({...state, fn});
|
||||||
}
|
}
|
||||||
const setInput = (input: EditorState) => {
|
const setInput = (input: EditorState) => {
|
||||||
setState({kind, env, fn, input, resolved});
|
setState({...state, input});
|
||||||
}
|
}
|
||||||
const setResolved = (resolved?: Dynamic) => {
|
const setResolved = (resolved?: Dynamic) => {
|
||||||
setState({kind, env, fn, input, resolved});
|
setState({...state, resolved});
|
||||||
}
|
}
|
||||||
const makeTheCall = (input, fn) => {
|
const makeTheCall = (input, fn) => {
|
||||||
console.log('makeTheCall...')
|
|
||||||
try {
|
try {
|
||||||
const outputResolved = apply(input.resolved)(fn.resolved);
|
const outputResolved = apply(input.resolved)(fn.resolved);
|
||||||
setResolved(outputResolved);
|
setResolved(outputResolved);
|
||||||
console.log("onResolve callblock..")
|
|
||||||
onResolve({
|
onResolve({
|
||||||
kind, env, fn, input, resolved: outputResolved
|
...state, resolved: outputResolved
|
||||||
});
|
});
|
||||||
setUnifyError(undefined);
|
setUnifyError(undefined);
|
||||||
}
|
}
|
||||||
|
|
@ -54,12 +54,11 @@ function headlessCallBlock({state: {kind, env, fn, input, resolved }, setState,
|
||||||
}
|
}
|
||||||
setUnifyError(e);
|
setUnifyError(e);
|
||||||
onResolve({
|
onResolve({
|
||||||
kind, env, fn, input, resolved: undefined
|
...state, resolved: undefined
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onFnResolve = (fnState) => {
|
const onFnResolve = (fnState) => {
|
||||||
console.log('my fn resolved')
|
|
||||||
if (input.resolved) {
|
if (input.resolved) {
|
||||||
makeTheCall(input, fnState);
|
makeTheCall(input, fnState);
|
||||||
}
|
}
|
||||||
|
|
@ -67,20 +66,18 @@ function headlessCallBlock({state: {kind, env, fn, input, resolved }, setState,
|
||||||
// setFn(fnState);
|
// setFn(fnState);
|
||||||
setResolved(undefined);
|
setResolved(undefined);
|
||||||
onResolve({
|
onResolve({
|
||||||
kind, env, fn: fnState, input, resolved: undefined
|
...state, resolved: undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onInputResolve = (inputState) => {
|
const onInputResolve = (inputState) => {
|
||||||
console.log('my input resolved')
|
|
||||||
if (fn.resolved) {
|
if (fn.resolved) {
|
||||||
makeTheCall(inputState, fn);
|
makeTheCall(inputState, fn);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// setInput(inputState);
|
|
||||||
setResolved(undefined);
|
setResolved(undefined);
|
||||||
onResolve({
|
onResolve({
|
||||||
kind, env, fn, input: inputState, resolved: undefined
|
...state, resolved: undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -90,24 +87,11 @@ function headlessCallBlock({state: {kind, env, fn, input, resolved }, setState,
|
||||||
const onInputCancel = () => {
|
const onInputCancel = () => {
|
||||||
setState(fn);
|
setState(fn);
|
||||||
}
|
}
|
||||||
// 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 {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel};
|
return {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
|
export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
|
||||||
const {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel}
|
const {unifyError, setFn, setInput, onFnResolve, onInputResolve, onInputCancel}
|
||||||
= headlessCallBlock({ state, setState, onResolve });
|
= headlessCallBlock({ state, setState, onResolve });
|
||||||
return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
|
return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
|
||||||
<FunctionHeader
|
<FunctionHeader
|
||||||
|
|
@ -120,6 +104,7 @@ export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
|
||||||
<InputParams
|
<InputParams
|
||||||
fn={state.fn} setFn={setFn}
|
fn={state.fn} setFn={setFn}
|
||||||
input={state.input} setInput={setInput}
|
input={state.input} setInput={setInput}
|
||||||
|
focus={true}
|
||||||
onFnResolve={onFnResolve}
|
onFnResolve={onFnResolve}
|
||||||
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
|
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
|
||||||
|
|
||||||
|
|
@ -153,38 +138,59 @@ function FunctionHeader({ fn, setFn, onFnResolve }) {
|
||||||
<Editor
|
<Editor
|
||||||
state={fn}
|
state={fn}
|
||||||
setState={setFn}
|
setState={setFn}
|
||||||
|
focus={false}
|
||||||
onResolve={onFnResolve}
|
onResolve={onFnResolve}
|
||||||
onCancel={() => {/*todo*/}}/>
|
onCancel={() => {/*todo*/}}
|
||||||
|
filter={() => true} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) {
|
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel, focus }) {
|
||||||
const {
|
const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => {
|
||||||
onInputResolve: onFnInputResolve,
|
if (fn.resolved) {
|
||||||
onFnResolve : onFnFnResolve
|
try {
|
||||||
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
|
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
|
||||||
|
|
||||||
// Input(s) of the function we're calling:
|
// Input(s) of the function we're calling:
|
||||||
<InputParams
|
<NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} />
|
||||||
fn={fn.fn}
|
|
||||||
setFn={fnFn => setFn({...fn, fn: fnFn})}
|
|
||||||
input={fn.input}
|
|
||||||
setInput={fnInput => setFn({...fn, input: fnInput})}
|
|
||||||
onFnResolve={onFnFnResolve}
|
|
||||||
onInputResolve={onFnInputResolve}
|
|
||||||
onInputCancel={() => {/*todo*/}}/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* Our own input */}
|
{/* Our own input */}
|
||||||
<Editor
|
<Editor
|
||||||
state={input}
|
state={input}
|
||||||
setState={setInput}
|
setState={setInput}
|
||||||
onResolve={onInputResolve}
|
onResolve={onInputResolve}
|
||||||
onCancel={onInputCancel} />
|
onCancel={onInputCancel}
|
||||||
|
filter={filterCompatibleInputs}
|
||||||
|
focus={focus} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function NestedInputParams({fn, setFn, onFnResolve}) {
|
||||||
|
const {
|
||||||
|
onInputResolve: onFnInputResolve,
|
||||||
|
onFnResolve : onFnFnResolve,
|
||||||
|
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
|
||||||
|
return <InputParams
|
||||||
|
fn={fn.fn}
|
||||||
|
setFn={fnFn => setFn({...fn, fn: fnFn})}
|
||||||
|
input={fn.input}
|
||||||
|
setInput={fnInput => setFn({...fn, input: fnInput})}
|
||||||
|
onFnResolve={onFnFnResolve}
|
||||||
|
onInputResolve={onFnInputResolve}
|
||||||
|
onInputCancel={() => {/*todo*/}}
|
||||||
|
focus={false}/>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
.typeSignature {
|
.typeSignature {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
/* vertical-align:; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.command {
|
.command {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,16 @@
|
||||||
import { getSymbol, getType, module2Env, ModuleStd, symbolFunction, getDefaultTypeParser } from "dope2";
|
import { getSymbol, getType, symbolFunction } from "dope2";
|
||||||
|
|
||||||
import { InputBlock, type InputBlockState } from "./InputBlock";
|
import { useEffect, useReducer, useRef, useState } from "react";
|
||||||
import { type Dynamic, type State2Props } from "./util/extra";
|
|
||||||
import { CallBlock, type CallBlockState } from "./CallBlock";
|
import { CallBlock, type CallBlockState } from "./CallBlock";
|
||||||
import { useEffect, useState } from "react";
|
import { InputBlock, type InputBlockState } from "./InputBlock";
|
||||||
import { Type } from "./Type";
|
import { Type } from "./Type";
|
||||||
|
import { type Dynamic, type State2Props } from "./util/extra";
|
||||||
|
|
||||||
import "./Editor.css"
|
import "./Editor.css";
|
||||||
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
|
||||||
import type { LetInBlockState } from "./LetInBlock";
|
import type { LetInBlockState } from "./LetInBlock";
|
||||||
|
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
||||||
interface LambdaBlockState {
|
import type { LambdaBlockState } from "./LambdaBlock";
|
||||||
kind: "lambda";
|
import { initialEditorState } from "./configurations";
|
||||||
env: any;
|
|
||||||
paramName: string;
|
|
||||||
expr: EditorState;
|
|
||||||
resolved: undefined | Dynamic;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EditorState =
|
export type EditorState =
|
||||||
InputBlockState
|
InputBlockState
|
||||||
|
|
@ -24,33 +18,42 @@ export type EditorState =
|
||||||
| LetInBlockState
|
| LetInBlockState
|
||||||
| LambdaBlockState;
|
| LambdaBlockState;
|
||||||
|
|
||||||
const mkType = getDefaultTypeParser();
|
|
||||||
export const initialEditorState: EditorState = {
|
|
||||||
kind: "input",
|
|
||||||
env: module2Env(ModuleStd.concat([
|
|
||||||
["functionWith3Params", {i: i=>j=>k=>i+j+k, t: mkType("Int->Int->Int->Int")}],
|
|
||||||
])),
|
|
||||||
text: "",
|
|
||||||
resolved: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EditorProps extends State2Props<EditorState> {
|
interface EditorProps extends State2Props<EditorState> {
|
||||||
|
focus: boolean;
|
||||||
|
filter: (suggestion: [string, Dynamic]) => boolean;
|
||||||
onResolve: (state: EditorState) => void;
|
onResolve: (state: EditorState) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dontFilter = () => true;
|
|
||||||
|
|
||||||
function getCommands(type) {
|
function getCommands(type) {
|
||||||
const commands = ['u', 't', 'Enter', 'Backspace', 'ArrowLeft', 'Tab', 'l', '='];
|
const commands = ['u', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '='];
|
||||||
if (getSymbol(type) === symbolFunction) {
|
if (getSymbol(type) === symbolFunction) {
|
||||||
commands.push('c');
|
commands.push('c');
|
||||||
}
|
}
|
||||||
return commands;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
function removeFocus(state: EditorState): EditorState {
|
||||||
|
if (state.kind === "input") {
|
||||||
|
return {...state, focus: false};
|
||||||
|
}
|
||||||
|
if (state.kind === "call") {
|
||||||
|
return {...state,
|
||||||
|
fn: removeFocus(state.fn),
|
||||||
|
input: removeFocus(state.input),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Editor({state, setState, onResolve, onCancel, filter, focus}: EditorProps) {
|
||||||
const [needCommand, setNeedCommand] = useState(false);
|
const [needCommand, setNeedCommand] = useState(false);
|
||||||
|
const commandInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (needCommand) {
|
||||||
|
commandInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [needCommand]);
|
||||||
const onMyResolve = (editorState: EditorState) => {
|
const onMyResolve = (editorState: EditorState) => {
|
||||||
setState(editorState);
|
setState(editorState);
|
||||||
if (editorState.resolved) {
|
if (editorState.resolved) {
|
||||||
|
|
@ -82,10 +85,11 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
||||||
setState({
|
setState({
|
||||||
kind: "call",
|
kind: "call",
|
||||||
env: state.env,
|
env: state.env,
|
||||||
fn: state,
|
fn: removeFocus(state),
|
||||||
input: initialEditorState,
|
input: initialEditorState,
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
});
|
});
|
||||||
|
// focusNextElement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// t -> Transform
|
// t -> Transform
|
||||||
|
|
@ -95,7 +99,7 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
||||||
kind: "call",
|
kind: "call",
|
||||||
env: state.env,
|
env: state.env,
|
||||||
fn: initialEditorState,
|
fn: initialEditorState,
|
||||||
input: state,
|
input: removeFocus(state),
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -104,6 +108,10 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
||||||
focusPrevElement();
|
focusPrevElement();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (e.key === "ArrowRight") {
|
||||||
|
focusNextElement();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// l -> Let ... in ...
|
// l -> Let ... in ...
|
||||||
// = -> assign to name
|
// = -> assign to name
|
||||||
if (e.key === 'l' || e.key === '=') {
|
if (e.key === 'l' || e.key === '=') {
|
||||||
|
|
@ -123,9 +131,17 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
||||||
const renderBlock = () => {
|
const renderBlock = () => {
|
||||||
switch (state.kind) {
|
switch (state.kind) {
|
||||||
case "input":
|
case "input":
|
||||||
return <InputBlock state={state} setState={setState} filter={dontFilter} onResolve={onMyResolve} onCancel={onCancel} />;
|
return <InputBlock
|
||||||
|
state={state}
|
||||||
|
setState={setState}
|
||||||
|
filter={filter}
|
||||||
|
onResolve={onMyResolve}
|
||||||
|
onCancel={onCancel} />;
|
||||||
case "call":
|
case "call":
|
||||||
return <CallBlock state={state} setState={setState} onResolve={onMyResolve} />;
|
return <CallBlock
|
||||||
|
state={state}
|
||||||
|
setState={setState}
|
||||||
|
onResolve={onMyResolve} />;
|
||||||
case "let":
|
case "let":
|
||||||
return <></>;
|
return <></>;
|
||||||
case "lambda":
|
case "lambda":
|
||||||
|
|
@ -140,12 +156,13 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
||||||
:: <Type type={getType(state.resolved)} />
|
:: <Type type={getType(state.resolved)} />
|
||||||
{ (needCommand)
|
{ (needCommand)
|
||||||
? <input
|
? <input
|
||||||
|
ref={commandInputRef}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoFocus={true}
|
|
||||||
className="editable command"
|
className="editable command"
|
||||||
placeholder="<enter command>"
|
placeholder="<enter command>"
|
||||||
onKeyDown={onCommand}
|
onKeyDown={onCommand}
|
||||||
value=""/>
|
value={""}
|
||||||
|
onChange={() => {}} /> /* gets rid of React warning */
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Double, getType, Int, newDynamic, trie } from "dope2";
|
import { Double, getType, Int, newDynamic, trie } from "dope2";
|
||||||
import { focusNextElement, focusPrevElement, getCaretPosition, setRightMostCaretPosition } from "./util/dom_trickery";
|
import { focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
||||||
import { parseDouble, parseInt } from "./util/parse";
|
import { parseDouble, parseInt } from "./util/parse";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
@ -7,13 +7,13 @@ import { Type } from "./Type";
|
||||||
|
|
||||||
import "./InputBlock.css";
|
import "./InputBlock.css";
|
||||||
import type { Dynamic, State2Props } from "./util/extra";
|
import type { Dynamic, State2Props } from "./util/extra";
|
||||||
import type { EditorState } from "./Editor";
|
|
||||||
|
|
||||||
export interface InputBlockState {
|
export interface InputBlockState {
|
||||||
kind: "input";
|
kind: "input";
|
||||||
env: any;
|
env: any;
|
||||||
text: string;
|
text: string;
|
||||||
resolved: undefined | Dynamic;
|
resolved: undefined | Dynamic;
|
||||||
|
focus: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputBlockProps extends State2Props<InputBlockState> {
|
interface InputBlockProps extends State2Props<InputBlockState> {
|
||||||
|
|
@ -22,70 +22,73 @@ interface InputBlockProps extends State2Props<InputBlockState> {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBlock({ state: {kind, env, text, resolved}, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
const computeSuggestions = (text, env, filter) => {
|
||||||
const ref = useRef<any>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current?.focus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [i, setI] = useState(0); // selected suggestion
|
|
||||||
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
|
||||||
|
|
||||||
const setText = (text: string) => {
|
|
||||||
setState({kind, env, text, resolved});
|
|
||||||
}
|
|
||||||
const setResolved = (resolved: Dynamic) => {
|
|
||||||
setState({kind, env, text, resolved});
|
|
||||||
}
|
|
||||||
|
|
||||||
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
|
||||||
|
|
||||||
const asDouble = parseDouble(text);
|
const asDouble = parseDouble(text);
|
||||||
const asInt = parseInt(text);
|
const asInt = parseInt(text);
|
||||||
|
|
||||||
const suggestions = [
|
const ls = [
|
||||||
... (asDouble ? [[asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
|
... (asDouble ? [[asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
|
||||||
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
||||||
|
... trie.suggest(env.name2dyn)(text)(10),
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
...ls.filter(filter), // ones that match filter come first
|
||||||
|
...ls.filter(s => !filter(s)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
... (text !== '') ? trie.suggest(env.name2dyn)(text)(Infinity) : [],
|
export function InputBlock({ state, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
||||||
].filter(filter);
|
const {env, text, resolved, focus} = state;
|
||||||
|
const inputRef = useRef<HTMLInputElement>(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 = computeSuggestions(text, env, filter);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setI(0); // reset
|
setI(0); // reset
|
||||||
if (ref.current) {
|
if (inputRef.current) {
|
||||||
ref.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
inputRef.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
||||||
}
|
}
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focus) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [focus]);
|
||||||
|
|
||||||
const onSelectSuggestion = ([name, dynamic]) => {
|
const onSelectSuggestion = ([name, dynamic]) => {
|
||||||
// setText(name);
|
|
||||||
// ref.current.textContent = name;
|
|
||||||
// setRightMostCaretPosition(ref.current);
|
|
||||||
// setI(0);
|
|
||||||
// setResolved(dynamic);
|
|
||||||
console.log("onResolve inputblock..")
|
|
||||||
onResolve({
|
onResolve({
|
||||||
kind: "input",
|
kind: "input",
|
||||||
env,
|
env,
|
||||||
text: name,
|
text: name,
|
||||||
resolved: dynamic,
|
resolved: dynamic,
|
||||||
|
focus: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInput = e => {
|
const onInput = e => {
|
||||||
setText(e.target.value);
|
setText(e.target.value);
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
|
// un-resolve
|
||||||
onResolve({
|
onResolve({
|
||||||
kind: "input",
|
kind: "input",
|
||||||
env,
|
env,
|
||||||
text: e.target.value,
|
text: e.target.value,
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
|
focus: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCaretPosition = () => {
|
const getCaretPosition = () => {
|
||||||
return ref.current.selectionStart;
|
return inputRef.current?.selectionStart || -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
|
@ -100,8 +103,7 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
||||||
if (singleSuggestion.length > 0) {
|
if (singleSuggestion.length > 0) {
|
||||||
const newText = text + singleSuggestion;
|
const newText = text + singleSuggestion;
|
||||||
setText(newText);
|
setText(newText);
|
||||||
// ref.current.textContent = newText;
|
setRightMostCaretPosition(inputRef.current);
|
||||||
setRightMostCaretPosition(ref.current);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -156,7 +158,7 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{/* Input box */}
|
{/* Input box */}
|
||||||
<input ref={ref}
|
<input ref={inputRef}
|
||||||
placeholder="start typing..."
|
placeholder="start typing..."
|
||||||
className="editable"
|
className="editable"
|
||||||
value={text}
|
value={text}
|
||||||
|
|
@ -179,8 +181,7 @@ function Suggestions({ suggestions, onSelect, i, setI }) {
|
||||||
setI(j);
|
setI(j);
|
||||||
onSelect(suggestions[i]);
|
onSelect(suggestions[i]);
|
||||||
};
|
};
|
||||||
|
return <>{(suggestions.length > 0) &&
|
||||||
return (suggestions.length > 0) ?
|
|
||||||
<div className={"suggestions"}>
|
<div className={"suggestions"}>
|
||||||
{suggestions.map(([name, dynamic], j) =>
|
{suggestions.map(([name, dynamic], j) =>
|
||||||
<div
|
<div
|
||||||
|
|
@ -189,8 +190,7 @@ function Suggestions({ suggestions, onSelect, i, setI }) {
|
||||||
onMouseEnter={onMouseEnter(j)}
|
onMouseEnter={onMouseEnter(j)}
|
||||||
onMouseDown={onMouseDown(j)}>
|
onMouseDown={onMouseDown(j)}>
|
||||||
{name} :: <Type type={getType(dynamic)} />
|
{name} :: <Type type={getType(dynamic)} />
|
||||||
</div>)
|
</div>)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
: <></>;
|
}</>;
|
||||||
}
|
}
|
||||||
11
src/LambdaBlock.tsx
Normal file
11
src/LambdaBlock.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { EditorState } from "./Editor";
|
||||||
|
import type { Dynamic } from "./util/extra";
|
||||||
|
|
||||||
|
|
||||||
|
export interface LambdaBlockState {
|
||||||
|
kind: "lambda";
|
||||||
|
env: any;
|
||||||
|
paramName: string;
|
||||||
|
expr: EditorState;
|
||||||
|
resolved: undefined | Dynamic;
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ export function Value({dynamic}) {
|
||||||
// case symbolSet:
|
// case symbolSet:
|
||||||
// return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
|
// return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
|
||||||
case symbolList:
|
case symbolList:
|
||||||
return <List val={inst} elemType={type.params[0](type)} />;
|
return <ValueList val={inst} elemType={type.params[0](type)} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <>don't know how to show value</>;
|
return <>don't know how to show value</>;
|
||||||
|
|
@ -51,8 +51,8 @@ function ValueFunction() {
|
||||||
// function Sum({val, elemType}) {
|
// function Sum({val, elemType}) {
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
function List({val, elemType}) {
|
function ValueList({val, elemType}) {
|
||||||
return <span className="listType">[{val.map((v, i) => <Value dynamic={{i:v, t:elemType}}/>)}]</span>;
|
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||||
}
|
}
|
||||||
function ValueSum({val, leftType, rightType}) {
|
function ValueSum({val, leftType, rightType}) {
|
||||||
return match(val)
|
return match(val)
|
||||||
|
|
|
||||||
102
src/configurations.ts
Normal file
102
src/configurations.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { getDefaultTypeParser, module2Env, ModuleStd, trie, Int, apply } from "dope2";
|
||||||
|
import type { EditorState } from "./Editor";
|
||||||
|
|
||||||
|
const mkType = getDefaultTypeParser();
|
||||||
|
export const extendedEnv = module2Env(ModuleStd.concat([
|
||||||
|
["functionWith3Params", { i: i => j => k => i + j + k, t: mkType("Int->Int->Int->Int") }],
|
||||||
|
]));
|
||||||
|
|
||||||
|
export const initialEditorState: EditorState = {
|
||||||
|
kind: "input",
|
||||||
|
env: extendedEnv,
|
||||||
|
text: "",
|
||||||
|
resolved: undefined,
|
||||||
|
focus: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listPush = trie.get(initialEditorState.env.name2dyn)("list.push");
|
||||||
|
const listEmptyList = trie.get(initialEditorState.env.name2dyn)("list.emptyList");
|
||||||
|
const fourtyTwo = { i: 42n, t: Int };
|
||||||
|
|
||||||
|
export const nonEmptyEditorState: EditorState = {
|
||||||
|
kind: "call",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
fn: {
|
||||||
|
kind: "call",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
fn: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "list.push",
|
||||||
|
resolved: listPush,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "list.emptyList",
|
||||||
|
resolved: listEmptyList,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
resolved: apply(listEmptyList)(listPush),
|
||||||
|
// focus: ,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "42",
|
||||||
|
resolved: fourtyTwo,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
// resolved: apply(fourtyTwo)(apply(listEmptyList)(listPush)),
|
||||||
|
resolved: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const functionWith3Params = trie.get(initialEditorState.env.name2dyn)("functionWith3Params");
|
||||||
|
const fourtyThree = { i: 43n, t: Int };
|
||||||
|
const fourtyFour = { i: 44n, t: Int };
|
||||||
|
|
||||||
|
export const tripleFunctionCallEditorState: EditorState = {
|
||||||
|
kind: "call",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
fn: {
|
||||||
|
kind: "call",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
fn: {
|
||||||
|
kind: "call",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
fn: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "functionWith3Params",
|
||||||
|
resolved: functionWith3Params,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "42",
|
||||||
|
resolved: fourtyTwo,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
resolved: apply(fourtyTwo)(functionWith3Params),
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "43",
|
||||||
|
resolved: fourtyThree,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
resolved: apply(fourtyThree)(apply(fourtyTwo)(functionWith3Params)),
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
kind: "input",
|
||||||
|
env: initialEditorState.env,
|
||||||
|
text: "44",
|
||||||
|
resolved: fourtyFour,
|
||||||
|
focus: false,
|
||||||
|
},
|
||||||
|
resolved: apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith3Params))),
|
||||||
|
};
|
||||||
|
|
||||||
6
todo.txt
Normal file
6
todo.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
- [DONE] figure out autofocus of text inputs to make app more usable
|
||||||
|
- maybe in a small example, see if focus can be controlled by react state?
|
||||||
|
|
||||||
|
- add 'let ... in...' block
|
||||||
|
|
||||||
|
- draw flow of events on a piece of paper to see if things can be simplified
|
||||||
Loading…
Add table
Add a link
Reference in a new issue