simplify some things

This commit is contained in:
Joeri Exelmans 2025-05-13 13:11:57 +02:00
parent fa70d2f3f4
commit 9ef160aeb7
8 changed files with 105 additions and 30 deletions

View file

@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import './App.css' import './App.css'
import { Editor, type EditorState } from './Editor' import { Editor, type EditorState } from './Editor'
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations"; import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
import { CommandContext } from './CommandContext';
export function App() { export function App() {
const [history, setHistory] = useState([initialEditorState]); const [history, setHistory] = useState([initialEditorState]);
@ -48,14 +49,39 @@ export function App() {
window.onkeydown = onKeyDown; window.onkeydown = onKeyDown;
}, []); }, []);
const commands = [
["call" , "[c] call" ],
["eval" , "[u] [Tab] [Enter] eval"],
["transform", "[t] transform" ],
["let" , "[=] let ... in ..." ],
];
const [highlighted, setHighlighted] = useState(
commands.map(() => false));
const doHighlight = Object.fromEntries(commands.map(([id], i) => {
return [id, () => {
setHighlighted(h => h.with(i, true));
setTimeout(() => setHighlighted(h => h.with(i, false)), 100);
}];
}));
return ( return (
<> <>
<header> <header>
<button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) [Ctrl+Z]</button> <button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) [Ctrl+Z]</button>
<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.map(([_, descr], i) =>
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
{descr}
</span>)
}
</header> </header>
<main> <main>
<CommandContext value={doHighlight}>
<Editor <Editor
state={history.at(-1)!} state={history.at(-1)!}
setState={pushHistory} setState={pushHistory}
@ -64,6 +90,7 @@ export function App() {
filter={() => true} filter={() => true}
focus={true} focus={true}
/> />
</CommandContext>
</main> </main>
<footer> <footer>

View file

@ -105,7 +105,6 @@ export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
<InputParams <InputParams
fn={state.fn} setFn={setFn} fn={state.fn} setFn={setFn}
input={state.input} setInput={setInput} input={state.input} setInput={setInput}
focus={true}
onFnResolve={onFnResolve} onFnResolve={onFnResolve}
onInputResolve={onInputResolve} onInputCancel={onInputCancel} /> onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
@ -158,7 +157,6 @@ function FunctionHeader({ fn, setFn, input, onFnResolve }) {
<Editor <Editor
state={fn} state={fn}
setState={setFn} setState={setFn}
focus={false}
onResolve={onFnResolve} onResolve={onFnResolve}
onCancel={() => {/*todo*/}} onCancel={() => {/*todo*/}}
filter={filterCompatibleFns} /> filter={filterCompatibleFns} />
@ -166,7 +164,7 @@ function FunctionHeader({ fn, setFn, input, onFnResolve }) {
} }
} }
function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel, focus }) { function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) {
const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => { const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => {
if (fn.resolved) { if (fn.resolved) {
try { try {
@ -195,7 +193,7 @@ function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve,
onResolve={onInputResolve} onResolve={onInputResolve}
onCancel={onInputCancel} onCancel={onInputCancel}
filter={filterCompatibleInputs} filter={filterCompatibleInputs}
focus={focus} /> />
</div>; </div>;
} }
@ -212,5 +210,5 @@ function NestedInputParams({fn, setFn, onFnResolve}) {
onFnResolve={onFnFnResolve} onFnResolve={onFnFnResolve}
onInputResolve={onFnInputResolve} onInputResolve={onFnInputResolve}
onInputCancel={() => {/*todo*/}} onInputCancel={() => {/*todo*/}}
focus={false}/>; />;
} }

3
src/CommandContext.ts Normal file
View file

@ -0,0 +1,3 @@
import { createContext } from "react";
export const CommandContext = createContext<{[key:string]: () => void}>({});

View file

@ -3,5 +3,5 @@
} }
.command { .command {
width: 136px; width: 160px;
} }

View file

@ -1,16 +1,17 @@
import { getSymbol, getType, symbolFunction } from "dope2"; import { getSymbol, getType, symbolFunction } from "dope2";
import { useEffect, useReducer, useRef, useState } from "react"; import { useContext, useEffect, useReducer, useRef, useState } from "react";
import { CallBlock, type CallBlockState } from "./CallBlock"; import { CallBlock, type CallBlockState } from "./CallBlock";
import { InputBlock, type InputBlockState } from "./InputBlock"; import { InputBlock, type InputBlockState } from "./InputBlock";
import { Type } from "./Type"; import { Type } from "./Type";
import { type Dynamic, type State2Props } from "./util/extra"; import { type Dynamic, type State2Props } from "./util/extra";
import "./Editor.css"; import "./Editor.css";
import type { LetInBlockState } from "./LetInBlock"; import { LetInBlock, type LetInBlockState } from "./LetInBlock";
import { focusNextElement, focusPrevElement } from "./util/dom_trickery"; import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
import type { LambdaBlockState } from "./LambdaBlock"; import type { LambdaBlockState } from "./LambdaBlock";
import { initialEditorState } from "./configurations"; import { initialEditorState } from "./configurations";
import { CommandContext } from "./CommandContext";
export type EditorState = export type EditorState =
InputBlockState InputBlockState
@ -19,7 +20,6 @@ export type EditorState =
| LambdaBlockState; | LambdaBlockState;
interface EditorProps extends State2Props<EditorState> { interface EditorProps extends State2Props<EditorState> {
focus: boolean;
filter: (suggestion: [string, Dynamic]) => boolean; filter: (suggestion: [string, Dynamic]) => boolean;
onResolve: (state: EditorState) => void; onResolve: (state: EditorState) => void;
onCancel: () => void; onCancel: () => void;
@ -32,6 +32,12 @@ function getCommands(type) {
} }
return commands; return commands;
} }
function getShortCommands(type) {
if (getSymbol(type) === symbolFunction) {
return 'c|Tab|t';
}
return 'Tab|t';
}
function removeFocus(state: EditorState): EditorState { function removeFocus(state: EditorState): EditorState {
if (state.kind === "input") { if (state.kind === "input") {
@ -46,7 +52,7 @@ function removeFocus(state: EditorState): EditorState {
return state; return state;
} }
export function Editor({state, setState, onResolve, onCancel, filter, focus}: EditorProps) { export function Editor({state, setState, onResolve, 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(() => {
@ -65,7 +71,8 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
onResolve(editorState); // pass up the fact that we're unresolved onResolve(editorState); // pass up the fact that we're unresolved
} }
} }
// const onMyCancel
const doHighlight = 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);
@ -77,6 +84,7 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
// u -> pass Up // u -> pass Up
if (e.key === "u" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) { if (e.key === "u" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) {
onResolve(state); onResolve(state);
doHighlight.eval();
return; return;
} }
if (e.key === "Tab" && e.shiftKey) { if (e.key === "Tab" && e.shiftKey) {
@ -93,6 +101,7 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
input: initialEditorState, input: initialEditorState,
resolved: undefined, resolved: undefined,
}); });
doHighlight.call();
// focusNextElement(); // focusNextElement();
return; return;
} }
@ -106,6 +115,7 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
input: removeFocus(state), input: removeFocus(state),
resolved: undefined, resolved: undefined,
}); });
doHighlight.transform();
return; return;
} }
if (e.key === "Backspace" || e.key === "ArrowLeft") { if (e.key === "Backspace" || e.key === "ArrowLeft") {
@ -128,6 +138,7 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
value: state, value: state,
resolved: undefined, resolved: undefined,
}); });
doHighlight.let();
return; return;
} }
}; };
@ -140,14 +151,20 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
setState={setState} setState={setState}
filter={filter} filter={filter}
onResolve={onMyResolve} onResolve={onMyResolve}
onCancel={onCancel} />; onCancel={onCancel}
/>;
case "call": case "call":
return <CallBlock return <CallBlock
state={state} state={state}
setState={setState} setState={setState}
onResolve={onMyResolve} />; onResolve={onMyResolve}
/>;
case "let": case "let":
return <></>; return <LetInBlock
state={state}
setState={setState}
onResolve={() => {}}
/>;
case "lambda": case "lambda":
return <></>; return <></>;
} }
@ -163,7 +180,7 @@ export function Editor({state, setState, onResolve, onCancel, filter, focus}: Ed
ref={commandInputRef} ref={commandInputRef}
spellCheck={false} spellCheck={false}
className="editable command" className="editable command"
placeholder="<enter command>" placeholder={`<command: ${getShortCommands(getType(state.resolved))}>`}
onKeyDown={onCommand} onKeyDown={onCommand}
value={""} value={""}
onChange={() => {}} /> /* gets rid of React warning */ onChange={() => {}} /> /* gets rid of React warning */

View file

@ -1,4 +1,4 @@
import type { EditorState } from "./Editor"; import { Editor, type EditorState } from "./Editor";
import type { Dynamic, State2Props } from "./util/extra"; import type { Dynamic, State2Props } from "./util/extra";
@ -16,6 +16,36 @@ interface LetInBlockProps extends State2Props<LetInBlockState> {
} }
export function LetInBlock({env, name, value, inner, resolved, onResolve}) { export function LetInBlock({state, setState, onResolve}: LetInBlockProps) {
const {env, name, value, inner, resolved} = state;
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setState({...state, name: e.target.value});
}
return <span className="letIn">
<div className="decl">
let <input
className='editable'
value={name}
placeholder="<variable name>"
onChange={onChangeName}
/> =
<Editor
state={value}
filter={() => true}
onResolve={(state: EditorState) => {} }
onCancel={() => {} }
setState={(state: EditorState) => {} }
/>
in
</div>
<div className="inner">
<Editor
state={inner}
filter={() => true}
onResolve={(state: EditorState) => {} }
onCancel={() => {} }
setState={(state: EditorState) => {} }
/>
</div>
</span>
} }

View file

@ -1,9 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020", "target": "ES2023",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,

View file

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022", "target": "ES2023",
"lib": ["ES2023"], "lib": ["ES2023"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,