Compare commits

...

2 commits

11 changed files with 214 additions and 222 deletions

8
pnpm-lock.yaml generated
View file

@ -10,7 +10,7 @@ importers:
dependencies: dependencies:
dope2: dope2:
specifier: git+https://deemz.org/git/joeri/dope2.git specifier: git+https://deemz.org/git/joeri/dope2.git
version: git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890 version: git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994
react: react:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0 version: 19.1.0
@ -633,8 +633,8 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dope2@git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890: dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
resolution: {commit: e631f11aa52b2adda8809c1b0b41cc991fbe8890, repo: https://deemz.org/git/joeri/dope2.git, type: git} resolution: {commit: 443a13998dc3eccab26c27bee4fa056cdbc8f994, repo: https://deemz.org/git/joeri/dope2.git, type: git}
version: 0.0.1 version: 0.0.1
dunder-proto@1.0.1: dunder-proto@1.0.1:
@ -1762,7 +1762,7 @@ snapshots:
depd@2.0.0: {} depd@2.0.0: {}
dope2@git+https://deemz.org/git/joeri/dope2.git#e631f11aa52b2adda8809c1b0b41cc991fbe8890: dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
dependencies: dependencies:
functional-red-black-tree: 1.0.1 functional-red-black-tree: 1.0.1

View file

@ -6,14 +6,15 @@ import { CommandContext } from './CommandContext';
import { EnvContext } from './EnvContext'; import { EnvContext } from './EnvContext';
export function App() { export function App() {
const [history, setHistory] = useState([initialEditorState]); // const [history, setHistory] = useState([initialEditorState]);
// const [history, setHistory] = useState([nonEmptyEditorState]); // const [history, setHistory] = useState([nonEmptyEditorState]);
// const [history, setHistory] = useState([tripleFunctionCallEditorState]); const [history, setHistory] = useState([tripleFunctionCallEditorState]);
const [future, setFuture] = useState<EditorState[]>([]); const [future, setFuture] = useState<EditorState[]>([]);
const pushHistory = (s: EditorState) => { const pushHistory = (callback: (p: EditorState) => EditorState) => {
setHistory(history.concat([s])); const newState = callback(history.at(-1)!);
setHistory(history.concat([newState]));
setFuture([]); setFuture([]);
}; };
@ -50,11 +51,11 @@ export function App() {
window.onkeydown = onKeyDown; window.onkeydown = onKeyDown;
}, []); }, []);
const commands = [ const commands: [string, string[], string][] = [
["call" , "[c] call" ], ["call" , ['c' ], "call" ],
["eval" , "[e] [Tab] [Enter] eval"], ["eval" , ['e','Tab','Enter' ], "eval" ],
["transform", "[t] [.] transform" ], ["transform", ['t', '.' ], "transform" ],
["let" , "[l] [=] let ... in ..." ], ["let" , ['l', '=', 'a' ], "let ... in ..."],
]; ];
const [highlighted, setHighlighted] = useState( const [highlighted, setHighlighted] = useState(
@ -74,8 +75,9 @@ export function App() {
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) [Ctrl+Shift+Z]</button> <button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) [Ctrl+Shift+Z]</button>
Commands: Commands:
{ {
commands.map(([_, descr], i) => commands.map(([_, keys, descr], i) =>
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}> <span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
{keys.map((key, j) => <kbd key={j}>{key}</kbd>)}
{descr} {descr}
</span>) </span>)
} }
@ -86,11 +88,12 @@ export function App() {
<Editor <Editor
state={history.at(-1)!} state={history.at(-1)!}
setState={pushHistory} setState={pushHistory}
onResolve={() => {}}
onCancel={() => {}} onCancel={() => {}}
filter={() => true} filter={() => true}
/> />
</CommandContext> </CommandContext>
</main> </main>
<footer> <footer>

View file

@ -73,12 +73,26 @@
border-left-color: darkred; border-left-color: darkred;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
background-color: rgb(95, 4, 4); /* background-color: rgb(95, 4, 4); */
color: white; background-color: pink;
/* color: white; */
color: black;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
border-left-color: rgb(95, 4, 4); /* border-left-color: rgb(95, 4, 4); */
border-left-color: pink;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam {
/* background-color: rgb(95, 4, 4); */
background-color: pink;
/* color: white; */
color: black;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam:after {
/* border-left-color: rgb(95, 4, 4); */
border-left-color: pink;
}
.functionBlock.unifyError > .functionParams > .outputParam { .functionBlock.unifyError > .functionParams > .outputParam {
background-color: pink; background-color: pink;

View file

@ -1,13 +1,14 @@
import { useState } from "react"; import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction, UnifyError } from "dope2";
import { apply, UnifyError, assignFn, getType, getSymbol, symbolFunction } from "dope2";
import { Editor, type EditorState } from "./Editor"; import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value"; import { Value } from "./Value";
import type { Dynamic, State2Props } from "./util/extra"; import type { Dynamic, SetStateFn, State2Props } from "./util/extra";
import { useEffect } from "react";
import "./CallBlock.css"; import "./CallBlock.css";
type ResolvedType = Dynamic | Error | undefined;
export interface CallBlockState< export interface CallBlockState<
FnState=EditorState, FnState=EditorState,
InputState=EditorState, InputState=EditorState,
@ -15,88 +16,74 @@ export interface CallBlockState<
kind: "call"; kind: "call";
fn: FnState; fn: FnState;
input: InputState; input: InputState;
resolved: undefined | Dynamic; resolved: ResolvedType;
// focus: boolean;
} }
interface CallBlockProps< interface CallBlockProps<
FnState=EditorState, FnState=EditorState,
InputState=EditorState, InputState=EditorState,
> extends State2Props<CallBlockState<FnState,InputState>> { > extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
onResolve: (resolved: EditorState) => void;
} }
function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
const [unifyError, setUnifyError] = useState<typeof UnifyError | undefined>(undefined); function have(resolved: ResolvedType) {
return resolved && !(resolved instanceof Error);
}
function headlessCallBlock({state, setState}: CallBlockProps) {
const {fn, input} = state; const {fn, input} = state;
const setFn = (fn: EditorState) => { const setFn = (callback: SetStateFn) => {
setState({...state, fn}); setState(state => ({...state, fn: callback(state.fn)}));
} }
const setInput = (input: EditorState) => { const setInput = (callback: SetStateFn) => {
setState({...state, input}); setState(state => ({...state, input: callback(state.input)}));
} }
const setResolved = (resolved?: Dynamic) => { const setResolved = (callback: SetStateFn<ResolvedType>) => {
setState({...state, resolved}); setState(state => ({...state, resolved: callback(state.resolved)}));
} }
const makeTheCall = (input, fn) => { useEffect(() => {
// Here we do something spooky: we update the state in response to state change...
// The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle.
if (have(input.resolved) && have(fn.resolved)) {
try { try {
const outputResolved = apply(input.resolved)(fn.resolved); const outputResolved = apply(input.resolved)(fn.resolved); // may throw
setResolved(outputResolved); setResolved(() => outputResolved); // success
onResolve({
...state, resolved: outputResolved
});
setUnifyError(undefined);
} }
catch (e) { catch (e) {
if (!(e instanceof UnifyError)) { if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
throw e; throw e;
} }
setUnifyError(e as typeof UnifyError); setResolved(() => e as Error); // eval error
onResolve({
...state, resolved: undefined
})
} }
}; }
const onFnResolve = (fnState) => { else if (input.resolved instanceof Error) {
if (input.resolved) { setResolved(() => input.resolved); // bubble up the error
makeTheCall(input, fnState); }
else if (fn.resolved instanceof Error) {
setResolved(() => fn.resolved); // bubble up the error
} }
else { else {
// setFn(fnState); // no errors and at least one is undefined:
setResolved(undefined); setResolved(() => undefined); // chill out
onResolve({
...state, resolved: undefined
});
} }
}; }, [input.resolved, fn.resolved]);
const onInputResolve = (inputState) => {
if (fn.resolved) {
makeTheCall(inputState, fn);
}
else {
setResolved(undefined);
onResolve({
...state, resolved: undefined
});
}
};
const onFnCancel = () => { const onFnCancel = () => {
setState(input); setState(state => state.input); // we become our input
} }
const onInputCancel = () => { const onInputCancel = () => {
setState(fn); setState(state => state.fn); // we become our function
} }
return {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel}; return {setFn, setInput, onFnCancel, onInputCancel};
} }
export function CallBlock({ state, setState, onResolve }: CallBlockProps) { export function CallBlock({ state, setState }: CallBlockProps) {
const {unifyError, setFn, setInput, onFnResolve, onInputResolve, onInputCancel} const {setFn, setInput, onFnCancel, onInputCancel}
= headlessCallBlock({ state, setState, onResolve }); = headlessCallBlock({ state, setState });
return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}> return <span className={"functionBlock" + ((state.resolved instanceof Error) ? " unifyError" : "")}>
<FunctionHeader <FunctionHeader
fn={state.fn} fn={state.fn}
setFn={setFn} setFn={setFn}
onFnResolve={onFnResolve} onFnCancel={onFnCancel}
input={state.input} /> input={state.input} />
<div className="functionParams"> <div className="functionParams">
<div className="outputParam"> <div className="outputParam">
@ -104,110 +91,93 @@ 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}
onFnResolve={onFnResolve} onInputCancel={onInputCancel} />
onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
{/* Output (or Error) */} {/* Output (or Error) */}
{state.resolved && <Value dynamic={state.resolved} />} { state.resolved instanceof Error && state.resolved.toString()
{unifyError && unifyError.toString()} || state.resolved && <><Value dynamic={state.resolved} />&#x2611;</>}
</div> </div>
</div> </div>
</span>; </span>;
} }
function FunctionHeader({ fn, setFn, input, onFnResolve }) { function filterFnInputs(fn: Dynamic|Error|undefined, input: Dynamic|Error|undefined) {
if (!have(fn) || !have(input)) {
return false;
}
const fnType = getType(fn);
if (getSymbol(fnType) !== symbolFunction) {
return false; // filter out non-functions already
}
try {
assignFn(fnType, getType(input)); // may throw
return true;
} catch (e) {
if (!(e instanceof UnifyError)) {
throw e;
}
return false;
}
}
function FunctionHeader({ fn, setFn, input, onFnCancel }) {
if (fn.kind === "call") { if (fn.kind === "call") {
// if the function we're calling is itself the result of a function call, // if the function we're calling is itself the result of a function call,
// then we are anonymous, and so we don't draw a function name // then we are anonymous, and so we don't draw a function name
// recurse: // recurse:
const { const {
onFnResolve : onFnFnResolve setFn : setFnFn,
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve}); onFnCancel : onFnFnCancel,
} = headlessCallBlock({state: fn, setState: setFn});
return <FunctionHeader return <FunctionHeader
fn={fn.fn} fn={fn.fn}
setFn={fnFn => setFn({...fn, fn: fnFn})} setFn={setFnFn}
onFnResolve={onFnFnResolve} onFnCancel={onFnFnCancel}
input={fn.input} />; input={fn.input} />;
} }
else { else {
const filterCompatibleFns = ([_name, dynamic]: [string, Dynamic]) => {
if (input.resolved) {
try {
const type = getType(dynamic);
if (getSymbol(type) !== symbolFunction) {
return false;
}
assignFn(type, getType(input.resolved));
} catch (e) {
if (!(e instanceof UnifyError)) {
throw e;
}
return false;
}
}
return true;
}
// end of recursion - draw function name // end of recursion - draw function name
return <div className="functionName"> return <span className="functionName">
&nbsp;&#119891;&#119899;&nbsp; &nbsp;&#119891;&#119899;&nbsp;
<Editor <Editor
state={fn} state={fn}
setState={setFn} setState={setFn}
onResolve={onFnResolve} onCancel={onFnCancel}
onCancel={() => {/*todo*/}} filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} />
filter={filterCompatibleFns} /> </span>;
</div>;
} }
} }
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) { function InputParams({ fn, setFn, input, setInput, onInputCancel }) {
const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => {
if (fn.resolved) {
try {
assignFn(getType(fn.resolved), getType(dynamic));
} catch (e) {
if (!(e instanceof UnifyError)) {
throw e;
}
return false;
}
}
return true;
}
return <div className="inputParam"> return <div className="inputParam">
{(fn.kind === "call") && {(fn.kind === "call") &&
// if the function we're calling is itself the result of a function call, // if the function we're calling is itself the result of a function call,
// then we render its input parameter nested in our own input parameter box, which is way more readable // then we render its input parameter nested in our own input parameter box, which is way more readable
// Input(s) of the function we're calling: // recurse:
<NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} /> <NestedParams fn={fn} setFn={setFn}/>
} }
{/* Our own input */} {/* Our own input */}
<Editor <Editor
state={input} state={input}
setState={setInput} setState={setInput}
onResolve={onInputResolve}
onCancel={onInputCancel} onCancel={onInputCancel}
filter={filterCompatibleInputs} filter={([_, inputCandidate]) => filterFnInputs(fn.resolved, inputCandidate)}
/> />
</div>; </div>;
} }
function NestedInputParams({fn, setFn, onFnResolve}) { function NestedParams({fn, setFn}) {
const { const {
onInputResolve: onFnInputResolve, setFn : setFnFn,
onFnResolve : onFnFnResolve, setInput : setFnInput,
} = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve}); } = headlessCallBlock({state: fn, setState: setFn});
return <InputParams return <InputParams
fn={fn.fn} fn={fn.fn}
setFn={fnFn => setFn({...fn, fn: fnFn})} setFn={setFnFn}
input={fn.input} input={fn.input}
setInput={fnInput => setFn({...fn, input: fnInput})} setInput={setFnInput}
onFnResolve={onFnFnResolve}
onInputResolve={onFnInputResolve}
onInputCancel={() => {/*todo*/}} onInputCancel={() => {/*todo*/}}
/>; />;
} }

View file

@ -3,5 +3,5 @@
} }
.commandInput { .commandInput {
width: 160px; width: 90px;
} }

View file

@ -21,7 +21,6 @@ export type EditorState =
interface EditorProps extends State2Props<EditorState> { interface EditorProps extends State2Props<EditorState> {
filter: (suggestion: [string, Dynamic]) => boolean; filter: (suggestion: [string, Dynamic]) => boolean;
onResolve: (state: EditorState) => void;
onCancel: () => void; onCancel: () => void;
} }
@ -52,7 +51,7 @@ function removeFocus(state: EditorState): EditorState {
return state; return state;
} }
export function Editor({state, setState, onResolve, onCancel, filter}: EditorProps) { export function Editor({state, setState, onCancel, filter}: EditorProps) {
const [needCommand, setNeedCommand] = useState(false); const [needCommand, setNeedCommand] = useState(false);
const commandInputRef = useRef<HTMLInputElement>(null); const commandInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@ -60,22 +59,27 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
commandInputRef.current?.focus(); commandInputRef.current?.focus();
} }
}, [needCommand]); }, [needCommand]);
const onMyResolve = (editorState: EditorState) => { // const onMyResolve = (editorState: EditorState) => {
setState(editorState); // setState(editorState);
if (editorState.resolved) { // onResolve(editorState);
setNeedCommand(true); // return;
}
else {
// unresolved // if (editorState.resolved) {
setNeedCommand(false); // setNeedCommand(true);
onResolve(editorState); // pass up the fact that we're unresolved // }
} // else {
} // // unresolved
// setNeedCommand(false);
// onResolve(editorState); // pass up the fact that we're unresolved
// }
// }
const globalContext = useContext(CommandContext); const globalContext = useContext(CommandContext);
const onCommand = (e: React.KeyboardEvent) => { const onCommand = (e: React.KeyboardEvent) => {
const type = getType(state.resolved); // const type = getType(state.resolved);
const commands = getCommands(type); // const commands = getCommands(type);
const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '=', '.', 'c'];
if (!commands.includes(e.key)) { if (!commands.includes(e.key)) {
return; return;
} }
@ -83,7 +87,7 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
setNeedCommand(false); setNeedCommand(false);
// u -> pass Up // u -> pass Up
if (e.key === "e" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) { if (e.key === "e" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) {
onResolve(state); // onResolve(state);
globalContext?.doHighlight.eval(); globalContext?.doHighlight.eval();
return; return;
} }
@ -94,12 +98,12 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
// c -> Call // c -> Call
if (e.key === "c") { if (e.key === "c") {
// we become CallBlock // we become CallBlock
setState({ setState(state => ({
kind: "call", kind: "call",
fn: removeFocus(state), fn: removeFocus(state),
input: initialEditorState, input: initialEditorState,
resolved: undefined, resolved: undefined,
}); }));
globalContext?.doHighlight.call(); globalContext?.doHighlight.call();
// focusNextElement(); // focusNextElement();
return; return;
@ -107,12 +111,12 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
// t -> Transform // t -> Transform
if (e.key === "t" || e.key === ".") { if (e.key === "t" || e.key === ".") {
// we become CallBlock // we become CallBlock
setState({ setState(state => ({
kind: "call", kind: "call",
fn: initialEditorState, fn: initialEditorState,
input: removeFocus(state), input: removeFocus(state),
resolved: undefined, resolved: undefined,
}); }));
globalContext?.doHighlight.transform(); globalContext?.doHighlight.transform();
return; return;
} }
@ -128,13 +132,13 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
// = -> assign to name // = -> assign to name
if (e.key === 'l' || e.key === '=') { if (e.key === 'l' || e.key === '=') {
// we become LetInBlock // we become LetInBlock
setState({ setState(state => ({
kind: "let", kind: "let",
inner: removeFocus(initialEditorState), inner: removeFocus(initialEditorState),
name: "", name: "",
value: removeFocus(state), value: removeFocus(state),
resolved: undefined, resolved: undefined,
}); }));
globalContext?.doHighlight.let(); globalContext?.doHighlight.let();
return; return;
} }
@ -145,22 +149,19 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
case "input": case "input":
return <InputBlock return <InputBlock
state={state} state={state}
setState={setState} setState={setState as (callback:(p:InputBlockState)=>EditorState)=>void}
filter={filter} filter={filter}
onResolve={onMyResolve}
onCancel={onCancel} onCancel={onCancel}
/>; />;
case "call": case "call":
return <CallBlock return <CallBlock
state={state} state={state}
setState={setState} setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
onResolve={onMyResolve}
/>; />;
case "let": case "let":
return <LetInBlock return <LetInBlock
state={state} state={state}
setState={setState} setState={setState as (callback:(p:LetInBlockState)=>EditorState)=>void}
onResolve={() => {}}
/>; />;
case "lambda": case "lambda":
return <></>; return <></>;
@ -169,22 +170,19 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
return <> return <>
{renderBlock()} {renderBlock()}
{ {
(state.resolved) (state.resolved && !(state.resolved instanceof Error))
? <div className="typeSignature"> ? <div className="typeSignature">
:: <Type type={getType(state.resolved)} /> :: <Type type={getType(state.resolved)} />
{ (needCommand)
? <input
ref={commandInputRef}
spellCheck={false}
className="editable commandInput"
placeholder={`<command: ${getShortCommands(getType(state.resolved))}>`}
onKeyDown={onCommand}
value={""}
onChange={() => {}} /> /* gets rid of React warning */
: <></>
}
</div> </div>
: <></> : <></>
} }
<input
ref={commandInputRef}
spellCheck={false}
className="editable commandInput"
placeholder={`<command>`}
onKeyDown={onCommand}
value={""}
onChange={() => {}} />
</>; </>;
} }

View file

@ -18,7 +18,6 @@ export interface InputBlockState {
interface InputBlockProps extends State2Props<InputBlockState> { interface InputBlockProps extends State2Props<InputBlockState> {
filter: (suggestion: [string, Dynamic]) => boolean; filter: (suggestion: [string, Dynamic]) => boolean;
onResolve: (state: InputBlockState) => void;
onCancel: () => void; onCancel: () => void;
} }
@ -31,6 +30,7 @@ const computeSuggestions = (text, env, filter) => {
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []), ... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
... trie.suggest(env.name2dyn)(text)(Infinity), ... trie.suggest(env.name2dyn)(text)(Infinity),
] ]
return ls;
return [ return [
...ls.filter(filter), // ones that match filter come first ...ls.filter(filter), // ones that match filter come first
...ls.filter(s => !filter(s)), ...ls.filter(s => !filter(s)),
@ -38,17 +38,13 @@ const computeSuggestions = (text, env, filter) => {
// .slice(0,30); // .slice(0,30);
} }
export function InputBlock({ state, setState, filter, onResolve, onCancel }: InputBlockProps) { export function InputBlock({ state, setState, filter, onCancel }: InputBlockProps) {
const {text, resolved, focus} = state; const {text, resolved, focus} = state;
const env = useContext(EnvContext); const env = useContext(EnvContext);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [i, setI] = useState(0); // selected suggestion idx const [i, setI] = useState(0); // selected suggestion idx
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
const setText = (text: string) => {
setState({...state, text});
}
const singleSuggestion = trie.growPrefix(env.name2dyn)(text); const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]); const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]);
@ -60,32 +56,22 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
} }
}, [focus]); }, [focus]);
const onSelectSuggestion = ([name, dynamic]) => { useEffect(() => {
onResolve({ if (suggestions.length >= i) {
kind: "input", setI(0);
text: name,
resolved: dynamic,
focus: false,
});
};
const onInput = e => {
setText(e.target.value);
if (resolved) {
// un-resolve
onResolve({
kind: "input",
text: e.target.value,
resolved: undefined,
focus: true,
});
} }
}; }, [suggestions.length]);
const getCaretPosition = () => { const getCaretPosition = () => {
return inputRef.current?.selectionStart || -1; return inputRef.current?.selectionStart || -1;
} }
const onTextChange = newText => {
const found = trie.get(env.name2dyn)(newText);
setState(state => ({...state, text: newText, resolved: found}));
}
// fired before onInput
const onKeyDown = (e: React.KeyboardEvent) => { const onKeyDown = (e: React.KeyboardEvent) => {
const fns = { const fns = {
Tab: () => { Tab: () => {
@ -97,7 +83,7 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
// not shift key // not shift key
if (singleSuggestion.length > 0) { if (singleSuggestion.length > 0) {
const newText = text + singleSuggestion; const newText = text + singleSuggestion;
setText(newText); onTextChange(newText);
setRightMostCaretPosition(inputRef.current); setRightMostCaretPosition(inputRef.current);
e.preventDefault(); e.preventDefault();
} }
@ -116,7 +102,7 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
e.preventDefault(); e.preventDefault();
}, },
ArrowLeft: () => { ArrowLeft: () => {
if (getCaretPosition() === 0) { if (getCaretPosition() <= 0) {
focusPrevElement(); focusPrevElement();
e.preventDefault(); e.preventDefault();
} }
@ -141,6 +127,14 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
fns[e.key]?.(); fns[e.key]?.();
}; };
const onInput = e => {
onTextChange(e.target.value);
};
const onSelectSuggestion = ([name, dynamic]) => {
setState(state => ({...state, text: name, resolved: dynamic}));
};
return <span> return <span>
<span className=""> <span className="">
{/* Dropdown suggestions */} {/* Dropdown suggestions */}
@ -164,6 +158,7 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
spellCheck={false}/> spellCheck={false}/>
{/* Single 'grey' suggestion */} {/* Single 'grey' suggestion */}
<span className="text-block suggest">{singleSuggestion}</span> <span className="text-block suggest">{singleSuggestion}</span>
{ resolved && <>&#x2611;</>}
</span> </span>
</span>; </span>;
} }

View file

@ -16,20 +16,19 @@ export interface LetInBlockState {
} }
interface LetInBlockProps extends State2Props<LetInBlockState> { interface LetInBlockProps extends State2Props<LetInBlockState> {
onResolve: (resolved: EditorState) => void;
} }
export function LetInBlock({state, setState, onResolve}: LetInBlockProps) { export function LetInBlock({state, setState}: LetInBlockProps) {
const {name, value, inner} = state; const {name, value, inner} = state;
const env = useContext(EnvContext); const env = useContext(EnvContext);
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const setInner = inner => setState({...state, inner}); const setInner = inner => setState(state => ({...state, inner}));
const setValue = value => setState({...state, value}); const setValue = value => setState(state => ({...state, value}));
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => { const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setState({...state, name: e.target.value}); setState(state => ({...state, name: e.target.value}));
} }
useEffect(() => { useEffect(() => {
@ -55,7 +54,6 @@ export function LetInBlock({state, setState, onResolve}: LetInBlockProps) {
state={value} state={value}
setState={setValue} setState={setValue}
filter={() => true} filter={() => true}
onResolve={() => {}}
onCancel={() => {}} onCancel={() => {}}
/> />
&nbsp;<span className="keyword">in</span> &nbsp;<span className="keyword">in</span>
@ -66,7 +64,6 @@ export function LetInBlock({state, setState, onResolve}: LetInBlockProps) {
state={inner} state={inner}
setState={setInner} setState={setInner}
filter={() => true} filter={() => true}
onResolve={onResolve}
onCancel={() => {}} onCancel={() => {}}
/> />
</EnvContext> </EnvContext>

View file

@ -1,4 +1,4 @@
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict} from "dope2"; import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool} from "dope2";
import "./Value.css"; import "./Value.css";
@ -11,6 +11,9 @@ export function Value({dynamic}) {
if (eqType(type)(Int)) { if (eqType(type)(Int)) {
return <ValueInt val={inst}/>; return <ValueInt val={inst}/>;
} }
if (eqType(type)(Bool)) {
return <ValueBool val={inst}/>;
}
const symbol = getSymbol(type); const symbol = getSymbol(type);
switch (symbol) { switch (symbol) {
@ -48,6 +51,9 @@ function ValueInt({val}) {
function ValueFunction() { function ValueFunction() {
return <>&#119891;&#119899;&nbsp;</>; return <>&#119891;&#119899;&nbsp;</>;
} }
function ValueBool({val}) {
return <span className="valuePrimitive">{val.toString()}</span>;
}
// function Sum({val, elemType}) { // function Sum({val, elemType}) {
// return // return
// } // }

View file

@ -9,3 +9,12 @@ body {
font-style: normal; font-style: normal;
font-variation-settings: "wdth" 100; font-variation-settings: "wdth" 100;
} }
kbd {
border: 2px darkgrey;
color: rgb(63, 63, 63);
border-style: outset;
background-color: whitesmoke;
border-radius: 3px;
margin: 0 2px 0 2px;
}

View file

@ -5,9 +5,9 @@ export interface Dynamic {
t: any; t: any;
} }
export interface State2Props<T> { export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
state: T;
// setState: (callback: (state: T) => T) => void; export interface State2Props<InType,OutType=InType> {
// setState: (state: T) => void; state: InType;
setState: (state: EditorState) => void; setState: (callback: SetStateFn<InType,OutType>) => void;
} }