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 { Editor, initialEditorState, type EditorState } from './Editor'
|
||||
import { trie, apply, Int } from "dope2";
|
||||
|
||||
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))),
|
||||
}
|
||||
import { Editor, type EditorState } from './Editor'
|
||||
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
|
||||
|
||||
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<EditorState[]>([]);
|
||||
|
||||
const pushHistory = (s: EditorState) => {
|
||||
console.log('pushHistory');
|
||||
setHistory(history.concat([s]));
|
||||
setFuture([]);
|
||||
};
|
||||
|
|
@ -118,10 +40,8 @@ export function App() {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
window['APP_STATE'] = history;
|
||||
// console.log("EDITOR STATE:", state);
|
||||
window['APP_STATE'] = history; // useful for debugging
|
||||
}, [history]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -139,8 +59,10 @@ export function App() {
|
|||
<Editor
|
||||
state={history.at(-1)!}
|
||||
setState={pushHistory}
|
||||
onResolve={() => {console.log("toplevel resolved")}}
|
||||
onCancel={() => {console.log("toplevel canceled")}}
|
||||
onResolve={() => {}}
|
||||
onCancel={() => {}}
|
||||
filter={() => true}
|
||||
focus={true}
|
||||
/>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
.functionBlock {
|
||||
border: solid 1px darkgray;
|
||||
display: inline-block;
|
||||
margin: 1px;
|
||||
margin: 4px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.functionBlock.unifyError {
|
||||
|
|
@ -18,37 +19,43 @@
|
|||
/* background-color: pink; */
|
||||
}
|
||||
|
||||
|
||||
.inputParam:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: solid 10px transparent;
|
||||
border-left-color: rgb(242, 253, 146);
|
||||
margin-left: 0px;
|
||||
/* z-index: 1; */
|
||||
border: solid transparent;
|
||||
border-width: 10px;
|
||||
right: -19px;
|
||||
top: 0;
|
||||
bottom:0;
|
||||
}
|
||||
.inputParam {
|
||||
/* height: 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);
|
||||
}
|
||||
|
||||
|
||||
.inputParam .inputParam {
|
||||
.outputParam > .inputParam > .inputParam {
|
||||
background-color: rgb(180, 248, 214);
|
||||
}
|
||||
.inputParam .inputParam:after {
|
||||
.outputParam > .inputParam > .inputParam:after {
|
||||
border-left-color: rgb(180, 248, 214);
|
||||
}
|
||||
.inputParam .inputParam .inputParam {
|
||||
.outputParam > .inputParam > .inputParam > .inputParam {
|
||||
background-color: rgb(153, 212, 214);
|
||||
}
|
||||
.inputParam .inputParam .inputParam:after {
|
||||
.outputParam > .inputParam > .inputParam > .inputParam:after {
|
||||
border-left-color: rgb(153, 212, 214);
|
||||
}
|
||||
|
||||
|
||||
.typeAnnot {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
|
@ -67,14 +74,21 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.functionBlock.unifyError .inputParam {
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam {
|
||||
background-color: darkred;
|
||||
color: white;
|
||||
}
|
||||
.functionBlock.unifyError .inputParam:after {
|
||||
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam:after {
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { apply, UnifyError } from "dope2";
|
||||
import { apply, UnifyError, assignFn, getType } from "dope2";
|
||||
|
||||
import { Editor, type EditorState } from "./Editor";
|
||||
import { Value } from "./Value";
|
||||
|
|
@ -17,6 +17,7 @@ export interface CallBlockState<
|
|||
fn: FnState;
|
||||
input: InputState;
|
||||
resolved: undefined | Dynamic;
|
||||
// focus: boolean;
|
||||
}
|
||||
|
||||
interface CallBlockProps<
|
||||
|
|
@ -26,25 +27,24 @@ interface CallBlockProps<
|
|||
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 {fn, input } = state;
|
||||
const setFn = (fn: EditorState) => {
|
||||
setState({kind, env, fn, input, resolved});
|
||||
setState({...state, fn});
|
||||
}
|
||||
const setInput = (input: EditorState) => {
|
||||
setState({kind, env, fn, input, resolved});
|
||||
setState({...state, input});
|
||||
}
|
||||
const setResolved = (resolved?: Dynamic) => {
|
||||
setState({kind, env, fn, input, resolved});
|
||||
setState({...state, resolved});
|
||||
}
|
||||
const makeTheCall = (input, fn) => {
|
||||
console.log('makeTheCall...')
|
||||
try {
|
||||
const outputResolved = apply(input.resolved)(fn.resolved);
|
||||
setResolved(outputResolved);
|
||||
console.log("onResolve callblock..")
|
||||
onResolve({
|
||||
kind, env, fn, input, resolved: outputResolved
|
||||
...state, resolved: outputResolved
|
||||
});
|
||||
setUnifyError(undefined);
|
||||
}
|
||||
|
|
@ -54,12 +54,11 @@ function headlessCallBlock({state: {kind, env, fn, input, resolved }, setState,
|
|||
}
|
||||
setUnifyError(e);
|
||||
onResolve({
|
||||
kind, env, fn, input, resolved: undefined
|
||||
...state, resolved: undefined
|
||||
})
|
||||
}
|
||||
};
|
||||
const onFnResolve = (fnState) => {
|
||||
console.log('my fn resolved')
|
||||
if (input.resolved) {
|
||||
makeTheCall(input, fnState);
|
||||
}
|
||||
|
|
@ -67,20 +66,18 @@ function headlessCallBlock({state: {kind, env, fn, input, resolved }, setState,
|
|||
// setFn(fnState);
|
||||
setResolved(undefined);
|
||||
onResolve({
|
||||
kind, env, fn: fnState, input, resolved: undefined
|
||||
...state, resolved: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
const onInputResolve = (inputState) => {
|
||||
console.log('my input resolved')
|
||||
if (fn.resolved) {
|
||||
makeTheCall(inputState, fn);
|
||||
}
|
||||
else {
|
||||
// setInput(inputState);
|
||||
setResolved(undefined);
|
||||
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 = () => {
|
||||
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};
|
||||
}
|
||||
|
||||
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 });
|
||||
return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
|
||||
<FunctionHeader
|
||||
|
|
@ -120,6 +104,7 @@ export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
|
|||
<InputParams
|
||||
fn={state.fn} setFn={setFn}
|
||||
input={state.input} setInput={setInput}
|
||||
focus={true}
|
||||
onFnResolve={onFnResolve}
|
||||
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
|
||||
|
||||
|
|
@ -153,38 +138,59 @@ function FunctionHeader({ fn, setFn, onFnResolve }) {
|
|||
<Editor
|
||||
state={fn}
|
||||
setState={setFn}
|
||||
focus={false}
|
||||
onResolve={onFnResolve}
|
||||
onCancel={() => {/*todo*/}}/>
|
||||
onCancel={() => {/*todo*/}}
|
||||
filter={() => true} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) {
|
||||
const {
|
||||
onInputResolve: onFnInputResolve,
|
||||
onFnResolve : onFnFnResolve
|
||||
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
|
||||
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel, focus }) {
|
||||
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">
|
||||
{(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:
|
||||
<InputParams
|
||||
<NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} />
|
||||
}
|
||||
{/* Our own input */}
|
||||
<Editor
|
||||
state={input}
|
||||
setState={setInput}
|
||||
onResolve={onInputResolve}
|
||||
onCancel={onInputCancel}
|
||||
filter={filterCompatibleInputs}
|
||||
focus={focus} />
|
||||
</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*/}}/>
|
||||
onInputCancel={() => {/*todo*/}}
|
||||
focus={false}/>;
|
||||
}
|
||||
|
||||
{/* Our own input */}
|
||||
<Editor
|
||||
state={input}
|
||||
setState={setInput}
|
||||
onResolve={onInputResolve}
|
||||
onCancel={onInputCancel} />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
.typeSignature {
|
||||
display: inline-block;
|
||||
/* vertical-align:; */
|
||||
}
|
||||
|
||||
.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 { type Dynamic, type State2Props } from "./util/extra";
|
||||
import { useEffect, useReducer, useRef, useState } from "react";
|
||||
import { CallBlock, type CallBlockState } from "./CallBlock";
|
||||
import { useEffect, useState } from "react";
|
||||
import { InputBlock, type InputBlockState } from "./InputBlock";
|
||||
import { Type } from "./Type";
|
||||
import { type Dynamic, type State2Props } from "./util/extra";
|
||||
|
||||
import "./Editor.css"
|
||||
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
||||
import "./Editor.css";
|
||||
import type { LetInBlockState } from "./LetInBlock";
|
||||
|
||||
interface LambdaBlockState {
|
||||
kind: "lambda";
|
||||
env: any;
|
||||
paramName: string;
|
||||
expr: EditorState;
|
||||
resolved: undefined | Dynamic;
|
||||
}
|
||||
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
||||
import type { LambdaBlockState } from "./LambdaBlock";
|
||||
import { initialEditorState } from "./configurations";
|
||||
|
||||
export type EditorState =
|
||||
InputBlockState
|
||||
|
|
@ -24,33 +18,42 @@ export type EditorState =
|
|||
| LetInBlockState
|
||||
| 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> {
|
||||
focus: boolean;
|
||||
filter: (suggestion: [string, Dynamic]) => boolean;
|
||||
onResolve: (state: EditorState) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const dontFilter = () => true;
|
||||
|
||||
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) {
|
||||
commands.push('c');
|
||||
}
|
||||
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 commandInputRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
if (needCommand) {
|
||||
commandInputRef.current?.focus();
|
||||
}
|
||||
}, [needCommand]);
|
||||
const onMyResolve = (editorState: EditorState) => {
|
||||
setState(editorState);
|
||||
if (editorState.resolved) {
|
||||
|
|
@ -82,10 +85,11 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
|||
setState({
|
||||
kind: "call",
|
||||
env: state.env,
|
||||
fn: state,
|
||||
fn: removeFocus(state),
|
||||
input: initialEditorState,
|
||||
resolved: undefined,
|
||||
});
|
||||
// focusNextElement();
|
||||
return;
|
||||
}
|
||||
// t -> Transform
|
||||
|
|
@ -95,7 +99,7 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
|||
kind: "call",
|
||||
env: state.env,
|
||||
fn: initialEditorState,
|
||||
input: state,
|
||||
input: removeFocus(state),
|
||||
resolved: undefined,
|
||||
});
|
||||
return;
|
||||
|
|
@ -104,6 +108,10 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
|||
focusPrevElement();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
focusNextElement();
|
||||
return;
|
||||
}
|
||||
// l -> Let ... in ...
|
||||
// = -> assign to name
|
||||
if (e.key === 'l' || e.key === '=') {
|
||||
|
|
@ -123,9 +131,17 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
|||
const renderBlock = () => {
|
||||
switch (state.kind) {
|
||||
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":
|
||||
return <CallBlock state={state} setState={setState} onResolve={onMyResolve} />;
|
||||
return <CallBlock
|
||||
state={state}
|
||||
setState={setState}
|
||||
onResolve={onMyResolve} />;
|
||||
case "let":
|
||||
return <></>;
|
||||
case "lambda":
|
||||
|
|
@ -140,12 +156,13 @@ export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
|||
:: <Type type={getType(state.resolved)} />
|
||||
{ (needCommand)
|
||||
? <input
|
||||
ref={commandInputRef}
|
||||
spellCheck={false}
|
||||
autoFocus={true}
|
||||
className="editable command"
|
||||
placeholder="<enter command>"
|
||||
onKeyDown={onCommand}
|
||||
value=""/>
|
||||
value={""}
|
||||
onChange={() => {}} /> /* gets rid of React warning */
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { useEffect, useRef, useState } from "react";
|
||||
|
|
@ -7,13 +7,13 @@ import { Type } from "./Type";
|
|||
|
||||
import "./InputBlock.css";
|
||||
import type { Dynamic, State2Props } from "./util/extra";
|
||||
import type { EditorState } from "./Editor";
|
||||
|
||||
export interface InputBlockState {
|
||||
kind: "input";
|
||||
env: any;
|
||||
text: string;
|
||||
resolved: undefined | Dynamic;
|
||||
focus: boolean
|
||||
}
|
||||
|
||||
interface InputBlockProps extends State2Props<InputBlockState> {
|
||||
|
|
@ -22,70 +22,73 @@ interface InputBlockProps extends State2Props<InputBlockState> {
|
|||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function InputBlock({ state: {kind, env, text, resolved}, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
||||
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 computeSuggestions = (text, env, filter) => {
|
||||
const asDouble = parseDouble(text);
|
||||
const asInt = parseInt(text);
|
||||
|
||||
const suggestions = [
|
||||
const ls = [
|
||||
... (asDouble ? [[asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
|
||||
... (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) : [],
|
||||
].filter(filter);
|
||||
export function InputBlock({ state, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
||||
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(() => {
|
||||
setI(0); // reset
|
||||
if (ref.current) {
|
||||
ref.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focus) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [focus]);
|
||||
|
||||
const onSelectSuggestion = ([name, dynamic]) => {
|
||||
// setText(name);
|
||||
// ref.current.textContent = name;
|
||||
// setRightMostCaretPosition(ref.current);
|
||||
// setI(0);
|
||||
// setResolved(dynamic);
|
||||
console.log("onResolve inputblock..")
|
||||
onResolve({
|
||||
kind: "input",
|
||||
env,
|
||||
text: name,
|
||||
resolved: dynamic,
|
||||
focus: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onInput = e => {
|
||||
setText(e.target.value);
|
||||
if (resolved) {
|
||||
// un-resolve
|
||||
onResolve({
|
||||
kind: "input",
|
||||
env,
|
||||
text: e.target.value,
|
||||
resolved: undefined,
|
||||
focus: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getCaretPosition = () => {
|
||||
return ref.current.selectionStart;
|
||||
return inputRef.current?.selectionStart || -1;
|
||||
}
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
|
|
@ -100,8 +103,7 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
|||
if (singleSuggestion.length > 0) {
|
||||
const newText = text + singleSuggestion;
|
||||
setText(newText);
|
||||
// ref.current.textContent = newText;
|
||||
setRightMostCaretPosition(ref.current);
|
||||
setRightMostCaretPosition(inputRef.current);
|
||||
e.preventDefault();
|
||||
}
|
||||
else {
|
||||
|
|
@ -156,7 +158,7 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
|||
</span>
|
||||
}
|
||||
{/* Input box */}
|
||||
<input ref={ref}
|
||||
<input ref={inputRef}
|
||||
placeholder="start typing..."
|
||||
className="editable"
|
||||
value={text}
|
||||
|
|
@ -179,8 +181,7 @@ function Suggestions({ suggestions, onSelect, i, setI }) {
|
|||
setI(j);
|
||||
onSelect(suggestions[i]);
|
||||
};
|
||||
|
||||
return (suggestions.length > 0) ?
|
||||
return <>{(suggestions.length > 0) &&
|
||||
<div className={"suggestions"}>
|
||||
{suggestions.map(([name, dynamic], j) =>
|
||||
<div
|
||||
|
|
@ -189,8 +190,7 @@ function Suggestions({ suggestions, onSelect, i, setI }) {
|
|||
onMouseEnter={onMouseEnter(j)}
|
||||
onMouseDown={onMouseDown(j)}>
|
||||
{name} :: <Type type={getType(dynamic)} />
|
||||
</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:
|
||||
// return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
|
||||
case symbolList:
|
||||
return <List val={inst} elemType={type.params[0](type)} />;
|
||||
return <ValueList val={inst} elemType={type.params[0](type)} />;
|
||||
|
||||
default:
|
||||
return <>don't know how to show value</>;
|
||||
|
|
@ -51,8 +51,8 @@ function ValueFunction() {
|
|||
// function Sum({val, elemType}) {
|
||||
// return
|
||||
// }
|
||||
function List({val, elemType}) {
|
||||
return <span className="listType">[{val.map((v, i) => <Value dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||
function ValueList({val, elemType}) {
|
||||
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||
}
|
||||
function ValueSum({val, leftType, rightType}) {
|
||||
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