From 5c3018b8c7a822ffc51a7edc44cfc92b45cf4b28 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 20 May 2025 09:02:19 +0200 Subject: [PATCH] hitting spacebar always adds a parameter to the first ancestor that is a CallBlock --- src/App.tsx | 13 ++++++++--- src/CallBlock.tsx | 23 ++++++++++++++----- src/ExprBlock.tsx | 30 +++++++++---------------- src/InputBlock.css | 2 ++ src/InputBlock.tsx | 23 ++++++++++++++----- src/LambdaBlock.tsx | 16 ++++++++++---- src/LetInBlock.tsx | 22 +++++++++++++----- src/eval.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 140 insertions(+), 43 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 463b1de..c2cce23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from 'react'; import './App.css'; -import { GlobalContext } from './GlobalContext'; 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 { evalEditorBlock } from "./eval"; +import { removeFocus } from "./eval"; const commands: [string, string[], string][] = [ ["call" , ['c' ], "call" ], @@ -174,6 +173,14 @@ export function App() { // console.log('suggestionPriority of App, always 0'); return 0; }} + addParam={(s: ExprBlockState) => { + pushHistory(state => ({ + kind: "call", + fn: removeFocus(state), + input: initialEditorState, + })); + doHighlight.call(); + }} /> diff --git a/src/CallBlock.tsx b/src/CallBlock.tsx index aa7d859..9650b35 100644 --- a/src/CallBlock.tsx +++ b/src/CallBlock.tsx @@ -2,11 +2,12 @@ import { useContext } from "react"; import { ExprBlock, type ExprBlockState, type SetStateFn, type State2Props } from "./ExprBlock"; 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 { Value } from "./Value"; import "./CallBlock.css"; +import { initialEditorState } from "./configurations"; export interface CallBlockState { kind: "call"; @@ -40,7 +41,7 @@ function nestedInputProperties({state, setState, suggestionPriority}: CallBlockP setState(state => ({...state, input: callback(state.input)})); } const onInputCancel = () => { - setState(state => state.fn); // we become our function + setState(state => addFocusRightMost(state.fn)); // we become our function } const inputSuggestionPriorirty = (inputSuggestion: ResolvedType) => computePriority( evalEditorBlock(state.fn, env)[0], // fn *may* be set @@ -53,9 +54,18 @@ function nestedInputProperties({state, setState, suggestionPriority}: CallBlockP export function CallBlock(props: CallBlockProps) { 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); return - +
{/* Sequence of input parameters */} @@ -63,6 +73,7 @@ export function CallBlock(props: CallBlockProps) { {...props} depth={0} errorDepth={(resolved.kind === "error") ? (resolved.depth) : -1} + addParam={addParam} /> { (resolved.kind === "error") && resolved.e.toString() || (resolved.kind === "value") && @@ -91,12 +102,12 @@ function FunctionHeader(props) { // end of recursion - draw function name return  𝑓𝑛  - + ; } } -function InputParams({ depth, errorDepth, ...rest }) { +function InputParams({ depth, errorDepth, addParam, ...rest }) { const env = useContext(EnvContext); const globalContext = useContext(GlobalContext); const isOffending = depth === errorDepth; @@ -107,10 +118,12 @@ function InputParams({ depth, errorDepth, ...rest }) { {...nestedFnProperties(rest as CallBlockProps, env)} depth={depth+1} errorDepth={errorDepth} + addParam={addParam} />} {/* Our own input param */}
; } diff --git a/src/ExprBlock.tsx b/src/ExprBlock.tsx index 0b74baf..132da6e 100644 --- a/src/ExprBlock.tsx +++ b/src/ExprBlock.tsx @@ -10,7 +10,7 @@ import { LambdaBlock, type LambdaBlockState } from "./LambdaBlock"; import { LetInBlock, type LetInBlockState } from "./LetInBlock"; import { Type } from "./Type"; 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 "./ExprBlock.css"; @@ -31,6 +31,7 @@ export interface State2Props { interface ExprBlockProps extends State2Props { onCancel: () => void; + addParam: (e: ExprBlockState) => void; } function getCommands(type) { @@ -47,20 +48,7 @@ function getShortCommands(type) { return 'Tab|.'; } -function removeFocus(state: ExprBlockState): ExprBlockState { - 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) { +export function ExprBlock({state, setState, suggestionPriority, onCancel, addParam}: ExprBlockProps) { const env = useContext(EnvContext); const [needCommand, setNeedCommand] = useState(false); const commandInputRef = useRef(null); @@ -95,7 +83,6 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB kind: "call", fn: removeFocus(state), input: initialEditorState, - resolved: undefined, })); globalContext?.doHighlight.call(); // focusNextElement(); @@ -108,7 +95,6 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB kind: "call", fn: initialEditorState, input: removeFocus(state), - resolved: undefined, })); globalContext?.doHighlight.transform(); return; @@ -127,9 +113,10 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB // we become LetInBlock setState(state => ({ kind: "let", - inner: removeFocus(initialEditorState), name: "", + focus: true, value: removeFocus(state), + inner: removeFocus(initialEditorState), })); globalContext?.doHighlight.let(); return; @@ -137,9 +124,10 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB if (e.key === 'L' || e.key === '=' && e.shiftKey) { setState(state => ({ kind: "let", - inner: removeFocus(state), name: "", + focus: true, value: removeFocus(initialEditorState), + inner: removeFocus(state), })); } // a -> lAmbdA @@ -147,6 +135,7 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB setState(state => ({ kind: "lambda", paramName: "", + focus: true, expr: removeFocus(state), })); globalContext?.doHighlight.lambda(); @@ -162,6 +151,7 @@ export function ExprBlock({state, setState, onCancel, suggestionPriority}: ExprB setState={setState as (callback:(p:InputBlockState)=>ExprBlockState)=>void} suggestionPriority={suggestionPriority} onCancel={onCancel} + addParam={addParam} />; case "call": return ExprBlockState)=>void} suggestionPriority={suggestionPriority} + addParam={addParam} />; case "lambda": return ExprBlockState)=>void} suggestionPriority={suggestionPriority} + addParam={addParam} />; } } diff --git a/src/InputBlock.css b/src/InputBlock.css index 99a7b9a..0665fb3 100644 --- a/src/InputBlock.css +++ b/src/InputBlock.css @@ -9,6 +9,8 @@ background-color: transparent; color: inherit; padding: 0; + cursor: text; + outline: 0; } .suggest { top: 2.4px; diff --git a/src/InputBlock.tsx b/src/InputBlock.tsx index 1a230e9..711d63d 100644 --- a/src/InputBlock.tsx +++ b/src/InputBlock.tsx @@ -6,9 +6,12 @@ import { EnvContext } from "./EnvContext"; import type { Dynamic, ResolvedType } from "./eval"; import "./InputBlock.css"; 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 { attemptParseLiteral } from "./eval"; +import { attemptParseLiteral, removeFocus } from "./eval"; +import { GlobalContext } from "./GlobalContext"; +import { initialEditorState } from "./configurations"; +import type { CallBlockState } from "./CallBlock"; interface Literal { kind: "literal"; @@ -32,8 +35,9 @@ export interface InputBlockState { export type SuggestionType = ['literal'|'name', string, Dynamic]; export type PrioritizedSuggestionType = [number, ...SuggestionType]; -interface InputBlockProps extends State2Props { +interface InputBlockProps extends State2Props { onCancel: () => void; + addParam: (e: ExprBlockState) => void; } 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 ls - .map(suggestion => [suggestionPriority(suggestion[2]), ...suggestion] as PrioritizedSuggestionType) + .map((suggestion) => [suggestionPriority(suggestion[2]), ...suggestion] as PrioritizedSuggestionType) .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 env = useContext(EnvContext); const inputRef = useRef(null); @@ -147,7 +152,13 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In onCancel(); e.preventDefault(); } - } + }, + " ": () => { + e.preventDefault(); + if (text.length > 0) { + addParam(initialEditorState); + } + }, }; fns[e.key]?.(); }; diff --git a/src/LambdaBlock.tsx b/src/LambdaBlock.tsx index f7b9963..f49b630 100644 --- a/src/LambdaBlock.tsx +++ b/src/LambdaBlock.tsx @@ -9,20 +9,24 @@ import { autoInputWidth } from "./util/dom_trickery"; import "./LambdaBlock.css"; import { Type } from "./Type"; +import type { CallBlockState } from "./CallBlock"; export interface LambdaBlockState { kind: "lambda"; paramName: string; + focus: boolean; expr: ExprBlockState; } interface LambdaBlockProps< FnState=ExprBlockState, InputState=ExprBlockState, -> extends State2Props {} +> extends State2Props { + addParam: (e: ExprBlockState) => void; +} -export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockProps) { +export function LambdaBlock({state, setState, suggestionPriority, addParam}: LambdaBlockProps) { const env = useContext(EnvContext); const nameRef = useRef(null); @@ -43,8 +47,10 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr }; useEffect(() => { - nameRef.current?.focus(); - }, []); + if (state.focus) { + nameRef.current?.focus(); + } + }, [state.focus]); useEffect(() => autoInputWidth(nameRef, state.paramName, 60), [nameRef, state.paramName]); @@ -75,6 +81,7 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr placeholder="" onKeyDown={onChangeName} onChange={e => setParamName(e.target.value)} + spellCheck={false} />
@@ -93,6 +100,7 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr // console.log('suggestionPriority of lambdaInner... just passing through'); return suggestionPriority(s); }} + addParam={addParam} />
diff --git a/src/LetInBlock.tsx b/src/LetInBlock.tsx index d604d79..1f2aed3 100644 --- a/src/LetInBlock.tsx +++ b/src/LetInBlock.tsx @@ -8,15 +8,19 @@ import { autoInputWidth } from "./util/dom_trickery"; import { GlobalContext } from "./GlobalContext"; import "./LetInBlock.css"; +import type { CallBlockState } from "./CallBlock"; export interface LetInBlockState { kind: "let"; name: string; + focus: boolean; value: ExprBlockState; inner: ExprBlockState; } -interface LetInBlockProps extends State2Props {} +interface LetInBlockProps extends State2Props { + addParam: (e: ExprBlockState) => void; +} export function LetInBlock(props: LetInBlockProps) { return @@ -29,7 +33,7 @@ export function LetInBlock(props: LetInBlockProps) { } -function DeclColumns({state: {name, value, inner}, setState, suggestionPriority}) { +function DeclColumns({state: {name, value, inner, focus}, setState, suggestionPriority, addParam}) { const env = useContext(EnvContext); const globalContext = useContext(GlobalContext); @@ -47,8 +51,11 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority} const nameRef = useRef(null); useEffect(() => { - nameRef.current?.focus(); - }, []); + if (focus) { + nameRef.current?.focus(); + } + }, [focus]); + useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]); const [valueResolved] = evalEditorBlock(value, env); @@ -63,6 +70,7 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority} value={name} placeholder="" onChange={onChangeName} + spellCheck={false} />  =  @@ -72,6 +80,7 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority} setState={setValue} suggestionPriority={valueSuggestionPriority} onCancel={() => setState(state => state.inner)} // keep inner + addParam={addParam} /> {inner.kind === "let" && @@ -81,13 +90,14 @@ function DeclColumns({state: {name, value, inner}, setState, suggestionPriority} state={inner} setState={setInner} suggestionPriority={suggestionPriority} + addParam={addParam} /> } ; } -function InnerMost({state, setState, suggestionPriority}) { +function InnerMost({state, setState, suggestionPriority, addParam}) { const env = useContext(EnvContext); const globalContext = useContext(GlobalContext); const setInner = callback => setState(state => ({...state, inner: callback(state.inner)})); @@ -100,6 +110,7 @@ function InnerMost({state, setState, suggestionPriority}) { state={state.inner} setState={setInner} suggestionPriority={suggestionPriority} + addParam={addParam} /> ; } @@ -110,6 +121,7 @@ function InnerMost({state, setState, suggestionPriority}) { setState={setInner} suggestionPriority={suggestionPriority} onCancel={onCancel} // keep value + addParam={addParam} /> } diff --git a/src/eval.ts b/src/eval.ts index 16ee0f7..cceb6a5 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -470,4 +470,56 @@ function makeError(env: Environment, e: Error, unification: Unification=new Map( nextFreeTypeVar: idx + 1, typeVars: new Set([...env.typeVars, UNBOUND_SYMBOLS[idx]]), }]; -} \ No newline at end of file +} + +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), + } + } +}