better-looking parameters

This commit is contained in:
Joeri Exelmans 2025-05-12 23:40:58 +02:00
parent 9afaa41fbb
commit 95eb8aef84
10 changed files with 306 additions and 229 deletions

View file

@ -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>

View file

@ -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;
} }

View file

@ -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} />
}
{/* 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} fn={fn.fn}
setFn={fnFn => setFn({...fn, fn: fnFn})} setFn={fnFn => setFn({...fn, fn: fnFn})}
input={fn.input} input={fn.input}
setInput={fnInput => setFn({...fn, input: fnInput})} setInput={fnInput => setFn({...fn, input: fnInput})}
onFnResolve={onFnFnResolve} onFnResolve={onFnFnResolve}
onInputResolve={onFnInputResolve} onInputResolve={onFnInputResolve}
onInputCancel={() => {/*todo*/}}/> onInputCancel={() => {/*todo*/}}
} focus={false}/>;
}
{/* Our own input */}
<Editor
state={input}
setState={setInput}
onResolve={onInputResolve}
onCancel={onInputCancel} />
</div>;
};

View file

@ -1,6 +1,5 @@
.typeSignature { .typeSignature {
display: inline-block; display: inline-block;
/* vertical-align:; */
} }
.command { .command {

View file

@ -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>

View file

@ -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
View 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;
}

View file

@ -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
View 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
View 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