refactor a bit

This commit is contained in:
Joeri Exelmans 2025-05-20 14:53:24 +02:00
parent 4fee37944d
commit 230916ceb1
12 changed files with 342 additions and 398 deletions

View file

@ -4,14 +4,8 @@ import { ExprBlock, type ExprBlockState } from './ExprBlock';
import { GlobalContext } from './GlobalContext'; 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 { removeFocus } from "./eval"; import { removeFocus } from "./eval";
import { actionShortcuts, getActions } from './actions';
const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ],
["eval" , ['e','Tab','Enter'], "eval" ],
["transform", ['t', '.' ], "transform" ],
["let" , ['l', '=' ], "let … in …" ],
["lambda" , ['a' ], "λx: …" ],
];
const examples: [string, ExprBlockState][] = [ const examples: [string, ExprBlockState][] = [
["empty editor" , initialEditorState ], ["empty editor" , initialEditorState ],
@ -114,9 +108,9 @@ export function App() {
}, []); }, []);
const [highlighted, setHighlighted] = useState( const [highlighted, setHighlighted] = useState(
commands.map(() => false)); actionShortcuts.map(() => false));
const doHighlight = Object.fromEntries(commands.map(([id], i) => { const doHighlight = Object.fromEntries(actionShortcuts.map(([id], i) => {
return [id, () => { return [id, () => {
setHighlighted(h => h.with(i, true)); setHighlighted(h => h.with(i, true));
setTimeout(() => setHighlighted(h => h.with(i, false)), 100); setTimeout(() => setHighlighted(h => h.with(i, false)), 100);
@ -138,9 +132,10 @@ export function App() {
<button disabled={appState.history.length===1} onClick={onUndo}>Undo ({appState.history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button> <button disabled={appState.history.length===1} onClick={onUndo}>Undo ({appState.history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
<button disabled={appState.future.length===0} onClick={onRedo}>Redo ({appState.future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button> <button disabled={appState.future.length===0} onClick={onRedo}>Redo ({appState.future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button>
{ {
commands.map(([_, keys, descr], i) => actionShortcuts.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>)} {keys.map((key, j) => <kbd key={j}>{key}</kbd>)}
&nbsp;
{descr} {descr}
</span>) </span>)
} }
@ -173,18 +168,11 @@ export function App() {
// console.log('suggestionPriority of App, always 0'); // console.log('suggestionPriority of App, always 0');
return 0; return 0;
}} }}
addParam={(s: ExprBlockState) => { addParam={() => {
pushHistory(state => ({ getActions({doHighlight}, pushHistory).c();
kind: "call",
fn: removeFocus(state),
input: initialEditorState,
}));
doHighlight.call();
}} }}
/> />
</GlobalContext> </GlobalContext>
</main> </main>
<footer> <footer>

View file

@ -28,11 +28,6 @@
display: inline-block; display: inline-block;
} }
.commandInput {
width: 30px;
margin-left: 10px;
}
.keyword { .keyword {
color: blue; color: blue;
font-weight: bold; font-weight: bold;

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useRef, useState } from "react"; import { useContext } from "react";
import { getSymbol, getType, symbolFunction } from "dope2"; import { getType } from "dope2";
import { CallBlock, type CallBlockState } from "./CallBlock"; import { CallBlock, type CallBlockState } from "./CallBlock";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
@ -9,11 +9,11 @@ import { InputBlock, type InputBlockState } from "./InputBlock";
import { LambdaBlock, type LambdaBlockState } from "./LambdaBlock"; 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 { evalEditorBlock, type ResolvedType } from "./eval";
import { evalEditorBlock, removeFocus, type ResolvedType } from "./eval";
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
import "./ExprBlock.css"; import "./ExprBlock.css";
import { Input } from "./Input";
import { getActions } from "./actions";
export type ExprBlockState = export type ExprBlockState =
InputBlockState InputBlockState
@ -34,160 +34,57 @@ interface ExprBlockProps extends State2Props<ExprBlockState> {
addParam: (e: ExprBlockState) => void; addParam: (e: ExprBlockState) => void;
} }
function getCommands(type) {
const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '=', '.'];
if (getSymbol(type) === symbolFunction) {
commands.push('c');
}
return commands;
}
function getShortCommands(type) {
if (getSymbol(type) === symbolFunction) {
return 'c|Tab|.';
}
return 'Tab|.';
}
export function ExprBlock({state, setState, suggestionPriority, onCancel, addParam}: ExprBlockProps) { export function ExprBlock({state, setState, suggestionPriority, onCancel, addParam}: ExprBlockProps) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const [needCommand, setNeedCommand] = useState(false);
const commandInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (needCommand) {
commandInputRef.current?.focus();
}
}, [needCommand]);
const globalContext = useContext(GlobalContext); const globalContext = useContext(GlobalContext);
const onCommand = (e: React.KeyboardEvent) => {
const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', 'L', '=', '.', 'c', 'a'];
if (!commands.includes(e.key)) {
return;
}
e.preventDefault();
setNeedCommand(false);
// u -> pass Up
if (e.key === "e" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) {
// onResolve(state);
globalContext?.doHighlight.eval();
return;
}
if (e.key === "Tab" && e.shiftKey) {
setNeedCommand(false);
focusPrevElement();
}
// c -> Call
if (e.key === "c") {
// we become CallBlock
setState(state => ({
kind: "call",
fn: removeFocus(state),
input: initialEditorState,
}));
globalContext?.doHighlight.call();
// focusNextElement();
return;
}
// t -> Transform
if (e.key === "t" || e.key === ".") {
// we become CallBlock
setState(state => ({
kind: "call",
fn: initialEditorState,
input: removeFocus(state),
}));
globalContext?.doHighlight.transform();
return;
}
if (e.key === "Backspace" || e.key === "ArrowLeft") {
focusPrevElement();
return;
}
if (e.key === "ArrowRight") {
focusNextElement();
return;
}
// l -> Let ... in ...
// = -> assign to name
if (e.key === 'l' || e.key === '=' && !e.shiftKey) {
// we become LetInBlock
setState(state => ({
kind: "let",
name: "",
focus: true,
value: removeFocus(state),
inner: removeFocus(initialEditorState),
}));
globalContext?.doHighlight.let();
return;
}
if (e.key === 'L' || e.key === '=' && e.shiftKey) {
setState(state => ({
kind: "let",
name: "",
focus: true,
value: removeFocus(initialEditorState),
inner: removeFocus(state),
}));
}
// a -> lAmbdA
if (e.key === "a") {
setState(state => ({
kind: "lambda",
paramName: "",
focus: true,
expr: removeFocus(state),
}));
globalContext?.doHighlight.lambda();
return;
}
};
const renderBlock = () => { const renderBlock = {
switch (state.kind) { input: () => <InputBlock
case "input": state={state as InputBlockState}
return <InputBlock setState={setState as (callback:(p:InputBlockState)=>ExprBlockState)=>void}
state={state} suggestionPriority={suggestionPriority}
setState={setState as (callback:(p:InputBlockState)=>ExprBlockState)=>void} onCancel={onCancel}
suggestionPriority={suggestionPriority} addParam={addParam}
onCancel={onCancel} />,
addParam={addParam} call: () => <CallBlock
/>; state={state as CallBlockState}
case "call": setState={setState as (callback:(p:CallBlockState)=>ExprBlockState)=>void}
return <CallBlock suggestionPriority={suggestionPriority}
state={state} />,
setState={setState as (callback:(p:CallBlockState)=>ExprBlockState)=>void} let: () => <LetInBlock
suggestionPriority={suggestionPriority} state={state as LetInBlockState}
/>; setState={setState as (callback:(p:LetInBlockState)=>ExprBlockState)=>void}
case "let": suggestionPriority={suggestionPriority}
return <LetInBlock addParam={addParam}
state={state} />,
setState={setState as (callback:(p:LetInBlockState)=>ExprBlockState)=>void} lambda: () => <LambdaBlock
suggestionPriority={suggestionPriority} state={state as LambdaBlockState}
addParam={addParam} setState={setState as (callback:(p:LambdaBlockState)=>ExprBlockState)=>void}
/>; suggestionPriority={suggestionPriority}
case "lambda": addParam={addParam}
return <LambdaBlock />,
state={state} };
setState={setState as (callback:(p:LambdaBlockState)=>ExprBlockState)=>void}
suggestionPriority={suggestionPriority}
addParam={addParam}
/>;
}
}
const [resolved] = evalEditorBlock(state, env); const [resolved] = evalEditorBlock(state, env);
const actions = getActions(globalContext, setState);
const extraHandlers = Object.fromEntries(Object.entries(actions).map(([shortcut, action]) =>
[shortcut, (e) => { e.preventDefault(); action(); }]))
return <span className={"editor" + ((resolved.kind!=="value") ? " "+resolved.kind : "")}> return <span className={"editor" + ((resolved.kind!=="value") ? " "+resolved.kind : "")}>
{renderBlock()} {renderBlock[state.kind]()}
<div className="typeSignature"> <div className="typeSignature">
&nbsp;::&nbsp;<Type type={getType(resolved)} /> &nbsp;::&nbsp;<Type type={getType(resolved)} />
</div> </div>
<input <Input
ref={commandInputRef} placeholder="<c>"
spellCheck={false} text=""
className="editable commandInput" suggestion=""
placeholder={`<c>`} focus={false}
onKeyDown={onCommand} onEnter={() => {}}
value={""} onCancel={onCancel}
onChange={() => {}} /> onTextChange={() => {}}
setFocus={() => {}}
extraHandlers={extraHandlers}
/>
</span>; </span>;
} }

156
src/Input.tsx Normal file
View file

@ -0,0 +1,156 @@
import { useEffect, useRef, type ReactNode, type KeyboardEvent, useState } from "react";
interface InputProps {
placeholder: string;
text: string;
suggestion: string;
onTextChange: (text: string) => void;
focus: boolean;
setFocus: (focus: boolean) => void;
onEnter: () => void;
onCancel: () => void;
extraHandlers: {[key:string]: (e: KeyboardEvent) => void}
children?: ReactNode | ReactNode[];
}
const autoInputWidth = (ref: React.RefObject<HTMLInputElement| null>, text, emptyWidth=150) => {
if (ref.current) {
ref.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.0)}px`;
}
}
function getCaretPosition(ref: React.RefObject<HTMLInputElement| null>): number {
return ref.current?.selectionStart || 0;
}
// Move caret all the way to the right in the currently focused element
function setRightMostCaretPosition(elem) {
const range = document.createRange();
range.selectNode(elem);
if (elem.lastChild) { // if no text is entered, there is no lastChild
range.setStart(elem.lastChild, elem.textContent.length);
range.setEnd(elem.lastChild, elem.textContent.length);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
}
function focusNextElement() {
const editable = Array.from<any>(document.querySelectorAll('input.editable'));
const index = editable.indexOf(document.activeElement);
const nextElem = editable[index+1];
if (nextElem) {
nextElem.focus();
}
}
function focusPrevElement() {
const editable = Array.from<any>(document.querySelectorAll('input.editable'));
const index = editable.indexOf(document.activeElement);
const prevElem = editable[index-1]
if (prevElem) {
prevElem.focus();
setRightMostCaretPosition(prevElem);
}
}
export function Input({placeholder, text, suggestion, onTextChange, focus, setFocus, onEnter, onCancel, extraHandlers, children}: InputProps) {
const ref = useRef<HTMLInputElement>(null);
const [hideChildren, setHideChildren] = useState(false);
useEffect(() => {
if (focus) {
ref.current?.focus();
setHideChildren(false);
}
}, [focus]);
useEffect(() => autoInputWidth(ref, (text+(focus?suggestion:'')) || placeholder), [ref, text, suggestion, focus]);
const onKeyDown = (e: KeyboardEvent) => {
setHideChildren(false);
const keys = {
// auto-complete
Tab: () => {
if (e.shiftKey) {
focusPrevElement();
e.preventDefault();
}
else {
// not shift key
if (suggestion.length > 0) {
// complete with greyed out text
const newText = text + suggestion;
onTextChange(newText);
setRightMostCaretPosition(ref.current);
e.preventDefault();
}
else {
onEnter();
e.preventDefault();
}
}
},
Enter: () => {
onEnter();
e.preventDefault();
},
// cancel
Backspace: () => {
if (text.length === 0) {
onCancel();
e.preventDefault();
}
},
// navigate with arrows
ArrowLeft: () => {
if (getCaretPosition(ref) <= 0) {
focusPrevElement();
e.preventDefault();
}
},
ArrowRight: () => {
if (getCaretPosition(ref) === text.length) {
focusNextElement();
e.preventDefault();
}
},
Escape: () => {
if (!hideChildren) {
setHideChildren(true);
e.preventDefault();
}
},
...extraHandlers,
};
const handler = keys[e.key];
if (handler) {
handler(e);
}
};
return <span className="inputBlock">
{focus && !hideChildren && children}
<span className="editable suggest">{text}{focus && suggestion}</span>
<input ref={ref}
placeholder={placeholder}
className="editable"
value={text}
onInput={(e) =>
// @ts-ignore
onTextChange(e.target.value)}
onKeyDown={onKeyDown}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
spellCheck={false}
/>
</span>;
}

View file

@ -7,11 +7,9 @@ import type { Dynamic, ResolvedType } from "./eval";
import "./InputBlock.css"; import "./InputBlock.css";
import { Type } from "./Type"; import { Type } from "./Type";
import type { ExprBlockState, 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 { Input } from "./Input";
import { GlobalContext } from "./GlobalContext";
import { initialEditorState } from "./configurations"; import { initialEditorState } from "./configurations";
import type { CallBlockState } from "./CallBlock";
interface Literal { interface Literal {
kind: "literal"; kind: "literal";
@ -64,17 +62,14 @@ const computeSuggestions = (text, env, suggestionPriority: (s: ResolvedType) =>
} }
export function InputBlock({ state, setState, suggestionPriority, onCancel, addParam }: 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);
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 singleSuggestion = trie.growPrefix(env.names)(text); const singleSuggestion = trie.growPrefix(env.names)(text);
const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text, suggestionPriority, env]); const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text, suggestionPriority, env]);
useEffect(() => autoInputWidth(inputRef, text+singleSuggestion), [inputRef, text, singleSuggestion]);
useEffect(() => { useEffect(() => {
if (focus) { if (focus) {
@ -88,10 +83,6 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel, addP
} }
}, [suggestions.length]); }, [suggestions.length]);
const getCaretPosition = () => {
return inputRef.current?.selectionStart || -1;
}
const onTextChange = newText => { const onTextChange = newText => {
setState(state => ({...state, setState(state => ({...state,
text: newText, text: newText,
@ -101,73 +92,8 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel, addP
})); }));
} }
// fired before onInput const onSelectSuggestion = () => {
const onKeyDown = (e: React.KeyboardEvent) => { const [_priority, kind, name, dynamic] = suggestions[i];
const fns = {
Tab: () => {
if (e.shiftKey) {
focusPrevElement();
e.preventDefault();
}
else {
// not shift key
if (singleSuggestion.length > 0) {
const newText = text + singleSuggestion;
onTextChange(newText);
setRightMostCaretPosition(inputRef.current);
e.preventDefault();
}
else {
onSelectSuggestion(suggestions[i]);
e.preventDefault();
}
}
},
ArrowDown: () => {
setI((i + 1) % suggestions.length);
e.preventDefault();
},
ArrowUp: () => {
setI((suggestions.length + i - 1) % suggestions.length);
e.preventDefault();
},
ArrowLeft: () => {
if (getCaretPosition() <= 0) {
focusPrevElement();
e.preventDefault();
}
},
ArrowRight: () => {
if (getCaretPosition() === text.length) {
focusNextElement();
e.preventDefault();
}
},
Enter: () => {
onSelectSuggestion(suggestions[i]);
e.preventDefault();
},
Backspace: () => {
if (text.length === 0) {
onCancel();
e.preventDefault();
}
},
" ": () => {
e.preventDefault();
if (text.length > 0) {
addParam(initialEditorState);
}
},
};
fns[e.key]?.();
};
const onInput = e => {
onTextChange(e.target.value);
};
const onSelectSuggestion = ([priority, kind, name, dynamic]: PrioritizedSuggestionType) => {
if (kind === "literal") { if (kind === "literal") {
setState(state => ({ setState(state => ({
...state, ...state,
@ -184,29 +110,41 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel, addP
} }
}; };
return <span className="inputBlock"> const extraHandlers = {
{/* Dropdown suggestions */} ArrowDown: (e) => {
{haveFocus && setI((i + 1) % suggestions.length);
<span className="suggestionsPlaceholder"> e.preventDefault();
},
ArrowUp: (e) => {
setI((suggestions.length + i - 1) % suggestions.length);
e.preventDefault();
},
" ": (e) => {
if (text.length > 0) {
addParam(initialEditorState);
}
e.preventDefault();
},
};
return <Input
placeholder="<name or literal>"
focus={focus}
setFocus={focus => setState(state => ({...state, focus}))}
onCancel={onCancel}
onEnter={onSelectSuggestion}
onTextChange={onTextChange}
text={text}
suggestion={singleSuggestion}
extraHandlers={extraHandlers}
>
{focus && <span className="suggestionsPlaceholder">
<Suggestions <Suggestions
suggestions={suggestions} suggestions={suggestions}
onSelect={onSelectSuggestion} onSelect={onSelectSuggestion}
i={i} setI={setI} /> i={i} setI={setI} />
</span> </span>}
} </Input>
{/* Single 'grey' suggestion */}
<span className="editable suggest">{text}{singleSuggestion}</span>
{/* Input box */}
<input ref={inputRef}
placeholder="<name or literal>"
className="editable"
value={text}
onInput={onInput}
onKeyDown={onKeyDown}
onFocus={() => setHaveFocus(true)}
onBlur={() => setHaveFocus(false)}
spellCheck={false}/>
</span>;
} }
function Suggestions({ suggestions, onSelect, i, setI }) { function Suggestions({ suggestions, onSelect, i, setI }) {

View file

@ -1,15 +1,14 @@
import { useContext, useEffect, useRef } from "react"; import { useContext } from "react";
import { eqType, getSymbol, reduceUnification, trie } from "dope2"; import { eqType, getSymbol, reduceUnification } from "dope2";
import { ExprBlock, type ExprBlockState, type State2Props } from "./ExprBlock"; import { ExprBlock, type ExprBlockState, type State2Props } from "./ExprBlock";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
import { evalEditorBlock, makeInnerEnv, makeTypeVar } from "./eval"; import { evalEditorBlock, makeInnerEnv, makeTypeVar } from "./eval";
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"; import { Input } from "./Input";
export interface LambdaBlockState { export interface LambdaBlockState {
kind: "lambda"; kind: "lambda";
@ -28,7 +27,6 @@ interface LambdaBlockProps<
export function LambdaBlock({state, setState, suggestionPriority, addParam}: LambdaBlockProps) { export function LambdaBlock({state, setState, suggestionPriority, addParam}: LambdaBlockProps) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const nameRef = useRef<HTMLInputElement>(null);
const setParamName = paramName => setState(state => ({ const setParamName = paramName => setState(state => ({
...state, ...state,
@ -39,21 +37,6 @@ export function LambdaBlock({state, setState, suggestionPriority, addParam}: Lam
expr: callback(state.expr), expr: callback(state.expr),
})); }));
const onChangeName = (e) => {
if (state.paramName === "" && e.key === 'Backspace') {
setState(state => state.expr);
e.preventDefault();
}
};
useEffect(() => {
if (state.focus) {
nameRef.current?.focus();
}
}, [state.focus]);
useEffect(() => autoInputWidth(nameRef, state.paramName, 60), [nameRef, state.paramName]);
const [paramType, staticInnerEnv] = makeTypeVar(env, state.paramName); const [paramType, staticInnerEnv] = makeTypeVar(env, state.paramName);
const [exprResolved] = evalEditorBlock(state.expr, staticInnerEnv); const [exprResolved] = evalEditorBlock(state.expr, staticInnerEnv);
@ -66,23 +49,23 @@ export function LambdaBlock({state, setState, suggestionPriority, addParam}: Lam
kind: "unknown", kind: "unknown",
t: inferredParamType, t: inferredParamType,
unification: new Map(), // <- is this correct? unification: new Map(), // <- is this correct?
}) });
// const {exprResolved, env: newEnv} = computeLambdaBlockType(state.paramName, state.expr, env);
return <span className="lambdaBlock"> return <span className="lambdaBlock">
<span className="keyword">&#955;</span> <span className="keyword">&#955;</span>
&nbsp; &nbsp;
<span className="lambdaInputParam"> <span className="lambdaInputParam">
<input <Input
ref={nameRef} placeholder="<name>"
className='editable' text={state.paramName}
value={state.paramName} suggestion=""
placeholder="<name>" focus={state.focus}
onKeyDown={onChangeName} onEnter={() => {}}
onChange={e => setParamName(e.target.value)} onCancel={() => {}}
spellCheck={false} onTextChange={txt => setParamName(txt)}
/> setFocus={focus => setState(state => ({...state, focus}))}
extraHandlers={{}}
/>
</span> </span>
<div className="typeSignature"> <div className="typeSignature">
&nbsp;::&nbsp;<Type type={inferredParamType} /> &nbsp;::&nbsp;<Type type={inferredParamType} />

View file

@ -1,14 +1,13 @@
import { useContext, useEffect, useRef } from "react"; import { useContext } from "react";
import { ExprBlock, type ExprBlockState } from "./ExprBlock"; import { ExprBlock, type ExprBlockState } from "./ExprBlock";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
import { evalEditorBlock, makeInnerEnv, scoreResolved, type ResolvedType } from "./eval"; import { evalEditorBlock, makeInnerEnv, scoreResolved, type ResolvedType } from "./eval";
import { type State2Props } from "./ExprBlock"; import { type State2Props } from "./ExprBlock";
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"; import { Input } from "./Input";
export interface LetInBlockState { export interface LetInBlockState {
kind: "let"; kind: "let";
@ -39,9 +38,6 @@ function DeclColumns({state: {name, value, inner, focus}, setState, suggestionPr
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)})); const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
const setValue = callback => setState(state => ({...state, value: callback(state.value)})); const setValue = callback => setState(state => ({...state, value: callback(state.value)}));
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(state => ({...state, name: e.target.value}));
}
const valueSuggestionPriority = (suggestion: ResolvedType) => { const valueSuggestionPriority = (suggestion: ResolvedType) => {
const innerEnv = makeInnerEnv(env, name, suggestion); const innerEnv = makeInnerEnv(env, name, suggestion);
@ -49,28 +45,22 @@ function DeclColumns({state: {name, value, inner, focus}, setState, suggestionPr
return scoreResolved(resolved, suggestionPriority); return scoreResolved(resolved, suggestionPriority);
}; };
const nameRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) {
nameRef.current?.focus();
}
}, [focus]);
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
const [valueResolved] = evalEditorBlock(value, env); const [valueResolved] = evalEditorBlock(value, env);
const innerEnv = makeInnerEnv(env, name, valueResolved); const innerEnv = makeInnerEnv(env, name, valueResolved);
return <> return <>
<span className="keyword column">let&nbsp;</span> <span className="keyword column">let&nbsp;</span>
<span className="column rightAlign"> <span className="column rightAlign">
<input <Input
ref={nameRef}
className='editable'
value={name}
placeholder="<name>" placeholder="<name>"
onChange={onChangeName} text={name}
spellCheck={false} suggestion=""
focus={focus}
onEnter={() => {}}
onCancel={() => {}}
onTextChange={name => setState(state => ({...state, name}))}
setFocus={focus => setState(state => ({...state, focus}))}
extraHandlers={{}}
/> />
</span> </span>
<span className="keyword column">&nbsp;=&nbsp;</span> <span className="keyword column">&nbsp;=&nbsp;</span>

View file

@ -12,6 +12,8 @@
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-variation-settings: "wdth" 100; font-variation-settings: "wdth" 100;
color: darkgrey;
} }
.functionType { .functionType {

60
src/actions.ts Normal file
View file

@ -0,0 +1,60 @@
import { initialEditorState } from "./configurations";
import { removeFocus } from "./eval";
export const actionShortcuts: [string, string[], string][] = [
["call" , ['c'], "expr ⌴" ],
["transform", ['.'], "⌴ expr" ],
["assign" , ['a'], "let (⌴ = expr) in ⌴"],
["declare" , ['d'], "let (⌴ = ⌴) in expr"],
["lambda" , ['l'], "λ⌴. expr" ],
];
export function getActions(globalContext, setState) {
return {
c: () => {
setState(state => ({
kind: "call",
fn: removeFocus(state),
input: initialEditorState,
}));
globalContext?.doHighlight.call();
},
'.': () => {
setState(state => ({
kind: "call",
fn: initialEditorState,
input: removeFocus(state),
}));
globalContext?.doHighlight.transform();
},
a: () => {
setState(state => ({
kind: "let",
name: "",
focus: true,
value: removeFocus(state),
inner: removeFocus(initialEditorState),
}));
globalContext?.doHighlight.assign();
},
d: () => {
setState(state => ({
kind: "let",
name: "",
focus: true,
value: removeFocus(initialEditorState),
inner: removeFocus(state),
}));
globalContext?.doHighlight.declare();
},
l: () => {
setState(state => ({
kind: "lambda",
paramName: "",
focus: true,
expr: removeFocus(state),
}));
globalContext?.doHighlight.lambda();
},
};
}

View file

@ -4,6 +4,7 @@ import type { ExprBlockState } from "./ExprBlock";
import type { InputValueType } from "./InputBlock"; import type { InputValueType } from "./InputBlock";
const IS_DEV = (import.meta.env.MODE === "development"); const IS_DEV = (import.meta.env.MODE === "development");
const VERBOSE = false;
export interface Environment { export interface Environment {
names: any; names: any;
@ -43,12 +44,6 @@ export interface Unknown {
// the value of every block is either known (Dynamic), an error, or unknown // the value of every block is either known (Dynamic), an error, or unknown
export type ResolvedType = Dynamic | DeepError | Unknown; export type ResolvedType = Dynamic | DeepError | Unknown;
// export const evalEditorBlock = (s: ExprBlockState, env: Environment): [ResolvedType,Environment] => {
// const [resolved] = proxyEditorBlock(s, env);
// const [t, newEnv] = recomputeTypeVarsForEnv(resolved.t, env);
// return [{...resolved, t }, newEnv];
// };
class NotFoundError extends Error {} class NotFoundError extends Error {}
export const evalEditorBlock = (s: ExprBlockState, env: Environment): [ResolvedType,Environment] => { export const evalEditorBlock = (s: ExprBlockState, env: Environment): [ResolvedType,Environment] => {
@ -74,10 +69,10 @@ export function evalInputBlock(text: string, value: InputValueType, env: Environ
const found = trie.get(env.names)(text); const found = trie.get(env.names)(text);
if (found) { if (found) {
if (found.kind === "unknown") { if (found.kind === "unknown") {
console.log('returning', text, 'as-is'); // console.log('returning', text, 'as-is');
return [found, env]; // don't rewrite lambda parameters return [found, env]; // don't rewrite lambda parameters
} }
console.log('rewriting', text); // console.log('rewriting', text);
return recomputeTypeVarsForEnv(text, found, env); return recomputeTypeVarsForEnv(text, found, env);
} }
} }
@ -171,7 +166,7 @@ function evalCallBlock3(fnResolved: ResolvedType, inputResolved: ResolvedType, e
const [abstractOutputType, env2] = makeTypeVar(env, "<out>"); const [abstractOutputType, env2] = makeTypeVar(env, "<out>");
const matchFnType = fnType(_ => inputResolved.t)(_ => abstractOutputType); const matchFnType = fnType(_ => inputResolved.t)(_ => abstractOutputType);
if (IS_DEV) { if (VERBOSE) {
console.log('========= evalCallBlock3 =========') console.log('========= evalCallBlock3 =========')
console.log('env :', env); console.log('env :', env);
console.log('fnKind :', fnResolved.kind); console.log('fnKind :', fnResolved.kind);
@ -197,7 +192,7 @@ function evalCallBlock3(fnResolved: ResolvedType, inputResolved: ResolvedType, e
// we don't want to 'bubble up' our outType substitution, because it's just a temporary variable // we don't want to 'bubble up' our outType substitution, because it's just a temporary variable
const unificationWithoutOutType = new Map([...unification].filter(([symbol]) => symbol !== abstractOutputType.symbol)); const unificationWithoutOutType = new Map([...unification].filter(([symbol]) => symbol !== abstractOutputType.symbol));
if (IS_DEV) { if (VERBOSE) {
console.log('unification :', prettyU(unification)); console.log('unification :', prettyU(unification));
console.log('unificationInvR :', prettyRU(unificationR)); console.log('unificationInvR :', prettyRU(unificationR));
console.log('unifiedFnType :', prettyT(unifiedFnType)); console.log('unifiedFnType :', prettyT(unifiedFnType));
@ -212,7 +207,7 @@ function evalCallBlock3(fnResolved: ResolvedType, inputResolved: ResolvedType, e
// const grandUnification = unificationWithoutOutType; // const grandUnification = unificationWithoutOutType;
if (IS_DEV) { if (VERBOSE) {
console.log('grandUnification :', prettyU(grandUnification)); console.log('grandUnification :', prettyU(grandUnification));
console.log('==================================') console.log('==================================')
} }
@ -278,8 +273,6 @@ function evalCallBlock3(fnResolved: ResolvedType, inputResolved: ResolvedType, e
} }
throw e; throw e;
} }
// }
// throw e;
} }
} }
@ -296,7 +289,7 @@ const prettyRU = (rUni: Map<string, Type>) => {
export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env: Environment): [ResolvedType,Environment] { export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env: Environment): [ResolvedType,Environment] {
const [paramType, staticInnerEnv] = makeTypeVar(env, paramName); const [paramType, staticInnerEnv] = makeTypeVar(env, paramName);
if (IS_DEV) { if (VERBOSE) {
console.log('====== begin evalLambdaBlock ======') console.log('====== begin evalLambdaBlock ======')
console.log('paramName :', paramName); console.log('paramName :', paramName);
console.log('paramType :', prettyT(paramType)); console.log('paramType :', prettyT(paramType));
@ -345,7 +338,7 @@ export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env: En
// const [lambdaResolvedNormalized, resultEnv] = recomputeTypeVarsForEnv(paramName, lambdaResolved, env); // const [lambdaResolvedNormalized, resultEnv] = recomputeTypeVarsForEnv(paramName, lambdaResolved, env);
if (IS_DEV) { if (VERBOSE) {
console.log('======= end evalLambdaBlock =======') console.log('======= end evalLambdaBlock =======')
console.log('paramType :', prettyT(paramType)); console.log('paramType :', prettyT(paramType));
console.log('exprType :', prettyT(exprResolved.t)); console.log('exprType :', prettyT(exprResolved.t));
@ -353,7 +346,6 @@ export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env: En
console.log('exprUnificationR :', prettyRU(reduced)); console.log('exprUnificationR :', prettyRU(reduced));
console.log('lambdaType :', prettyT(lambdaT)); console.log('lambdaType :', prettyT(lambdaT));
console.log('lambdaTypeSubsituted:', prettyT(lambdaTSubstituted)); console.log('lambdaTypeSubsituted:', prettyT(lambdaTSubstituted));
// console.log('normalizedT :', prettyT(lambdaResolvedNormalized.t));
console.log('===================================') console.log('===================================')
} }
return [lambdaResolved, env]; return [lambdaResolved, env];

View file

@ -1,56 +0,0 @@
import type { Ref } from "react";
// If there is a caret anywhere in the document (user entering text), returns the position of the caret in the focused element
export function getCaretPosition(): number | undefined {
const selection = window.getSelection();
if (selection) {
const range = selection.getRangeAt(0);
const clonedRange = range.cloneRange();
return clonedRange.startOffset;
}
}
// export function setCursorPosition(elem, pos) {
// const range = document.createRange();
// range.selectNode(elem);
// range.setStart(elem, pos);
// range.setEnd(elem, pos);
// const selection = window.getSelection();
// selection?.removeAllRanges();
// selection?.addRange(range);
// }
// Move caret all the way to the right in the currently focused element
export function setRightMostCaretPosition(elem) {
const range = document.createRange();
range.selectNode(elem);
if (elem.lastChild) { // if no text is entered, there is no lastChild
range.setStart(elem.lastChild, elem.textContent.length);
range.setEnd(elem.lastChild, elem.textContent.length);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
}
export function focusNextElement() {
const editable = Array.from<any>(document.querySelectorAll('input'));
const index = editable.indexOf(document.activeElement);
editable[index+1]?.focus();
}
export function focusPrevElement() {
const editable = Array.from<any>(document.querySelectorAll('input'));
const index = editable.indexOf(document.activeElement);
const prevElem = editable[index-1]
if (prevElem) {
prevElem.focus();
setRightMostCaretPosition(prevElem);
}
}
export const autoInputWidth = (inputRef: React.RefObject<HTMLInputElement| null>, text, emptyWidth=150) => {
if (inputRef.current) {
inputRef.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.0)}px`;
}
}

View file

@ -1 +0,0 @@