making some good progress

This commit is contained in:
Joeri Exelmans 2025-05-11 13:22:12 +02:00
parent 5f3d697866
commit e901fc3f76
15 changed files with 546 additions and 165 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#3b8548e9af5528069f07ec62519e263511ecc34b version: git+https://deemz.org/git/joeri/dope2.git#d531c48a9298f318d089b900009f32cb9f04a53a
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#3b8548e9af5528069f07ec62519e263511ecc34b: dope2@git+https://deemz.org/git/joeri/dope2.git#d531c48a9298f318d089b900009f32cb9f04a53a:
resolution: {commit: 3b8548e9af5528069f07ec62519e263511ecc34b, repo: https://deemz.org/git/joeri/dope2.git, type: git} resolution: {commit: d531c48a9298f318d089b900009f32cb9f04a53a, 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:
@ -1758,7 +1758,7 @@ snapshots:
depd@2.0.0: {} depd@2.0.0: {}
dope2@git+https://deemz.org/git/joeri/dope2.git#3b8548e9af5528069f07ec62519e263511ecc34b: dope2@git+https://deemz.org/git/joeri/dope2.git#d531c48a9298f318d089b900009f32cb9f04a53a:
dependencies: dependencies:
functional-red-black-tree: 1.0.1 functional-red-black-tree: 1.0.1

View file

@ -1,11 +1,15 @@
// import { useState } from 'react' // import { useState } from 'react'
import { useEffect, useState } from 'react';
import './App.css' import './App.css'
import { Editor } from './Editor' import { Editor, initialEditorState, type EditorState } from './Editor'
import {module2Env, ModuleStd} from "dope2";
function App() { export function App() {
const env = module2Env(ModuleStd); const [state, setState] = useState<EditorState>(initialEditorState);
useEffect(() => {
// console.log("EDITOR STATE:", state);
}, [state]);
return ( return (
<> <>
@ -14,7 +18,7 @@ function App() {
</header> </header>
<main> <main>
<Editor env={env}></Editor> <Editor state={state} setState={setState}></Editor>
</main> </main>
<footer> <footer>
@ -23,5 +27,3 @@ function App() {
</> </>
) )
} }
export default App

65
src/CallBlock.css Normal file
View file

@ -0,0 +1,65 @@
.functionBlock {
border: solid 1px darkgray;
display: inline-block;
margin: 1px;
}
.functionName {
text-align: center;
}
.inputParam:after {
content: "";
position: absolute;
border: solid 10px transparent;
border-left-color: rgba(242, 253, 146);
margin-left: 0px;
}
.inputParam {
height: 20px;
margin-right: 20px;
/* vertical-align: ; */
/* margin-left: 0; */
display: inline-block;
/* border: solid 1px black; */
background-color: rgba(242, 253, 146);
/* border-radius: 10px; */
}
.typeAnnot {
display: inline-block;
vertical-align: top;
}
/* .outputParam:before {
content: "";
position: absolute;
width: 0px;
height: 0px;
margin-left: -28px;
margin-top: 0px;
border-style: solid;
border-color: orange;
border-width: 14px;
border-left-color: transparent;
} */
.outputParam {
vertical-align: top;
/* margin-left: 28px; */
padding: 0px;
/* padding-left: 14px; */
display: inline-block;
/* border: solid 2px orange; */
background-color: rgb(233, 224, 205);
/* border-radius: 10px; */
width: 100%;
}

75
src/CallBlock.tsx Normal file
View file

@ -0,0 +1,75 @@
import type { Dynamic, State2Props } from "./util/extra";
import { Editor, type EditorState } from "./Editor";
import "./CallBlock.css";
export interface CallBlockState {
kind: "call";
env: any;
fn: EditorState;
input: EditorState;
}
interface CallBlockProps extends State2Props<CallBlockState> {
onResolve: (d: Dynamic) => void;
}
export function CallBlock({ state: {kind, env, fn, input}, setState, onResolve }: CallBlockProps) {
const setFn = (fn: EditorState) => {
setState({kind, env, fn, input});
}
const setInput = (input: EditorState) => {
setState({kind, env, fn, input});
}
return <span className="functionBlock">
<div className="functionName">
&#119891;&#119899;&nbsp;
<Editor state={fn} setState={setFn}/>
</div>
<div className="functionParams">
<div className="outputParam">
<div className="inputParam">
<Editor state={input} setState={setInput} />
</div>
result
</div>
</div>
</span>;
}
// function FunctionBlock({env, done, name, funDynamic}) {
// const functionType = getType(funDynamic);
// const inType = functionType.params[0](functionType);
// const [outDynamic, setOutDynamic] = useState<any>(null);
// const [input, setInput] = useState<any>(null);
// const gotInput = (name, inDynamic) => {
// setInput([name, inDynamic]);
// const outDynamic = apply(inDynamic)(funDynamic);
// setOutDynamic(outDynamic);
// // propagate result up
// done(outDynamic);
// };
// return <span className="functionBlock">
// <div className="functionName">
// &#119891;&#119899;&nbsp;
// {name}
// {/* <Type type={functionType}/> */}
// </div>
// <div className="functionParams">
// <div className="outputParam">
// <div className="inputParam">
// {
// (input === null)
// ? <InputBlock env={env} done={gotInput} type={inType} />
// : <DynamicBlock env={env} name={input[0]} dynamic={input[1]}/>
// }
// </div>
// {
// (input === null)
// ? <>&nbsp;</>
// : <DynamicBlock env={env} name="" dynamic={outDynamic} />
// }
// </div>
// </div>
// </span>;
// }

View file

@ -1,138 +1,100 @@
import { useEffect, useRef, useState } from "react"; import { getSymbol, getType, module2Env, ModuleStd, symbolFunction } from "dope2";
import {growPrefix, suggest, getType, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, symbolSetIterator, symbolDictIterator, getSymbol, getHumanReadableName} from "dope2";
import "./Editor.css"; import { InputBlock, type InputBlockState } from "./InputBlock";
import { type Dynamic, type State2Props } from "./util/extra";
import { CallBlock, type CallBlockState } from "./CallBlock";
export function Editor({env}) { interface LetInBlockState {
return <Block env={env} />; kind: "let";
env: any;
name: string;
value: EditorState;
inner: EditorState;
} }
function getCursorPosition() { interface LambdaBlockState {
const selection = window.getSelection(); kind: "lambda";
if (selection) { paramName: string;
const range = selection.getRangeAt(0); expr: EditorState;
const clonedRange = range.cloneRange();
return clonedRange.startOffset;
}
} }
function setCursorPosition(elem, pos) { export type EditorState =
const range = document.createRange(); InputBlockState
range.selectNode(elem); | CallBlockState
range.setStart(elem, pos); | LetInBlockState
range.setEnd(elem, pos); | LambdaBlockState;
const selection = window.getSelection();
if (!selection) {
console.log('no selection!')
}
selection?.removeAllRanges();
selection?.addRange(range);
}
function Block({env}) { export const initialEditorState: EditorState = {
const [text, setText] = useState("edit me!"); kind: "input",
const ref = useRef<any>(null); env: module2Env(ModuleStd),
const singleSuggestion = growPrefix(env.name2dyn)(text); text: "",
const suggestions = suggest(env.name2dyn)(text)(16); resolved: undefined,
const [i, setI] = useState(0);
const resetFocus = () => {
ref.current?.focus();
}; };
useEffect(resetFocus, [ref.current])
const onSelect = ([name]) => {
setText(name);
ref.current.innerText = name;
setCursorPosition(ref.current.lastChild, name.length);
setI(0);
}
const onInput = e => {
const pos = getCursorPosition();
setText(e.target.innerText);
setCursorPosition(e.target.lastChild, pos);
};
const onKeyDown = e => {
if (e.key === "Tab") {
const newText = text + singleSuggestion;
setText(newText);
ref.current.innerText = newText;
setCursorPosition(ref.current.lastChild, newText.length);
e.preventDefault();
return;
}
if (e.key === "ArrowDown") {
setI((i + 1) % suggestions.length);
e.preventDefault();
return;
}
if (e.key === "ArrowUp") {
setI((i - 1) % suggestions.length);
e.preventDefault();
return;
}
if (e.key === "Enter") {
onSelect(suggestions[i]);
e.preventDefault();
return;
}
};
return <span>
<span ref={ref} contentEditable="plaintext-only" onInput={onInput} tabIndex={0} onKeyDown={onKeyDown} onBlur={() =>{
// hacky, but couldn't find another way:
setTimeout(resetFocus, 0);
}}></span>
<Suggestions suggestions={suggestions} onSelect={onSelect} i={i} setI={setI}/>
<span className="text-block suggest">{singleSuggestion}</span>
</span>;
}
function Suggestions({suggestions, onSelect, i, setI}) { type EditorProps = State2Props<EditorState>;
return (suggestions.length > 0) ?
<div className="suggestions"> const dontFilter = () => true;
{suggestions.map(([name, dynamic], j) => <div key={`${i}_${name}`} className={i===j?"selected":""} onClick={() => setI(j)} onDoubleClick={() => onSelect(suggestions[i])}>{name} :: <Type type={getType(dynamic)}/></div>)}
</div> export function Editor({state, setState}: EditorProps) {
: <></>; let onResolve;
switch (state.kind) {
case "input":
onResolve = (resolved: InputBlockState) => {
console.log('resolved!', state, resolved);
if (resolved) {
const type = getType(resolved.resolved);
if (getSymbol(type) === symbolFunction) {
console.log('function!');
// we were InputBlock
// now we become FunctionBlock
setState({
kind: "call",
env: state.env,
fn: resolved,
input: initialEditorState,
})
} }
}
// const type = getType(state.resolved);
function Type({type}) { // if (getSymbol(type) === symbolFunction) {
const symbol = getSymbol(type); // console.log('function!');
switch (symbol) { // console.log('editor state:', state);
case symbolFunction: // }
return <BinaryType type={type} cssClass="functionType" infix="&rarr;" prefix="" suffix=""/>; }
case symbolProduct: return <InputBlock state={state} setState={setState} filter={dontFilter} onResolve={onResolve} />;
return <BinaryType type={type} cssClass="productType" infix="&#10799;" prefix="" suffix=""/>; case "call":
case symbolSum: onResolve = (d: Dynamic) => {}
return <BinaryType type={type} cssClass="sumType" infix="+" prefix="" suffix=""/>; return <CallBlock state={state} setState={setState} onResolve={onResolve} />;
case symbolDict: case "let":
return <BinaryType type={type} cssClass="dictType" infix="&rArr;" prefix="{" suffix="}"/>; return <></>;
case symbolSet: case "lambda":
return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />; return <></>;
case symbolList:
return <UnaryType type={type} cssClass="listType" prefix="[" suffix="]" />;
case symbolSetIterator:
return <UnaryType type={type} cssClass="setType iteratorType" prefix="{*" suffix="}" />;
case symbolDictIterator:
return <BinaryType type={type} cssClass="dictType iteratorType" infix="*&rArr;" prefix="{" suffix="}"/>;
default:
return <div className="type">{getHumanReadableName(symbol)}</div>
} }
} }
function BinaryType({type, cssClass, infix, prefix, suffix}) { // function DynamicBlock({env, name, dynamic}) {
return <div className={`type ${cssClass}`}> // const type = getType(dynamic);
{prefix} // if (getSymbol(type) === symbolFunction) {
<Type type={type.params[0](type)}/> // return <FunctionBlock env={env} name={name} funDynamic={dynamic} />;
<span className="infix">{infix}</span> // }
<Type type={type.params[1](type)}/> // else return <>{getInst(dynamic).toString()} :: <Type type={type}/></>;
{suffix} // }
</div>
} // function InputBlock({env, done, type}) {
// const filterInputType = ([_, dynamic]) => {
// try {
// unify(type, getType(dynamic));
// return true;
// } catch (e) {
// if (!(e instanceof UnifyError)) {
// console.error(e);
// }
// return false;
// }
// }
// return <>
// <span className="typeAnnot"><InputBlock env={env} done={done} filter={filterInputType} /></span>
// <span className="typeAnnot">:: <Type type={type}/></span>
// </>;
// }
function UnaryType({type, cssClass, prefix, suffix}) {
return <div className={`type ${cssClass}`}>
{prefix}
<Type type={type.params[0](type)}/>
{suffix}
</div>
}

25
src/InputBlock.css Normal file
View file

@ -0,0 +1,25 @@
.suggest {
color: #aaa;
}
.suggestions {
position: absolute;
/* display: inline-block; */
/* top: 20px; */
border: solid 1px lightgrey;
cursor: pointer;
max-height: calc(100vh - 44px);
overflow: scroll;
}
.selected {
background-color: dodgerblue;
color: white;
}
.editable {
outline: 0px solid transparent;
display: inline-block;
}
.border-around-input {
border: 1px solid black;
padding: 1px;
margin: 1px;
}

149
src/InputBlock.tsx Normal file
View file

@ -0,0 +1,149 @@
import { Double, getType, Int, newDynamic, trie } from "dope2";
import { focusNextElement, focusPrevElement, getCaretPosition, setRightMostCaretPosition } from "./util/dom_trickery";
import { parseDouble, parseInt } from "./util/parse";
import { useEffect, useRef, useState } from "react";
import { Type } from "./Type";
import "./InputBlock.css";
import type { Dynamic, State2Props } from "./util/extra";
export interface InputBlockState {
kind: "input";
env: any;
text: string;
resolved: undefined | Dynamic;
}
interface InputBlockProps extends State2Props<InputBlockState> {
filter: (ls: any[]) => boolean;
onResolve: (state: InputBlockState) => void;
}
export function InputBlock({ state: {kind, env, text, resolved}, setState, filter, onResolve }: InputBlockProps) {
const ref = useRef<any>(null);
useEffect(() => {
ref.current?.focus();
if (ref.current) {
ref.current.textContent = text;
}
}, []);
const [i, setI] = useState(0); // selected suggestion
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
const setText = (text: string) => {
setState({kind, env, text, resolved});
}
const setResolved = (resolved: Dynamic) => {
setState({kind, env, text, resolved});
}
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
const asDouble = parseDouble(text);
const asInt = parseInt(text);
const suggestions = [
... (asDouble ? [[asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
... (text !== '') ? trie.suggest(env.name2dyn)(text)(Infinity) : [],
].filter(filter);
useEffect(() => {
setI(0); // reset
const found = trie.get(env.name2dyn)(text);
setResolved(found); // may be undefined
}, [text]);
const onSelectSuggestion = ([name, _dynamic]) => {
// setText(name);
// ref.current.textContent = name;
// setRightMostCaretPosition(ref.current);
// setI(0);
onResolve({
env,
kind: "input",
resolved: _dynamic,
text: name,
});
};
const onInput = e => {
setText(e.target.textContent);
};
const onKeyDown = (e: React.KeyboardEvent) => {
const fns = {
Tab: () => {
if (e.shiftKey) {
focusPrevElement();
e.preventDefault();
}
else {
// not shift key
if (singleSuggestion.length > 0) {
const newText = text + singleSuggestion;
setText(newText);
ref.current.textContent = newText;
setRightMostCaretPosition(ref.current);
e.preventDefault();
}
}
},
ArrowDown: () => {
setI((i + 1) % suggestions.length);
e.preventDefault();
},
ArrowUp: () => {
setI((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();
},
};
fns[e.key]?.();
};
return <span>
<span className="border-around-input">
<span className="editable" ref={ref} contentEditable="plaintext-only" onInput={onInput} onKeyDown={onKeyDown}
onFocus={() => setHaveFocus(true)}
onBlur={() => {
// hacky, but couldn't find another way:
// setTimeout(resetFocus, 0);
setHaveFocus(false);
} } style={{ height: 19 }}></span>
{
(haveFocus)
? <Suggestions suggestions={suggestions} onSelect={onSelectSuggestion} i={i} setI={setI} />
: <></>
}
<span className="text-block suggest">{singleSuggestion}</span>
</span>
</span>;
}
function Suggestions({ suggestions, onSelect, i, setI }) {
return (suggestions.length > 0) ?
<div className="suggestions">
{suggestions.map(([name, dynamic], j) => <div key={`${j}_${name}`} className={i === j ? "selected" : ""} onClick={() => setI(j)} onDoubleClick={() => onSelect(suggestions[i])}>{name} :: <Type type={getType(dynamic)} /></div>)}
</div>
: <></>;
}

View file

@ -1,26 +1,3 @@
.suggest {
color: #aaa;
}
.suggestions {
position: absolute;
border: solid 1px lightgrey;
cursor: pointer;
max-height: calc(100vh - 48px);
overflow: scroll;
}
.selected {
background-color: darkseagreen;
color: white;
}
[contenteditable] {
outline: 0px solid transparent;
}
/* TYPES */
.infix { .infix {
margin-left: 1px; margin-left: 1px;
margin-right: 1px; margin-right: 1px;

46
src/Type.tsx Normal file
View file

@ -0,0 +1,46 @@
import { getHumanReadableName, getSymbol, symbolDict, symbolDictIterator, symbolFunction, symbolList, symbolProduct, symbolSet, symbolSetIterator, symbolSum } from "dope2";
import "./Type.css";
export function Type({type}) {
const symbol = getSymbol(type);
switch (symbol) {
case symbolFunction:
return <BinaryType type={type} cssClass="functionType" infix="&rarr;" prefix="" suffix=""/>;
case symbolProduct:
return <BinaryType type={type} cssClass="productType" infix="&#10799;" prefix="" suffix=""/>;
case symbolSum:
return <BinaryType type={type} cssClass="sumType" infix="+" prefix="" suffix=""/>;
case symbolDict:
return <BinaryType type={type} cssClass="dictType" infix="&rArr;" prefix="{" suffix="}"/>;
case symbolSet:
return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
case symbolList:
return <UnaryType type={type} cssClass="listType" prefix="[" suffix="]" />;
case symbolSetIterator:
return <UnaryType type={type} cssClass="setType iteratorType" prefix="{*" suffix="}" />;
case symbolDictIterator:
return <BinaryType type={type} cssClass="dictType iteratorType" infix="*&rArr;" prefix="{" suffix="}"/>;
default:
return <div className="type">{getHumanReadableName(symbol)}</div>
}
}
function BinaryType({type, cssClass, infix, prefix, suffix}) {
return <div className={`type ${cssClass}`}>
{prefix}
<Type type={type.params[0](type)}/>
<span className="infix">{infix}</span>
<Type type={type.params[1](type)}/>
{suffix}
</div>
}
function UnaryType({type, cssClass, prefix, suffix}) {
return <div className={`type ${cssClass}`}>
{prefix}
<Type type={type.params[0](type)}/>
{suffix}
</div>
}

View file

@ -1,7 +1,7 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { App } from './App.tsx'
import './index.css' import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>

49
src/util/dom_trickery.ts Normal file
View file

@ -0,0 +1,49 @@
// 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('[contenteditable]'));
const index = editable.indexOf(document.activeElement);
editable[index+1]?.focus();
}
export function focusPrevElement() {
const editable = Array.from<any>(document.querySelectorAll('[contenteditable]'));
const index = editable.indexOf(document.activeElement);
const prevElem = editable[index-1]
if (prevElem) {
prevElem.focus();
setRightMostCaretPosition(prevElem);
}
}

10
src/util/extra.ts Normal file
View file

@ -0,0 +1,10 @@
export interface Dynamic {
i: any;
t: any;
}
export interface State2Props<T> {
state: T;
// setState: (callback: (state: T) => T) => void;
setState: (state: T) => void;
}

21
src/util/parse.ts Normal file
View file

@ -0,0 +1,21 @@
// Helpers...
export function parseDouble(text: string): number | undefined {
if (text === '') {
return;
}
const num = Number(text);
if (Number.isNaN(num)) {
return;
}
return num;
}
export function parseInt(text: string): bigint | undefined {
if (text === '') {
return;
}
try {
return BigInt(text);
}
catch (e) { };
}

View file

@ -10,7 +10,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
// polyfills // polyfills
"node:util": './src/fake_node_util.js', "node:util": './src/util/fake_node_util.js',
} }
} }
}); });