hitting spacebar always adds a parameter to the first ancestor that is a CallBlock

This commit is contained in:
Joeri Exelmans 2025-05-20 09:02:19 +02:00
parent 5b6bcf5ffa
commit 5c3018b8c7
8 changed files with 140 additions and 43 deletions

View file

@ -1,10 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import './App.css'; import './App.css';
import { GlobalContext } from './GlobalContext';
import { ExprBlock, type ExprBlockState } from './ExprBlock'; import { ExprBlock, type ExprBlockState } from './ExprBlock';
import { extendedEnv } from './EnvContext'; import { GlobalContext } from './GlobalContext';
import { biggerExample, higherOrder, higherOrder2Params, inc, initialEditorState, lambda2Params, nonEmptyEditorState, pushBool, tripleFunctionCallEditorState } from "./configurations"; import { biggerExample, higherOrder, higherOrder2Params, inc, initialEditorState, lambda2Params, nonEmptyEditorState, pushBool, tripleFunctionCallEditorState } from "./configurations";
import { evalEditorBlock } from "./eval"; import { removeFocus } from "./eval";
const commands: [string, string[], string][] = [ const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ], ["call" , ['c' ], "call" ],
@ -174,6 +173,14 @@ export function App() {
// console.log('suggestionPriority of App, always 0'); // console.log('suggestionPriority of App, always 0');
return 0; return 0;
}} }}
addParam={(s: ExprBlockState) => {
pushHistory(state => ({
kind: "call",
fn: removeFocus(state),
input: initialEditorState,
}));
doHighlight.call();
}}
/> />
</GlobalContext> </GlobalContext>

View file

@ -2,11 +2,12 @@ import { useContext } from "react";
import { ExprBlock, type ExprBlockState, type SetStateFn, type State2Props } from "./ExprBlock"; import { ExprBlock, type ExprBlockState, type SetStateFn, type State2Props } from "./ExprBlock";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
import { evalCallBlock2, evalEditorBlock, scoreResolved, type ResolvedType } from "./eval"; import { addFocusRightMost, evalCallBlock2, evalEditorBlock, removeFocus, scoreResolved, type ResolvedType } from "./eval";
import { GlobalContext } from "./GlobalContext"; import { GlobalContext } from "./GlobalContext";
import { Value } from "./Value"; import { Value } from "./Value";
import "./CallBlock.css"; import "./CallBlock.css";
import { initialEditorState } from "./configurations";
export interface CallBlockState { export interface CallBlockState {
kind: "call"; kind: "call";
@ -40,7 +41,7 @@ function nestedInputProperties({state, setState, suggestionPriority}: CallBlockP
setState(state => ({...state, input: callback(state.input)})); setState(state => ({...state, input: callback(state.input)}));
} }
const onInputCancel = () => { const onInputCancel = () => {
setState(state => state.fn); // we become our function setState(state => addFocusRightMost(state.fn)); // we become our function
} }
const inputSuggestionPriorirty = (inputSuggestion: ResolvedType) => computePriority( const inputSuggestionPriorirty = (inputSuggestion: ResolvedType) => computePriority(
evalEditorBlock(state.fn, env)[0], // fn *may* be set evalEditorBlock(state.fn, env)[0], // fn *may* be set
@ -53,9 +54,18 @@ function nestedInputProperties({state, setState, suggestionPriority}: CallBlockP
export function CallBlock(props: CallBlockProps) { export function CallBlock(props: CallBlockProps) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext);
const addParam = (s: ExprBlockState) => {
props.setState(state => ({
kind: "call",
fn: removeFocus(state),
input: s,
}));
globalContext?.doHighlight.call();
};
const [resolved] = evalEditorBlock(props.state, env); const [resolved] = evalEditorBlock(props.state, env);
return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}> return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}>
<FunctionHeader {...props} /> <FunctionHeader {...props} addParam={addParam} />
<div className="functionParams"> <div className="functionParams">
<div className="outputParam"> <div className="outputParam">
{/* Sequence of input parameters */} {/* Sequence of input parameters */}
@ -63,6 +73,7 @@ export function CallBlock(props: CallBlockProps) {
{...props} {...props}
depth={0} depth={0}
errorDepth={(resolved.kind === "error") ? (resolved.depth) : -1} errorDepth={(resolved.kind === "error") ? (resolved.depth) : -1}
addParam={addParam}
/> />
{ (resolved.kind === "error") && resolved.e.toString() { (resolved.kind === "error") && resolved.e.toString()
|| (resolved.kind === "value") && <Value dynamic={resolved} /> || (resolved.kind === "value") && <Value dynamic={resolved} />
@ -91,12 +102,12 @@ function FunctionHeader(props) {
// end of recursion - draw function name // end of recursion - draw function name
return <span className="functionName"> return <span className="functionName">
&nbsp;&#119891;&#119899;&nbsp; &nbsp;&#119891;&#119899;&nbsp;
<ExprBlock {...nestedProperties} /> <ExprBlock {...nestedProperties} addParam={props.addParam} />
</span>; </span>;
} }
} }
function InputParams({ depth, errorDepth, ...rest }) { function InputParams({ depth, errorDepth, addParam, ...rest }) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext); const globalContext = useContext(GlobalContext);
const isOffending = depth === errorDepth; const isOffending = depth === errorDepth;
@ -107,10 +118,12 @@ function InputParams({ depth, errorDepth, ...rest }) {
{...nestedFnProperties(rest as CallBlockProps, env)} {...nestedFnProperties(rest as CallBlockProps, env)}
depth={depth+1} depth={depth+1}
errorDepth={errorDepth} errorDepth={errorDepth}
addParam={addParam}
/>} />}
{/* Our own input param */} {/* Our own input param */}
<ExprBlock <ExprBlock
{...nestedInputProperties(rest as CallBlockProps, env)} {...nestedInputProperties(rest as CallBlockProps, env)}
addParam={addParam}
/> />
</div>; </div>;
} }

View file

@ -10,7 +10,7 @@ import { LambdaBlock, type LambdaBlockState } from "./LambdaBlock";
import { LetInBlock, type LetInBlockState } from "./LetInBlock"; import { LetInBlock, type LetInBlockState } from "./LetInBlock";
import { Type } from "./Type"; import { Type } from "./Type";
import { initialEditorState } from "./configurations"; import { initialEditorState } from "./configurations";
import { evalEditorBlock, type ResolvedType } from "./eval"; import { evalEditorBlock, removeFocus, type ResolvedType } from "./eval";
import { focusNextElement, focusPrevElement } from "./util/dom_trickery"; import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
import "./ExprBlock.css"; import "./ExprBlock.css";
@ -31,6 +31,7 @@ export interface State2Props<InType, OutType = InType> {
interface ExprBlockProps extends State2Props<ExprBlockState> { interface ExprBlockProps extends State2Props<ExprBlockState> {
onCancel: () => void; onCancel: () => void;
addParam: (e: ExprBlockState) => void;
} }
function getCommands(type) { function getCommands(type) {
@ -47,20 +48,7 @@ function getShortCommands(type) {
return 'Tab|.'; return 'Tab|.';
} }
function removeFocus(state: ExprBlockState): ExprBlockState { export function ExprBlock({state, setState, suggestionPriority, onCancel, addParam}: ExprBlockProps) {
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 ExprBlock({state, setState, onCancel, suggestionPriority}: ExprBlockProps) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const [needCommand, setNeedCommand] = useState(false); const [needCommand, setNeedCommand] = useState(false);
const commandInputRef = useRef<HTMLInputElement>(null); const commandInputRef = useRef<HTMLInputElement>(null);
@ -95,7 +83,6 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
kind: "call", kind: "call",
fn: removeFocus(state), fn: removeFocus(state),
input: initialEditorState, input: initialEditorState,
resolved: undefined,
})); }));
globalContext?.doHighlight.call(); globalContext?.doHighlight.call();
// focusNextElement(); // focusNextElement();
@ -108,7 +95,6 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
kind: "call", kind: "call",
fn: initialEditorState, fn: initialEditorState,
input: removeFocus(state), input: removeFocus(state),
resolved: undefined,
})); }));
globalContext?.doHighlight.transform(); globalContext?.doHighlight.transform();
return; return;
@ -127,9 +113,10 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
// we become LetInBlock // we become LetInBlock
setState(state => ({ setState(state => ({
kind: "let", kind: "let",
inner: removeFocus(initialEditorState),
name: "", name: "",
focus: true,
value: removeFocus(state), value: removeFocus(state),
inner: removeFocus(initialEditorState),
})); }));
globalContext?.doHighlight.let(); globalContext?.doHighlight.let();
return; return;
@ -137,9 +124,10 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
if (e.key === 'L' || e.key === '=' && e.shiftKey) { if (e.key === 'L' || e.key === '=' && e.shiftKey) {
setState(state => ({ setState(state => ({
kind: "let", kind: "let",
inner: removeFocus(state),
name: "", name: "",
focus: true,
value: removeFocus(initialEditorState), value: removeFocus(initialEditorState),
inner: removeFocus(state),
})); }));
} }
// a -> lAmbdA // a -> lAmbdA
@ -147,6 +135,7 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
setState(state => ({ setState(state => ({
kind: "lambda", kind: "lambda",
paramName: "", paramName: "",
focus: true,
expr: removeFocus(state), expr: removeFocus(state),
})); }));
globalContext?.doHighlight.lambda(); globalContext?.doHighlight.lambda();
@ -162,6 +151,7 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
setState={setState as (callback:(p:InputBlockState)=>ExprBlockState)=>void} setState={setState as (callback:(p:InputBlockState)=>ExprBlockState)=>void}
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
onCancel={onCancel} onCancel={onCancel}
addParam={addParam}
/>; />;
case "call": case "call":
return <CallBlock return <CallBlock
@ -174,12 +164,14 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB
state={state} state={state}
setState={setState as (callback:(p:LetInBlockState)=>ExprBlockState)=>void} setState={setState as (callback:(p:LetInBlockState)=>ExprBlockState)=>void}
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
addParam={addParam}
/>; />;
case "lambda": case "lambda":
return <LambdaBlock return <LambdaBlock
state={state} state={state}
setState={setState as (callback:(p:LambdaBlockState)=>ExprBlockState)=>void} setState={setState as (callback:(p:LambdaBlockState)=>ExprBlockState)=>void}
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
addParam={addParam}
/>; />;
} }
} }

View file

@ -9,6 +9,8 @@
background-color: transparent; background-color: transparent;
color: inherit; color: inherit;
padding: 0; padding: 0;
cursor: text;
outline: 0;
} }
.suggest { .suggest {
top: 2.4px; top: 2.4px;

View file

@ -6,9 +6,12 @@ import { EnvContext } from "./EnvContext";
import type { Dynamic, ResolvedType } from "./eval"; import type { Dynamic, ResolvedType } from "./eval";
import "./InputBlock.css"; import "./InputBlock.css";
import { Type } from "./Type"; import { Type } from "./Type";
import type { State2Props } from "./ExprBlock"; import type { ExprBlockState, State2Props } from "./ExprBlock";
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery"; import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
import { attemptParseLiteral } from "./eval"; import { attemptParseLiteral, removeFocus } from "./eval";
import { GlobalContext } from "./GlobalContext";
import { initialEditorState } from "./configurations";
import type { CallBlockState } from "./CallBlock";
interface Literal { interface Literal {
kind: "literal"; kind: "literal";
@ -32,8 +35,9 @@ export interface InputBlockState {
export type SuggestionType = ['literal'|'name', string, Dynamic]; export type SuggestionType = ['literal'|'name', string, Dynamic];
export type PrioritizedSuggestionType = [number, ...SuggestionType]; export type PrioritizedSuggestionType = [number, ...SuggestionType];
interface InputBlockProps extends State2Props<InputBlockState> { interface InputBlockProps extends State2Props<InputBlockState,ExprBlockState> {
onCancel: () => void; onCancel: () => void;
addParam: (e: ExprBlockState) => void;
} }
const computeSuggestions = (text, env, suggestionPriority: (s: ResolvedType) => number): PrioritizedSuggestionType[] => { const computeSuggestions = (text, env, suggestionPriority: (s: ResolvedType) => number): PrioritizedSuggestionType[] => {
@ -55,11 +59,12 @@ const computeSuggestions = (text, env, suggestionPriority: (s: ResolvedType) =>
] ]
// return []; // <-- uncomment to disable suggestions (useful for debugging) // return []; // <-- uncomment to disable suggestions (useful for debugging)
return ls return ls
.map(suggestion => [suggestionPriority(suggestion[2]), ...suggestion] as PrioritizedSuggestionType) .map((suggestion) => [suggestionPriority(suggestion[2]), ...suggestion] as PrioritizedSuggestionType)
.sort(([priorityA], [priorityB]) => priorityB - priorityA) .sort(([priorityA], [priorityB]) => priorityB - priorityA)
} }
export function InputBlock({ state, setState, suggestionPriority, onCancel }: InputBlockProps) { export function InputBlock({ state, setState, suggestionPriority, onCancel, addParam }: InputBlockProps) {
const globalContext = useContext(GlobalContext);
const {text, focus} = state; const {text, focus} = state;
const env = useContext(EnvContext); const env = useContext(EnvContext);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -147,7 +152,13 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
onCancel(); onCancel();
e.preventDefault(); e.preventDefault();
} }
} },
" ": () => {
e.preventDefault();
if (text.length > 0) {
addParam(initialEditorState);
}
},
}; };
fns[e.key]?.(); fns[e.key]?.();
}; };

View file

@ -9,20 +9,24 @@ import { autoInputWidth } from "./util/dom_trickery";
import "./LambdaBlock.css"; import "./LambdaBlock.css";
import { Type } from "./Type"; import { Type } from "./Type";
import type { CallBlockState } from "./CallBlock";
export interface LambdaBlockState { export interface LambdaBlockState {
kind: "lambda"; kind: "lambda";
paramName: string; paramName: string;
focus: boolean;
expr: ExprBlockState; expr: ExprBlockState;
} }
interface LambdaBlockProps< interface LambdaBlockProps<
FnState=ExprBlockState, FnState=ExprBlockState,
InputState=ExprBlockState, InputState=ExprBlockState,
> extends State2Props<LambdaBlockState,ExprBlockState> {} > extends State2Props<LambdaBlockState,ExprBlockState> {
addParam: (e: ExprBlockState) => void;
}
export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockProps) { export function LambdaBlock({state, setState, suggestionPriority, addParam}: LambdaBlockProps) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
@ -43,8 +47,10 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
}; };
useEffect(() => { useEffect(() => {
nameRef.current?.focus(); if (state.focus) {
}, []); nameRef.current?.focus();
}
}, [state.focus]);
useEffect(() => autoInputWidth(nameRef, state.paramName, 60), [nameRef, state.paramName]); useEffect(() => autoInputWidth(nameRef, state.paramName, 60), [nameRef, state.paramName]);
@ -75,6 +81,7 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
placeholder="<name>" placeholder="<name>"
onKeyDown={onChangeName} onKeyDown={onChangeName}
onChange={e => setParamName(e.target.value)} onChange={e => setParamName(e.target.value)}
spellCheck={false}
/> />
</span> </span>
<div className="typeSignature"> <div className="typeSignature">
@ -93,6 +100,7 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
// console.log('suggestionPriority of lambdaInner... just passing through'); // console.log('suggestionPriority of lambdaInner... just passing through');
return suggestionPriority(s); return suggestionPriority(s);
}} }}
addParam={addParam}
/> />
</EnvContext> </EnvContext>
</div> </div>

View file

@ -8,15 +8,19 @@ import { autoInputWidth } from "./util/dom_trickery";
import { GlobalContext } from "./GlobalContext"; import { GlobalContext } from "./GlobalContext";
import "./LetInBlock.css"; import "./LetInBlock.css";
import type { CallBlockState } from "./CallBlock";
export interface LetInBlockState { export interface LetInBlockState {
kind: "let"; kind: "let";
name: string; name: string;
focus: boolean;
value: ExprBlockState; value: ExprBlockState;
inner: ExprBlockState; inner: ExprBlockState;
} }
interface LetInBlockProps extends State2Props<LetInBlockState,ExprBlockState> {} interface LetInBlockProps extends State2Props<LetInBlockState,ExprBlockState> {
addParam: (e: ExprBlockState) => void;
}
export function LetInBlock(props: LetInBlockProps) { export function LetInBlock(props: LetInBlockProps) {
return <span className="letIn"> return <span className="letIn">
@ -29,7 +33,7 @@ export function LetInBlock(props: LetInBlockProps) {
</span> </span>
} }
function DeclColumns({state: {name, value, inner}, setState, suggestionPriority}) { function DeclColumns({state: {name, value, inner, focus}, setState, suggestionPriority, addParam}) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext); const globalContext = useContext(GlobalContext);
@ -47,8 +51,11 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority}
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
nameRef.current?.focus(); if (focus) {
}, []); nameRef.current?.focus();
}
}, [focus]);
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]); useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
const [valueResolved] = evalEditorBlock(value, env); const [valueResolved] = evalEditorBlock(value, env);
@ -63,6 +70,7 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority}
value={name} value={name}
placeholder="<name>" placeholder="<name>"
onChange={onChangeName} onChange={onChangeName}
spellCheck={false}
/> />
</span> </span>
<span className="keyword column">&nbsp;=&nbsp;</span> <span className="keyword column">&nbsp;=&nbsp;</span>
@ -72,6 +80,7 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority}
setState={setValue} setState={setValue}
suggestionPriority={valueSuggestionPriority} suggestionPriority={valueSuggestionPriority}
onCancel={() => setState(state => state.inner)} // keep inner onCancel={() => setState(state => state.inner)} // keep inner
addParam={addParam}
/> />
</span> </span>
{inner.kind === "let" && {inner.kind === "let" &&
@ -81,13 +90,14 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority}
state={inner} state={inner}
setState={setInner} setState={setInner}
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
addParam={addParam}
/> />
</EnvContext> </EnvContext>
} }
</>; </>;
} }
function InnerMost({state, setState, suggestionPriority}) { function InnerMost({state, setState, suggestionPriority, addParam}) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext); const globalContext = useContext(GlobalContext);
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)})); const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
@ -100,6 +110,7 @@ function InnerMost({state, setState, suggestionPriority}) {
state={state.inner} state={state.inner}
setState={setInner} setState={setInner}
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
addParam={addParam}
/> />
</EnvContext>; </EnvContext>;
} }
@ -110,6 +121,7 @@ function InnerMost({state, setState, suggestionPriority}) {
setState={setInner} setState={setInner}
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
onCancel={onCancel} // keep value onCancel={onCancel} // keep value
addParam={addParam}
/> />
</EnvContext> </EnvContext>
} }

View file

@ -470,4 +470,56 @@ function makeError(env: Environment, e: Error, unification: Unification=new Map(
nextFreeTypeVar: idx + 1, nextFreeTypeVar: idx + 1,
typeVars: new Set([...env.typeVars, UNBOUND_SYMBOLS[idx]]), typeVars: new Set([...env.typeVars, UNBOUND_SYMBOLS[idx]]),
}]; }];
} }
export function removeFocus(state: ExprBlockState): ExprBlockState {
if (state.kind === "input") {
return { ...state, focus: false };
}
else if (state.kind === "call") {
return {
...state,
fn: removeFocus(state.fn),
input: removeFocus(state.input),
};
}
else if (state.kind === "lambda") {
return {
...state,
focus: false,
expr: removeFocus(state.expr),
};
}
else { // state.kind === "let"
return {
...state,
focus: false,
value: removeFocus(state.value),
inner: removeFocus(state.inner),
}
}
}
export function addFocusRightMost(state: ExprBlockState) : ExprBlockState {
if (state.kind === "input") {
return { ...state, focus: true };
}
else if (state.kind === "call") {
return {
... state,
input: addFocusRightMost(state.input),
};
}
else if (state.kind === "lambda") {
return {
...state,
expr: addFocusRightMost(state.expr),
};
}
else { // state.kind === "let"
return {
...state,
inner: addFocusRightMost(state.inner),
}
}
}