making some good progress
This commit is contained in:
parent
5f3d697866
commit
e901fc3f76
15 changed files with 546 additions and 165 deletions
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
16
src/App.tsx
16
src/App.tsx
|
|
@ -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
65
src/CallBlock.css
Normal 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
75
src/CallBlock.tsx
Normal 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">
|
||||||
|
𝑓𝑛
|
||||||
|
<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">
|
||||||
|
// 𝑓𝑛
|
||||||
|
// {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)
|
||||||
|
// ? <> </>
|
||||||
|
// : <DynamicBlock env={env} name="" dynamic={outDynamic} />
|
||||||
|
// }
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </span>;
|
||||||
|
// }
|
||||||
210
src/Editor.tsx
210
src/Editor.tsx
|
|
@ -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="→" prefix="" suffix=""/>;
|
}
|
||||||
case symbolProduct:
|
return <InputBlock state={state} setState={setState} filter={dontFilter} onResolve={onResolve} />;
|
||||||
return <BinaryType type={type} cssClass="productType" infix="⨯" 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="⇒" 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="*⇒" 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
25
src/InputBlock.css
Normal 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
149
src/InputBlock.tsx
Normal 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>
|
||||||
|
: <></>;
|
||||||
|
}
|
||||||
|
|
@ -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
46
src/Type.tsx
Normal 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="→" prefix="" suffix=""/>;
|
||||||
|
case symbolProduct:
|
||||||
|
return <BinaryType type={type} cssClass="productType" infix="⨯" prefix="" suffix=""/>;
|
||||||
|
case symbolSum:
|
||||||
|
return <BinaryType type={type} cssClass="sumType" infix="+" prefix="" suffix=""/>;
|
||||||
|
case symbolDict:
|
||||||
|
return <BinaryType type={type} cssClass="dictType" infix="⇒" 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="*⇒" 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>
|
||||||
|
}
|
||||||
|
|
@ -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
49
src/util/dom_trickery.ts
Normal 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
10
src/util/extra.ts
Normal 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
21
src/util/parse.ts
Normal 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) { };
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue