Compare commits
2 commits
e901fc3f76
...
2b0d8bc2c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b0d8bc2c6 | |||
| a9ae4f9888 |
15 changed files with 408 additions and 171 deletions
|
|
@ -3,11 +3,11 @@ import { useEffect, useState } from 'react';
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { Editor, initialEditorState, type EditorState } from './Editor'
|
import { Editor, initialEditorState, type EditorState } from './Editor'
|
||||||
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [state, setState] = useState<EditorState>(initialEditorState);
|
const [state, setState] = useState<EditorState>(initialEditorState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
window['APP_STATE'] = state;
|
||||||
// console.log("EDITOR STATE:", state);
|
// console.log("EDITOR STATE:", state);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
|
@ -18,7 +18,12 @@ export function App() {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Editor state={state} setState={setState}></Editor>
|
<Editor
|
||||||
|
state={state}
|
||||||
|
setState={setState}
|
||||||
|
onResolve={() => {console.log("toplevel resolved")}}
|
||||||
|
onCancel={() => {console.log("toplevel canceled")}}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.functionName {
|
.functionName {
|
||||||
text-align: center;
|
/* text-align: center; */
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,43 +16,23 @@
|
||||||
border: solid 10px transparent;
|
border: solid 10px transparent;
|
||||||
border-left-color: rgba(242, 253, 146);
|
border-left-color: rgba(242, 253, 146);
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
/* z-index: 1; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputParam {
|
.inputParam {
|
||||||
height: 20px;
|
/* height: 20px; */
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
/* vertical-align: ; */
|
|
||||||
/* margin-left: 0; */
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
/* border: solid 1px black; */
|
|
||||||
background-color: rgba(242, 253, 146);
|
background-color: rgba(242, 253, 146);
|
||||||
/* border-radius: 10px; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.typeAnnot {
|
.typeAnnot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
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 {
|
.outputParam {
|
||||||
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
/* margin-left: 28px; */
|
/* margin-left: 28px; */
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,104 @@
|
||||||
|
import { apply, getType, getInst } from "dope2";
|
||||||
import type { Dynamic, State2Props } from "./util/extra";
|
import type { Dynamic, State2Props } from "./util/extra";
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
|
|
||||||
import "./CallBlock.css";
|
import "./CallBlock.css";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Type } from "./Type";
|
||||||
|
import { Value } from "./Value";
|
||||||
|
import { focusPrevElement } from "./util/dom_trickery";
|
||||||
|
|
||||||
export interface CallBlockState {
|
export interface CallBlockState {
|
||||||
kind: "call";
|
kind: "call";
|
||||||
env: any;
|
env: any;
|
||||||
fn: EditorState;
|
fn: EditorState;
|
||||||
input: EditorState;
|
input: EditorState;
|
||||||
|
resolved: undefined | Dynamic;
|
||||||
|
rollback: EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CallBlockProps extends State2Props<CallBlockState> {
|
interface CallBlockProps extends State2Props<CallBlockState> {
|
||||||
onResolve: (d: Dynamic) => void;
|
onResolve: (resolved: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallBlock({ state: {kind, env, fn, input}, setState, onResolve }: CallBlockProps) {
|
export function CallBlock({ state: {kind, env, fn, input, resolved, rollback }, setState, onResolve }: CallBlockProps) {
|
||||||
|
const setResolved = (resolved?: Dynamic) => {
|
||||||
|
setState({kind, env, fn, input, resolved, rollback});
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeTheCall = (input, fn) => {
|
||||||
|
console.log('makeTheCall...')
|
||||||
|
try {
|
||||||
|
const outputResolved = apply(input.resolved)(fn.resolved);
|
||||||
|
setResolved(outputResolved);
|
||||||
|
console.log("onResolve callblock..")
|
||||||
|
onResolve({
|
||||||
|
kind, env, fn, input, resolved: outputResolved, rollback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log('makeTheCall:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setFn = (fn: EditorState) => {
|
const setFn = (fn: EditorState) => {
|
||||||
setState({kind, env, fn, input});
|
setState({kind, env, fn, input, resolved, rollback});
|
||||||
}
|
}
|
||||||
const setInput = (input: EditorState) => {
|
const setInput = (input: EditorState) => {
|
||||||
setState({kind, env, fn, input});
|
setState({kind, env, fn, input, resolved, rollback});
|
||||||
}
|
}
|
||||||
|
const onFnResolve = (fnState) => {
|
||||||
|
console.log('my fn resolved')
|
||||||
|
if (input.resolved) {
|
||||||
|
makeTheCall(input, fnState);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// setFn(fnState);
|
||||||
|
setResolved(undefined);
|
||||||
|
onResolve({
|
||||||
|
kind, env, fn: fnState, input, resolved: undefined, rollback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onInputResolve = (inputState) => {
|
||||||
|
console.log('my input resolved')
|
||||||
|
if (fn.resolved) {
|
||||||
|
makeTheCall(inputState, fn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// setInput(inputState);
|
||||||
|
setResolved(undefined);
|
||||||
|
onResolve({
|
||||||
|
kind, env, fn, input: inputState, resolved: undefined, rollback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onFnCancel = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
const onInputCancel = () => {
|
||||||
|
// we become what we were before we became a CallBlock
|
||||||
|
if (rollback) {
|
||||||
|
setState(rollback);
|
||||||
|
focusPrevElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return <span className="functionBlock">
|
return <span className="functionBlock">
|
||||||
<div className="functionName">
|
<div className="functionName">
|
||||||
𝑓𝑛
|
𝑓𝑛
|
||||||
<Editor state={fn} setState={setFn}/>
|
<Editor state={fn} setState={setFn}
|
||||||
|
onResolve={onFnResolve} onCancel={onFnCancel}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="functionParams">
|
<div className="functionParams">
|
||||||
<div className="outputParam">
|
<div className="outputParam">
|
||||||
<div className="inputParam">
|
<div className="inputParam">
|
||||||
<Editor state={input} setState={setInput} />
|
<Editor state={input} setState={setInput} onResolve={onInputResolve} onCancel={onInputCancel} />
|
||||||
</div>
|
</div>
|
||||||
result
|
{ resolved
|
||||||
|
? <Value dynamic={resolved} />
|
||||||
|
: <></> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>;
|
</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>;
|
|
||||||
// }
|
|
||||||
8
src/Editor.css
Normal file
8
src/Editor.css
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.typeSignature {
|
||||||
|
display: inline-block;
|
||||||
|
/* vertical-align:; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.command {
|
||||||
|
width: 136px;
|
||||||
|
}
|
||||||
165
src/Editor.tsx
165
src/Editor.tsx
|
|
@ -3,6 +3,11 @@ import { getSymbol, getType, module2Env, ModuleStd, symbolFunction } from "dope2
|
||||||
import { InputBlock, type InputBlockState } from "./InputBlock";
|
import { InputBlock, type InputBlockState } from "./InputBlock";
|
||||||
import { type Dynamic, type State2Props } from "./util/extra";
|
import { type Dynamic, type State2Props } from "./util/extra";
|
||||||
import { CallBlock, type CallBlockState } from "./CallBlock";
|
import { CallBlock, type CallBlockState } from "./CallBlock";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Type } from "./Type";
|
||||||
|
|
||||||
|
import "./Editor.css"
|
||||||
|
import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
|
||||||
|
|
||||||
interface LetInBlockState {
|
interface LetInBlockState {
|
||||||
kind: "let";
|
kind: "let";
|
||||||
|
|
@ -10,12 +15,17 @@ interface LetInBlockState {
|
||||||
name: string;
|
name: string;
|
||||||
value: EditorState;
|
value: EditorState;
|
||||||
inner: EditorState;
|
inner: EditorState;
|
||||||
|
resolved: undefined | Dynamic;
|
||||||
|
rollback?: EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LambdaBlockState {
|
interface LambdaBlockState {
|
||||||
kind: "lambda";
|
kind: "lambda";
|
||||||
|
env: any;
|
||||||
paramName: string;
|
paramName: string;
|
||||||
expr: EditorState;
|
expr: EditorState;
|
||||||
|
resolved: undefined | Dynamic;
|
||||||
|
rollback?: EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditorState =
|
export type EditorState =
|
||||||
|
|
@ -29,72 +39,107 @@ export const initialEditorState: EditorState = {
|
||||||
env: module2Env(ModuleStd),
|
env: module2Env(ModuleStd),
|
||||||
text: "",
|
text: "",
|
||||||
resolved: undefined,
|
resolved: undefined,
|
||||||
|
rollback: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditorProps = State2Props<EditorState>;
|
interface EditorProps extends State2Props<EditorState> {
|
||||||
|
onResolve: (state: EditorState) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const dontFilter = () => true;
|
const dontFilter = () => true;
|
||||||
|
|
||||||
export function Editor({state, setState}: EditorProps) {
|
function getCommands(type) {
|
||||||
let onResolve;
|
const commands = ['u', 't', 'Enter', 'Backspace', 'ArrowLeft', 'Tab'];
|
||||||
switch (state.kind) {
|
if (getSymbol(type) === symbolFunction) {
|
||||||
case "input":
|
commands.push('c');
|
||||||
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);
|
|
||||||
// if (getSymbol(type) === symbolFunction) {
|
|
||||||
// console.log('function!');
|
|
||||||
// console.log('editor state:', state);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
return <InputBlock state={state} setState={setState} filter={dontFilter} onResolve={onResolve} />;
|
|
||||||
case "call":
|
|
||||||
onResolve = (d: Dynamic) => {}
|
|
||||||
return <CallBlock state={state} setState={setState} onResolve={onResolve} />;
|
|
||||||
case "let":
|
|
||||||
return <></>;
|
|
||||||
case "lambda":
|
|
||||||
return <></>;
|
|
||||||
}
|
}
|
||||||
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
// function DynamicBlock({env, name, dynamic}) {
|
export function Editor({state, setState, onResolve, onCancel}: EditorProps) {
|
||||||
// const type = getType(dynamic);
|
const [needCommand, setNeedCommand] = useState(false);
|
||||||
// if (getSymbol(type) === symbolFunction) {
|
const onMyResolve = (editorState: EditorState) => {
|
||||||
// return <FunctionBlock env={env} name={name} funDynamic={dynamic} />;
|
setState(editorState);
|
||||||
// }
|
if (editorState.resolved) {
|
||||||
// else return <>{getInst(dynamic).toString()} :: <Type type={type}/></>;
|
setNeedCommand(true);
|
||||||
// }
|
}
|
||||||
|
else {
|
||||||
// function InputBlock({env, done, type}) {
|
// unresolved
|
||||||
// const filterInputType = ([_, dynamic]) => {
|
setNeedCommand(false);
|
||||||
// try {
|
onResolve(editorState); // pass up the fact that we're unresolved
|
||||||
// unify(type, getType(dynamic));
|
}
|
||||||
// return true;
|
}
|
||||||
// } catch (e) {
|
// const onMyCancel
|
||||||
// if (!(e instanceof UnifyError)) {
|
const onCommand = (e: React.KeyboardEvent) => {
|
||||||
// console.error(e);
|
const type = getType(state.resolved);
|
||||||
// }
|
const commands = getCommands(type);
|
||||||
// return false;
|
if (!commands.includes(e.key)) {
|
||||||
// }
|
return;
|
||||||
// }
|
}
|
||||||
// return <>
|
e.preventDefault();
|
||||||
// <span className="typeAnnot"><InputBlock env={env} done={done} filter={filterInputType} /></span>
|
setNeedCommand(false);
|
||||||
// <span className="typeAnnot">:: <Type type={type}/></span>
|
// u -> pass Up
|
||||||
// </>;
|
if (e.key === "u" || e.key === "Enter" || e.key === "Tab") {
|
||||||
// }
|
onResolve(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// c -> Call
|
||||||
|
if (e.key === "c") {
|
||||||
|
// we become CallBlock
|
||||||
|
setState({
|
||||||
|
kind: "call",
|
||||||
|
env: state.env,
|
||||||
|
fn: state,
|
||||||
|
input: initialEditorState,
|
||||||
|
resolved: undefined,
|
||||||
|
rollback: state,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// t -> Transform
|
||||||
|
if (e.key === "t") {
|
||||||
|
// we become CallBlock
|
||||||
|
setState({
|
||||||
|
kind: "call",
|
||||||
|
env: state.env,
|
||||||
|
fn: initialEditorState,
|
||||||
|
input: state,
|
||||||
|
resolved: undefined,
|
||||||
|
rollback: state,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Backspace" || e.key === "ArrowLeft") {
|
||||||
|
focusPrevElement();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBlock = () => {
|
||||||
|
switch (state.kind) {
|
||||||
|
case "input":
|
||||||
|
return <InputBlock state={state} setState={setState} filter={dontFilter} onResolve={onMyResolve} onCancel={onCancel} />;
|
||||||
|
case "call":
|
||||||
|
return <CallBlock state={state} setState={setState} onResolve={onMyResolve} />;
|
||||||
|
case "let":
|
||||||
|
return <></>;
|
||||||
|
case "lambda":
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
{renderBlock()}
|
||||||
|
{
|
||||||
|
(state.resolved)
|
||||||
|
? <div className="typeSignature">
|
||||||
|
:: <Type type={getType(state.resolved)} />
|
||||||
|
{ (needCommand)
|
||||||
|
? <input autoFocus={true} className="editable command" placeholder="<enter command>" onKeyDown={onCommand} value=""/>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,32 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@500&display=swap');
|
||||||
|
|
||||||
.suggest {
|
.suggest {
|
||||||
|
margin-left: -3.5px;
|
||||||
|
margin-right: 5px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
min-width: 30px;
|
||||||
|
|
||||||
|
font-size: 13pt;
|
||||||
|
font-family: "Inconsolata", monospace;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings: "wdth" 100;
|
||||||
|
|
||||||
}
|
}
|
||||||
.suggestions {
|
.suggestions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.suggestions {
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
/* display: inline-block; */
|
border: solid 1px dodgerblue;
|
||||||
/* top: 20px; */
|
|
||||||
border: solid 1px lightgrey;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-height: calc(100vh - 44px);
|
max-height: calc(100vh - 44px);
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
.selected {
|
.selected {
|
||||||
background-color: dodgerblue;
|
background-color: dodgerblue;
|
||||||
|
|
@ -17,6 +35,23 @@
|
||||||
.editable {
|
.editable {
|
||||||
outline: 0px solid transparent;
|
outline: 0px solid transparent;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
border: 0;
|
||||||
|
/* box-sizing: border-box; */
|
||||||
|
/* width: ; */
|
||||||
|
/* border: 1px black solid; */
|
||||||
|
/* border-style: dashed none dashed; */
|
||||||
|
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
font-size: 13pt;
|
||||||
|
font-family: "Inconsolata", monospace;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings: "wdth" 100;
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.border-around-input {
|
.border-around-input {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,37 @@ import { Type } from "./Type";
|
||||||
|
|
||||||
import "./InputBlock.css";
|
import "./InputBlock.css";
|
||||||
import type { Dynamic, State2Props } from "./util/extra";
|
import type { Dynamic, State2Props } from "./util/extra";
|
||||||
|
import type { EditorState } from "./Editor";
|
||||||
|
import { ShowIf } from "./ShowIf";
|
||||||
|
|
||||||
export interface InputBlockState {
|
export interface InputBlockState {
|
||||||
kind: "input";
|
kind: "input";
|
||||||
env: any;
|
env: any;
|
||||||
text: string;
|
text: string;
|
||||||
resolved: undefined | Dynamic;
|
resolved: undefined | Dynamic;
|
||||||
|
rollback?: EditorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputBlockProps extends State2Props<InputBlockState> {
|
interface InputBlockProps extends State2Props<InputBlockState> {
|
||||||
filter: (ls: any[]) => boolean;
|
filter: (ls: any[]) => boolean;
|
||||||
onResolve: (state: InputBlockState) => void;
|
onResolve: (state: InputBlockState) => void;
|
||||||
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputBlock({ state: {kind, env, text, resolved}, setState, filter, onResolve }: InputBlockProps) {
|
export function InputBlock({ state: {kind, env, text, resolved, rollback}, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
||||||
const ref = useRef<any>(null);
|
const ref = useRef<any>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
if (ref.current) {
|
|
||||||
ref.current.textContent = text;
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [i, setI] = useState(0); // selected suggestion
|
const [i, setI] = useState(0); // selected suggestion
|
||||||
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
||||||
|
|
||||||
const setText = (text: string) => {
|
const setText = (text: string) => {
|
||||||
setState({kind, env, text, resolved});
|
setState({kind, env, text, resolved, rollback});
|
||||||
}
|
}
|
||||||
const setResolved = (resolved: Dynamic) => {
|
const setResolved = (resolved: Dynamic) => {
|
||||||
setState({kind, env, text, resolved});
|
setState({kind, env, text, resolved, rollback});
|
||||||
}
|
}
|
||||||
|
|
||||||
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
||||||
|
|
@ -53,28 +54,45 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setI(0); // reset
|
setI(0); // reset
|
||||||
const found = trie.get(env.name2dyn)(text);
|
if (ref.current) {
|
||||||
setResolved(found); // may be undefined
|
ref.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
||||||
|
}
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
|
const onSelectSuggestion = ([name, dynamic]) => {
|
||||||
const onSelectSuggestion = ([name, _dynamic]) => {
|
console.log(name);
|
||||||
// setText(name);
|
// setText(name);
|
||||||
// ref.current.textContent = name;
|
// ref.current.textContent = name;
|
||||||
// setRightMostCaretPosition(ref.current);
|
// setRightMostCaretPosition(ref.current);
|
||||||
// setI(0);
|
// setI(0);
|
||||||
|
// setResolved(dynamic);
|
||||||
|
console.log("onResolve inputblock..")
|
||||||
onResolve({
|
onResolve({
|
||||||
env,
|
|
||||||
kind: "input",
|
kind: "input",
|
||||||
resolved: _dynamic,
|
env,
|
||||||
text: name,
|
text: name,
|
||||||
|
resolved: dynamic,
|
||||||
|
rollback,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInput = e => {
|
const onInput = e => {
|
||||||
setText(e.target.textContent);
|
setText(e.target.value);
|
||||||
|
if (resolved) {
|
||||||
|
onResolve({
|
||||||
|
kind: "input",
|
||||||
|
env,
|
||||||
|
text: e.target.value,
|
||||||
|
resolved: undefined,
|
||||||
|
rollback,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCaretPosition = () => {
|
||||||
|
return ref.current.selectionStart;
|
||||||
|
}
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
const fns = {
|
const fns = {
|
||||||
Tab: () => {
|
Tab: () => {
|
||||||
|
|
@ -87,10 +105,14 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
||||||
if (singleSuggestion.length > 0) {
|
if (singleSuggestion.length > 0) {
|
||||||
const newText = text + singleSuggestion;
|
const newText = text + singleSuggestion;
|
||||||
setText(newText);
|
setText(newText);
|
||||||
ref.current.textContent = newText;
|
// ref.current.textContent = newText;
|
||||||
setRightMostCaretPosition(ref.current);
|
setRightMostCaretPosition(ref.current);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
onSelectSuggestion(suggestions[i]);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ArrowDown: () => {
|
ArrowDown: () => {
|
||||||
|
|
@ -117,33 +139,50 @@ export function InputBlock({ state: {kind, env, text, resolved}, setState, filte
|
||||||
onSelectSuggestion(suggestions[i]);
|
onSelectSuggestion(suggestions[i]);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
},
|
},
|
||||||
|
Backspace: () => {
|
||||||
|
if (text.length === 0) {
|
||||||
|
onCancel();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
fns[e.key]?.();
|
fns[e.key]?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return <span>
|
return <span>
|
||||||
<span className="border-around-input">
|
<span className="">
|
||||||
<span className="editable" ref={ref} contentEditable="plaintext-only" onInput={onInput} onKeyDown={onKeyDown}
|
<input ref={ref} placeholder="start typing..." className="editable" value={text} onInput={onInput} onKeyDown={onKeyDown} onFocus={() => setHaveFocus(true)} onBlur={() => setTimeout(() => setHaveFocus(false), 200)}/>
|
||||||
onFocus={() => setHaveFocus(true)}
|
<span className="text-block suggest">{singleSuggestion}</span>
|
||||||
onBlur={() => {
|
</span>
|
||||||
// hacky, but couldn't find another way:
|
<ShowIf cond={haveFocus}>
|
||||||
// setTimeout(resetFocus, 0);
|
<Suggestions
|
||||||
setHaveFocus(false);
|
suggestions={suggestions}
|
||||||
} } style={{ height: 19 }}></span>
|
onSelect={onSelectSuggestion}
|
||||||
{
|
i={i} setI={setI} />
|
||||||
(haveFocus)
|
</ShowIf>
|
||||||
? <Suggestions suggestions={suggestions} onSelect={onSelectSuggestion} i={i} setI={setI} />
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
<span className="text-block suggest">{singleSuggestion}</span>
|
|
||||||
</span>
|
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Suggestions({ suggestions, onSelect, i, setI }) {
|
function Suggestions({ suggestions, onSelect, i, setI }) {
|
||||||
|
const onMouseEnter = j => () => {
|
||||||
|
setI(j);
|
||||||
|
};
|
||||||
|
const onMouseDown = j => () => {
|
||||||
|
setI(j);
|
||||||
|
onSelect(suggestions[i]);
|
||||||
|
};
|
||||||
|
|
||||||
return (suggestions.length > 0) ?
|
return (suggestions.length > 0) ?
|
||||||
<div className="suggestions">
|
<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>)}
|
{suggestions.map(([name, dynamic], j) =>
|
||||||
|
<div
|
||||||
|
key={`${j}_${name}`}
|
||||||
|
className={i === j ? "selected" : ""}
|
||||||
|
onMouseEnter={onMouseEnter(j)}
|
||||||
|
onMouseDown={onMouseDown(j)}>
|
||||||
|
{name} :: <Type type={getType(dynamic)} />
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
: <></>;
|
: <></>;
|
||||||
}
|
}
|
||||||
9
src/ShowIf.tsx
Normal file
9
src/ShowIf.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// syntactic sugar
|
||||||
|
export function ShowIf({cond, children}) {
|
||||||
|
if (cond) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ export function Type({type}) {
|
||||||
case symbolDictIterator:
|
case symbolDictIterator:
|
||||||
return <BinaryType type={type} cssClass="dictType iteratorType" infix="*⇒" prefix="{" suffix="}"/>;
|
return <BinaryType type={type} cssClass="dictType iteratorType" infix="*⇒" prefix="{" suffix="}"/>;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <div className="type">{getHumanReadableName(symbol)}</div>
|
return <div className="type">{getHumanReadableName(symbol)}</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
src/Value.css
Normal file
9
src/Value.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.value {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
60
src/Value.tsx
Normal file
60
src/Value.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight} from "dope2";
|
||||||
|
|
||||||
|
import "./Value.css";
|
||||||
|
|
||||||
|
export function Value({dynamic}) {
|
||||||
|
const type = getType(dynamic);
|
||||||
|
const inst = getInst(dynamic);
|
||||||
|
if (eqType(type)(Double)) {
|
||||||
|
return <ValueDouble val={inst}/>;
|
||||||
|
}
|
||||||
|
if (eqType(type)(Int)) {
|
||||||
|
return <ValueInt val={inst}/>;
|
||||||
|
}
|
||||||
|
const symbol = getSymbol(type);
|
||||||
|
switch (symbol) {
|
||||||
|
case symbolFunction:
|
||||||
|
return <ValueFunction/>;
|
||||||
|
// return <BinaryType type={type} cssClass="functionType" infix="→" prefix="" suffix=""/>;
|
||||||
|
// case symbolProduct:
|
||||||
|
// return <BinaryType type={type} cssClass="productType" infix="⨯" prefix="" suffix=""/>;
|
||||||
|
case symbolSum:
|
||||||
|
return <ValueSum val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>;
|
||||||
|
case symbolProduct:
|
||||||
|
return <ValueProduct val={inst} leftType={type.params[0](type)} rightType={type.params[1](type)}/>;
|
||||||
|
|
||||||
|
// case symbolDict:
|
||||||
|
// return <BinaryType type={type} cssClass="dictType" infix="⇒" prefix="{" suffix="}"/>;
|
||||||
|
// case symbolSet:
|
||||||
|
// return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
|
||||||
|
case symbolList:
|
||||||
|
return <List val={inst} elemType={type.params[0](type)} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <>don't know how to show value</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ValueDouble({val}) {
|
||||||
|
return <span className="value">{val.toString()}</span>;
|
||||||
|
}
|
||||||
|
function ValueInt({val}) {
|
||||||
|
return <span className="value">{val.toString()}</span>;
|
||||||
|
}
|
||||||
|
function ValueFunction() {
|
||||||
|
return <>𝑓𝑛 </>;
|
||||||
|
}
|
||||||
|
// function Sum({val, elemType}) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
function List({val, elemType}) {
|
||||||
|
return <span className="listType">[{val.map((v, i) => <Value dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||||
|
}
|
||||||
|
function ValueSum({val, leftType, rightType}) {
|
||||||
|
return match(val)
|
||||||
|
(l => <>L <Value dynamic={{i:l, t:leftType}}/></>)
|
||||||
|
(r => <>R <Value dynamic={{i:r, t:rightType}}/></>);
|
||||||
|
}
|
||||||
|
function ValueProduct({val, leftType, rightType}) {
|
||||||
|
return <>(<Value dynamic={{i:getLeft(val), t:leftType}}/>, <Value dynamic={{i:getRight(val), t:rightType}} />)</>;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||||
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings:
|
||||||
|
"wdth" 100;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-within:not(body) {
|
||||||
|
/* outline: 2px solid black; */
|
||||||
|
/* background-color: aqua; */
|
||||||
}
|
}
|
||||||
|
|
@ -33,13 +33,13 @@ export function setRightMostCaretPosition(elem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusNextElement() {
|
export function focusNextElement() {
|
||||||
const editable = Array.from<any>(document.querySelectorAll('[contenteditable]'));
|
const editable = Array.from<any>(document.querySelectorAll('input'));
|
||||||
const index = editable.indexOf(document.activeElement);
|
const index = editable.indexOf(document.activeElement);
|
||||||
editable[index+1]?.focus();
|
editable[index+1]?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusPrevElement() {
|
export function focusPrevElement() {
|
||||||
const editable = Array.from<any>(document.querySelectorAll('[contenteditable]'));
|
const editable = Array.from<any>(document.querySelectorAll('input'));
|
||||||
const index = editable.indexOf(document.activeElement);
|
const index = editable.indexOf(document.activeElement);
|
||||||
const prevElem = editable[index-1]
|
const prevElem = editable[index-1]
|
||||||
if (prevElem) {
|
if (prevElem) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { EditorState } from "../Editor";
|
||||||
|
|
||||||
export interface Dynamic {
|
export interface Dynamic {
|
||||||
i: any;
|
i: any;
|
||||||
t: any;
|
t: any;
|
||||||
|
|
@ -6,5 +8,6 @@ export interface Dynamic {
|
||||||
export interface State2Props<T> {
|
export interface State2Props<T> {
|
||||||
state: T;
|
state: T;
|
||||||
// setState: (callback: (state: T) => T) => void;
|
// setState: (callback: (state: T) => T) => void;
|
||||||
setState: (state: T) => void;
|
// setState: (state: T) => void;
|
||||||
|
setState: (state: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue