change the way text suggestions are rendered + option to disable syntactic sugar
This commit is contained in:
parent
ea8c015eff
commit
2d81e42447
12 changed files with 357 additions and 291 deletions
52
pnpm-lock.yaml
generated
52
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#443a13998dc3eccab26c27bee4fa056cdbc8f994
|
version: git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
|
|
@ -262,8 +262,8 @@ packages:
|
||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.11.2':
|
'@modelcontextprotocol/sdk@1.11.3':
|
||||||
resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==}
|
resolution: {integrity: sha512-rmOWVRUbUJD7iSvJugjUbFZshTAuJ48MXoZ80Osx1GM0K/H1w7rSEvmw8m6vdWxNASgtaHIhAgre4H/E9GJiYQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
|
|
@ -617,8 +617,8 @@ packages:
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
supports-color: '*'
|
supports-color: '*'
|
||||||
|
|
@ -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#443a13998dc3eccab26c27bee4fa056cdbc8f994:
|
dope2@git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381:
|
||||||
resolution: {commit: 443a13998dc3eccab26c27bee4fa056cdbc8f994, repo: https://deemz.org/git/joeri/dope2.git, type: git}
|
resolution: {commit: d75bf9f0f200769a5248ace8e4e2ace04fd60381, 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:
|
||||||
|
|
@ -729,8 +729,8 @@ packages:
|
||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
eventsource-parser@3.0.1:
|
eventsource-parser@3.0.2:
|
||||||
resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==}
|
resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
eventsource@3.0.7:
|
eventsource@3.0.7:
|
||||||
|
|
@ -1369,7 +1369,7 @@ snapshots:
|
||||||
'@eslint/config-array@0.20.0':
|
'@eslint/config-array@0.20.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.6
|
'@eslint/object-schema': 2.1.6
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
@ -1383,7 +1383,7 @@ snapshots:
|
||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
espree: 10.3.0
|
espree: 10.3.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
|
|
@ -1416,7 +1416,7 @@ snapshots:
|
||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.11.2':
|
'@modelcontextprotocol/sdk@1.11.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
|
|
@ -1590,7 +1590,7 @@ snapshots:
|
||||||
'@typescript-eslint/types': 8.32.1
|
'@typescript-eslint/types': 8.32.1
|
||||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.32.1
|
'@typescript-eslint/visitor-keys': 8.32.1
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
eslint: 9.26.0
|
eslint: 9.26.0
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
@ -1605,7 +1605,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3)
|
'@typescript-eslint/utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3)
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
eslint: 9.26.0
|
eslint: 9.26.0
|
||||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
|
|
@ -1618,7 +1618,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.32.1
|
'@typescript-eslint/types': 8.32.1
|
||||||
'@typescript-eslint/visitor-keys': 8.32.1
|
'@typescript-eslint/visitor-keys': 8.32.1
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
|
|
@ -1681,7 +1681,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
http-errors: 2.0.0
|
http-errors: 2.0.0
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
|
|
@ -1754,7 +1754,7 @@ snapshots:
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
debug@4.4.0:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
|
@ -1762,7 +1762,7 @@ snapshots:
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
|
dope2@git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381:
|
||||||
dependencies:
|
dependencies:
|
||||||
functional-red-black-tree: 1.0.1
|
functional-red-black-tree: 1.0.1
|
||||||
|
|
||||||
|
|
@ -1846,13 +1846,13 @@ snapshots:
|
||||||
'@humanfs/node': 0.16.6
|
'@humanfs/node': 0.16.6
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@humanwhocodes/retry': 0.4.3
|
'@humanwhocodes/retry': 0.4.3
|
||||||
'@modelcontextprotocol/sdk': 1.11.2
|
'@modelcontextprotocol/sdk': 1.11.3
|
||||||
'@types/estree': 1.0.7
|
'@types/estree': 1.0.7
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 8.3.0
|
eslint-scope: 8.3.0
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
|
@ -1895,11 +1895,11 @@ snapshots:
|
||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
eventsource-parser@3.0.1: {}
|
eventsource-parser@3.0.2: {}
|
||||||
|
|
||||||
eventsource@3.0.7:
|
eventsource@3.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
eventsource-parser: 3.0.1
|
eventsource-parser: 3.0.2
|
||||||
|
|
||||||
express-rate-limit@7.5.0(express@5.1.0):
|
express-rate-limit@7.5.0(express@5.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -1913,7 +1913,7 @@ snapshots:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cookie-signature: 1.2.2
|
cookie-signature: 1.2.2
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
|
|
@ -1969,7 +1969,7 @@ snapshots:
|
||||||
|
|
||||||
finalhandler@2.1.0:
|
finalhandler@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
|
|
@ -2260,7 +2260,7 @@ snapshots:
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
is-promise: 4.0.0
|
is-promise: 4.0.0
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
|
|
@ -2282,7 +2282,7 @@ snapshots:
|
||||||
|
|
||||||
send@1.2.0:
|
send@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.0
|
debug: 4.4.1
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
|
|
|
||||||
12
src/App.tsx
12
src/App.tsx
|
|
@ -48,6 +48,8 @@ export function App() {
|
||||||
// load from localStorage
|
// load from localStorage
|
||||||
const [appState, setAppState] = useState(loadFromLocalStorage());
|
const [appState, setAppState] = useState(loadFromLocalStorage());
|
||||||
|
|
||||||
|
const [syntacticSugar, setSyntacticSugar] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// persist accross reloads
|
// persist accross reloads
|
||||||
localStorage["appState"] = JSON.stringify(appState);
|
localStorage["appState"] = JSON.stringify(appState);
|
||||||
|
|
@ -146,15 +148,21 @@ export function App() {
|
||||||
<button className="factoryReset" onClick={factoryReset}>
|
<button className="factoryReset" onClick={factoryReset}>
|
||||||
FACTORY RESET
|
FACTORY RESET
|
||||||
</button>
|
</button>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
checked={syntacticSugar}
|
||||||
|
onChange={e => setSyntacticSugar(e.target.checked)}/>
|
||||||
|
syntactic sugar
|
||||||
|
</label>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main onKeyDown={onKeyDown}>
|
<main onKeyDown={onKeyDown}>
|
||||||
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight}}>
|
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight, syntacticSugar}}>
|
||||||
<Editor
|
<Editor
|
||||||
state={appState.history.at(-1)!}
|
state={appState.history.at(-1)!}
|
||||||
setState={pushHistory}
|
setState={pushHistory}
|
||||||
onCancel={() => {}}
|
onCancel={() => {}}
|
||||||
suggestionPriority={() => 0}
|
suggestionPriority={() => 1}
|
||||||
/>
|
/>
|
||||||
</CommandContext>
|
</CommandContext>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { useContext } from "react";
|
import { useContext, useInsertionEffect } from "react";
|
||||||
|
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
import { Value } from "./Value";
|
import { Value } from "./Value";
|
||||||
import { type SetStateFn, type State2Props } from "./Editor";
|
import { type SetStateFn, type State2Props } from "./Editor";
|
||||||
import { DeepError, evalCallBlock, evalEditorBlock, haveValue } from "./eval";
|
import { evalCallBlock, evalEditorBlock } from "./eval";
|
||||||
import { type ResolvedType } from "./eval";
|
import { type ResolvedType } from "./eval";
|
||||||
import "./CallBlock.css";
|
import "./CallBlock.css";
|
||||||
import { EnvContext } from "./EnvContext";
|
import { EnvContext } from "./EnvContext";
|
||||||
import type { SuggestionType } from "./InputBlock";
|
import type { SuggestionType } from "./InputBlock";
|
||||||
import { getType, NotAFunctionError, unify, UnifyError } from "dope2";
|
import { UnifyError } from "dope2";
|
||||||
|
|
||||||
export interface CallBlockState<
|
export interface CallBlockState<
|
||||||
FnState=EditorState,
|
FnState=EditorState,
|
||||||
|
|
@ -47,7 +47,7 @@ export function CallBlock({ state, setState, suggestionPriority }: CallBlockProp
|
||||||
= headlessCallBlock(setState);
|
= headlessCallBlock(setState);
|
||||||
const env = useContext(EnvContext);
|
const env = useContext(EnvContext);
|
||||||
const resolved = evalEditorBlock(state, env);
|
const resolved = evalEditorBlock(state, env);
|
||||||
return <span className={"functionBlock" + ((resolved instanceof DeepError) ? " unifyError" : "")}>
|
return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}>
|
||||||
<FunctionHeader
|
<FunctionHeader
|
||||||
fn={state.fn}
|
fn={state.fn}
|
||||||
setFn={setFn}
|
setFn={setFn}
|
||||||
|
|
@ -56,32 +56,79 @@ export function CallBlock({ state, setState, suggestionPriority }: CallBlockProp
|
||||||
suggestionPriority={suggestionPriority}
|
suggestionPriority={suggestionPriority}
|
||||||
/>
|
/>
|
||||||
<div className="functionParams">
|
<div className="functionParams">
|
||||||
<div className="outputParam">
|
<Output resolved={resolved}>
|
||||||
{/* Sequence of input parameters */}
|
{/* Sequence of input parameters */}
|
||||||
<InputParams
|
<InputParams
|
||||||
fn={state.fn} setFn={setFn}
|
fn={state.fn} setFn={setFn}
|
||||||
input={state.input} setInput={setInput}
|
input={state.input} setInput={setInput}
|
||||||
onInputCancel={onInputCancel}
|
onInputCancel={onInputCancel}
|
||||||
depth={0}
|
depth={0}
|
||||||
errorDepth={resolved instanceof DeepError ? (resolved.depth) : -1}
|
errorDepth={(resolved.kind === "error") ? (resolved.depth) : -1}
|
||||||
suggestionPriority={suggestionPriority}
|
suggestionPriority={suggestionPriority}
|
||||||
/>
|
/>
|
||||||
{/* Output (or Error) */}
|
</Output>
|
||||||
{ resolved instanceof DeepError && resolved.e.toString()
|
</div>
|
||||||
|| resolved && <><Value dynamic={resolved} />
|
</span>;
|
||||||
</>}
|
}
|
||||||
</div>
|
|
||||||
|
export function Output({resolved, children}) {
|
||||||
|
return <div className="outputParam">
|
||||||
|
{children}
|
||||||
|
{ (resolved.kind === "error") && resolved.e.toString()
|
||||||
|
|| (resolved.kind === "value") && <Value dynamic={resolved} />
|
||||||
|
|| "unknown" }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CallBlockNoSugar({ state, setState, suggestionPriority }: CallBlockProps) {
|
||||||
|
const {setFn, setInput, onFnCancel, onInputCancel}
|
||||||
|
= headlessCallBlock(setState);
|
||||||
|
const env = useContext(EnvContext);
|
||||||
|
const resolved = evalEditorBlock(state, env);
|
||||||
|
const isOffending = (resolved.kind === "error") ? (resolved.depth===0) : false;
|
||||||
|
return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}>
|
||||||
|
<FunctionName fn={state.fn} setFn={setFn} onFnCancel={onFnCancel} suggestionPriority={suggestionPriority} input={state.input} />
|
||||||
|
<div className="functionParams">
|
||||||
|
<Output resolved={resolved}>
|
||||||
|
<div className={"inputParam" + (isOffending ? " offending" : "")}>
|
||||||
|
<Editor
|
||||||
|
state={state.input}
|
||||||
|
setState={setInput}
|
||||||
|
onCancel={onInputCancel}
|
||||||
|
suggestionPriority={
|
||||||
|
(inputSuggestion: SuggestionType) => computePriority(
|
||||||
|
evalEditorBlock(state.fn, env), // fn *may* be set
|
||||||
|
inputSuggestion[2], // suggestions will be for input
|
||||||
|
suggestionPriority, // priority function we get from parent block
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Output>
|
||||||
</div>
|
</div>
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) {
|
function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) {
|
||||||
const resolved = evalCallBlock(fn, input);
|
const resolved = evalCallBlock(fn, input);
|
||||||
if ((resolved && !(resolved instanceof DeepError))) {
|
|
||||||
|
const r = outPriority(['literal', '<computed>',
|
||||||
|
// @ts-ignore: // TODO fix this
|
||||||
|
{t: resolved.t}]);
|
||||||
|
|
||||||
|
if (resolved.kind === "value") {
|
||||||
// The computed output becomes an input for the surrounding block, which may also have a priority function defined, for instance our output is the input to another function
|
// The computed output becomes an input for the surrounding block, which may also have a priority function defined, for instance our output is the input to another function
|
||||||
return 1 + outPriority(['literal', '<computed>', resolved]);
|
console.log('r:', r);
|
||||||
|
return 1 + r;
|
||||||
|
}
|
||||||
|
else if (resolved.kind === "unknown") {
|
||||||
|
return 0 + r;
|
||||||
|
}
|
||||||
|
if (resolved.e instanceof UnifyError) {
|
||||||
|
return -1 + r; // parameter doesn't match
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return -2 + r; // even worse: fn is not a function!
|
||||||
}
|
}
|
||||||
return 0; // no fit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
|
function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
|
||||||
|
|
@ -110,23 +157,28 @@ function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// end of recursion - draw function name
|
// end of recursion - draw function name
|
||||||
return <span className="functionName">
|
return <FunctionName fn={fn} setFn={setFn} onFnCancel={onFnCancel} suggestionPriority={suggestionPriority} input={input}/>;
|
||||||
𝑓𝑛
|
|
||||||
<Editor
|
|
||||||
state={fn}
|
|
||||||
setState={setFn}
|
|
||||||
onCancel={onFnCancel}
|
|
||||||
suggestionPriority={
|
|
||||||
(fnSuggestion: SuggestionType) => computePriority(
|
|
||||||
fnSuggestion[2], // suggestions will be for function
|
|
||||||
evalEditorBlock(input, env), // input *may* be set
|
|
||||||
suggestionPriority, // priority function we get from parent block
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FunctionName({fn, setFn, onFnCancel, suggestionPriority, input}) {
|
||||||
|
const env = useContext(EnvContext);
|
||||||
|
return <span className="functionName">
|
||||||
|
𝑓𝑛
|
||||||
|
<Editor
|
||||||
|
state={fn}
|
||||||
|
setState={setFn}
|
||||||
|
onCancel={onFnCancel}
|
||||||
|
suggestionPriority={
|
||||||
|
(fnSuggestion: SuggestionType) => computePriority(
|
||||||
|
fnSuggestion[2], // suggestions will be for function
|
||||||
|
evalEditorBlock(input, env), // input *may* be set
|
||||||
|
suggestionPriority, // priority function we get from parent block
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth, suggestionPriority }) {
|
function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth, suggestionPriority }) {
|
||||||
const env = useContext(EnvContext);
|
const env = useContext(EnvContext);
|
||||||
let nestedParams;
|
let nestedParams;
|
||||||
|
|
@ -158,6 +210,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
|
||||||
const isOffending = depth === errorDepth;
|
const isOffending = depth === errorDepth;
|
||||||
return <div className={"inputParam" + (isOffending ? " offending" : "")}>
|
return <div className={"inputParam" + (isOffending ? " offending" : "")}>
|
||||||
{nestedParams}
|
{nestedParams}
|
||||||
|
{/* Our own input param */}
|
||||||
<Editor
|
<Editor
|
||||||
state={input}
|
state={input}
|
||||||
setState={setInput}
|
setState={setInput}
|
||||||
|
|
@ -170,53 +223,4 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
// {(fn.kind === "call") &&
|
|
||||||
// // if the function we're calling is itself the result of a function call,
|
|
||||||
// // then we render its input parameter nested in our own input parameter box, which is way more readable
|
|
||||||
|
|
||||||
// // recurse:
|
|
||||||
// <NestedParams
|
|
||||||
// fn={fn}
|
|
||||||
// setFn={setFn}
|
|
||||||
// depth={depth}
|
|
||||||
// errorDepth={errorDepth}
|
|
||||||
// suggestionPriority={suggestionPriority}
|
|
||||||
// />
|
|
||||||
// }
|
|
||||||
// {/* Our own input */}
|
|
||||||
// <Editor
|
|
||||||
// state={input}
|
|
||||||
// setState={setInput}
|
|
||||||
// onCancel={onInputCancel}
|
|
||||||
// suggestionPriority={
|
|
||||||
// (inputSuggestion: SuggestionType) => computePriority(
|
|
||||||
// evalEditorBlock(fn, env), // fn *may* be set
|
|
||||||
// inputSuggestion[2], // suggestions will be for input
|
|
||||||
// suggestionPriority, // priority function we get from parent block
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// </div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// function NestedParams({fn, setFn, depth, errorDepth, suggestionPriority}) {
|
|
||||||
// const env = useContext(EnvContext);
|
|
||||||
// const {
|
|
||||||
// setFn : setFnFn,
|
|
||||||
// setInput : setFnInput,
|
|
||||||
// } = headlessCallBlock(setFn);
|
|
||||||
// return <InputParams
|
|
||||||
// fn={fn.fn}
|
|
||||||
// setFn={setFnFn}
|
|
||||||
// input={fn.input}
|
|
||||||
// setInput={setFnInput}
|
|
||||||
// onInputCancel={() => {/*todo*/}}
|
|
||||||
// depth={depth+1}
|
|
||||||
// errorDepth={errorDepth}
|
|
||||||
// suggestionPriority={
|
|
||||||
// (inputSuggestion: SuggestionType) => computePriority(
|
|
||||||
// evalEditorBlock(fn.fn, env), // fn *may* be set
|
|
||||||
// inputSuggestion[2], // suggestions will be for input
|
|
||||||
// suggestionPriority, // priority function we get from parent block
|
|
||||||
// )}
|
|
||||||
// />;
|
|
||||||
// }
|
|
||||||
|
|
@ -4,6 +4,7 @@ interface GlobalActions {
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
redo: () => void;
|
redo: () => void;
|
||||||
doHighlight: {[key:string]: () => void};
|
doHighlight: {[key:string]: () => void};
|
||||||
|
syntacticSugar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommandContext = createContext<GlobalActions|null>(null);
|
export const CommandContext = createContext<GlobalActions|null>(null);
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@
|
||||||
|
|
||||||
.commandInput {
|
.commandInput {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
@ -2,10 +2,10 @@ import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { getSymbol, getType, symbolFunction } from "dope2";
|
import { getSymbol, getType, symbolFunction } from "dope2";
|
||||||
|
|
||||||
import { CallBlock, type CallBlockState } from "./CallBlock";
|
import { CallBlock, CallBlockNoSugar, type CallBlockState } from "./CallBlock";
|
||||||
import { InputBlock, type InputBlockState, type SuggestionType } from "./InputBlock";
|
import { InputBlock, type InputBlockState, type SuggestionType } from "./InputBlock";
|
||||||
import { Type } from "./Type";
|
import { Type } from "./Type";
|
||||||
import { DeepError, evalEditorBlock } from "./eval";
|
import { evalEditorBlock } from "./eval";
|
||||||
import { CommandContext } from "./CommandContext";
|
import { CommandContext } from "./CommandContext";
|
||||||
import "./Editor.css";
|
import "./Editor.css";
|
||||||
import { EnvContext } from "./EnvContext";
|
import { EnvContext } from "./EnvContext";
|
||||||
|
|
@ -148,11 +148,20 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
/>;
|
/>;
|
||||||
case "call":
|
case "call":
|
||||||
return <CallBlock
|
if (globalContext?.syntacticSugar) {
|
||||||
state={state}
|
return <CallBlock
|
||||||
setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
|
state={state}
|
||||||
suggestionPriority={suggestionPriority}
|
setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
|
||||||
/>;
|
suggestionPriority={suggestionPriority}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <CallBlockNoSugar
|
||||||
|
state={state}
|
||||||
|
setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
|
||||||
|
suggestionPriority={suggestionPriority}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
case "let":
|
case "let":
|
||||||
return <LetInBlock
|
return <LetInBlock
|
||||||
state={state}
|
state={state}
|
||||||
|
|
@ -165,20 +174,16 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
|
||||||
const resolved = evalEditorBlock(state, env);
|
const resolved = evalEditorBlock(state, env);
|
||||||
return <>
|
return <>
|
||||||
{renderBlock()}
|
{renderBlock()}
|
||||||
{
|
<div className="typeSignature">
|
||||||
(resolved && !(resolved instanceof DeepError))
|
:: <Type type={getType(resolved)} />
|
||||||
? <div className="typeSignature">
|
</div>
|
||||||
:: <Type type={getType(resolved)} />
|
|
||||||
</div>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
<input
|
<input
|
||||||
ref={commandInputRef}
|
ref={commandInputRef}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="editable commandInput"
|
className="editable commandInput"
|
||||||
placeholder={`<command>`}
|
placeholder={`<command>`}
|
||||||
onKeyDown={onCommand}
|
onKeyDown={onCommand}
|
||||||
value={""}
|
value={""}
|
||||||
onChange={() => {}} />
|
onChange={() => {}} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,52 @@
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@500&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@500&display=swap');
|
||||||
|
|
||||||
.suggest {
|
.inputBlock {
|
||||||
margin-left: -3.5px;
|
position: relative;
|
||||||
margin-right: 5px;
|
}
|
||||||
color: #aaa;
|
.editable {
|
||||||
min-width: 30px;
|
position: relative;
|
||||||
|
outline: 0px solid transparent;
|
||||||
|
display: inline-block;
|
||||||
|
border: 0;
|
||||||
font-size: 13pt;
|
font-size: 13pt;
|
||||||
font-family: "Inconsolata", monospace;
|
font-family: "Inconsolata", monospace;
|
||||||
font-optical-sizing: auto;
|
font-optical-sizing: auto;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-variation-settings: "wdth" 100;
|
font-variation-settings: "wdth" 100;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.suggestions {
|
.suggest {
|
||||||
display: none;
|
left: 0;
|
||||||
color: black;
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.suggestionsPlaceholder {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
.suggestions {
|
.suggestions {
|
||||||
display: block;
|
display: block;
|
||||||
|
color: black;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: 7px;
|
|
||||||
margin-left: 4px;
|
|
||||||
border: solid 1px dodgerblue;
|
border: solid 1px dodgerblue;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-height: calc(100vh - 64px);
|
max-height: calc(100vh - 64px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
.selected {
|
.selected {
|
||||||
background-color: dodgerblue;
|
background-color: dodgerblue;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.editable {
|
|
||||||
outline: 0px solid transparent;
|
|
||||||
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;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.border-around-input {
|
.border-around-input {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { memo, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Double, getType, Int, newDynamic, prettyT, trie } from "dope2";
|
import { getType, prettyT, trie } from "dope2";
|
||||||
|
|
||||||
import { EnvContext } from "./EnvContext";
|
import { EnvContext } from "./EnvContext";
|
||||||
import type { Dynamic } from "./eval";
|
import type { Dynamic } from "./eval";
|
||||||
|
|
@ -8,7 +8,7 @@ import "./InputBlock.css";
|
||||||
import { Type } from "./Type";
|
import { Type } from "./Type";
|
||||||
import type { State2Props } from "./Editor";
|
import type { State2Props } from "./Editor";
|
||||||
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
||||||
import { parseDouble, parseInt } from "./util/parse";
|
import { attemptParseLiteral } from "./eval";
|
||||||
|
|
||||||
interface Literal {
|
interface Literal {
|
||||||
kind: "literal";
|
kind: "literal";
|
||||||
|
|
@ -38,13 +38,15 @@ interface InputBlockProps extends State2Props<InputBlockState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeSuggestions = (text, env, suggestionPriority: (s: SuggestionType) => number): PrioritizedSuggestionType[] => {
|
const computeSuggestions = (text, env, suggestionPriority: (s: SuggestionType) => number): PrioritizedSuggestionType[] => {
|
||||||
const asDouble = parseDouble(text);
|
const literals = attemptParseLiteral(text);
|
||||||
const asInt = parseInt(text);
|
|
||||||
|
|
||||||
const ls = [
|
const ls = [
|
||||||
... (asDouble ? [["literal", asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
|
// literals
|
||||||
... (asInt ? [["literal", asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
... literals.map((lit) => ["literal", text, lit]),
|
||||||
... trie.suggest(env.name2dyn)(text)(Infinity).map(([name,type]) => ["name", name, type]),
|
|
||||||
|
// names
|
||||||
|
... trie.suggest(env.name2dyn)(text)(Infinity)
|
||||||
|
.map(([name,type]) => ["name", name, type]),
|
||||||
]
|
]
|
||||||
// return ls;
|
// return ls;
|
||||||
return ls
|
return ls
|
||||||
|
|
@ -60,9 +62,9 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
|
||||||
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
||||||
|
|
||||||
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
||||||
const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text]);
|
const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text, suggestionPriority, env]);
|
||||||
|
|
||||||
useEffect(() => autoInputWidth(inputRef, text), [inputRef, text]);
|
useEffect(() => autoInputWidth(inputRef, text+singleSuggestion), [inputRef, text, singleSuggestion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focus) {
|
if (focus) {
|
||||||
|
|
@ -81,7 +83,6 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTextChange = newText => {
|
const onTextChange = newText => {
|
||||||
const found = trie.get(env.name2dyn)(newText);
|
|
||||||
setState(state => ({...state, text: newText}));
|
setState(state => ({...state, text: newText}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,51 +163,67 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <span>
|
return <span className="inputBlock">
|
||||||
<span className="">
|
{/* Dropdown suggestions */}
|
||||||
{/* Dropdown suggestions */}
|
{haveFocus &&
|
||||||
{haveFocus &&
|
<span className="suggestionsPlaceholder">
|
||||||
<span style={{display:'inline-block'}}>
|
|
||||||
<Suggestions
|
<Suggestions
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
onSelect={onSelectSuggestion}
|
onSelect={onSelectSuggestion}
|
||||||
i={i} setI={setI} />
|
i={i} setI={setI} />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{/* Input box */}
|
{/* Single 'grey' suggestion */}
|
||||||
<input ref={inputRef}
|
<span className="editable suggest">{text}{singleSuggestion}</span>
|
||||||
placeholder="<name or literal>"
|
{/* Input box */}
|
||||||
className="editable"
|
<input ref={inputRef}
|
||||||
value={text}
|
placeholder="<name or literal>"
|
||||||
onInput={onInput}
|
className="editable"
|
||||||
onKeyDown={onKeyDown}
|
value={text}
|
||||||
onFocus={() => setHaveFocus(true)}
|
onInput={onInput}
|
||||||
onBlur={() => setHaveFocus(false)}
|
onKeyDown={onKeyDown}
|
||||||
spellCheck={false}/>
|
onFocus={() => setHaveFocus(true)}
|
||||||
{/* Single 'grey' suggestion */}
|
onBlur={() => setHaveFocus(false)}
|
||||||
<span className="text-block suggest">{singleSuggestion}</span>
|
spellCheck={false}/>
|
||||||
</span>
|
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Suggestions({ suggestions, onSelect, i, setI }) {
|
function Suggestions({ suggestions, onSelect, i, setI }) {
|
||||||
|
return <>{(suggestions.length > 0) &&
|
||||||
|
<div className={"suggestions"}>
|
||||||
|
{suggestions.map((suggestion, j) =>
|
||||||
|
<SuggestionMemo key={j}
|
||||||
|
{...{setI, j,
|
||||||
|
onSelect,
|
||||||
|
highlighted: i===j,
|
||||||
|
suggestion}}/>)}
|
||||||
|
</div>
|
||||||
|
}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuggestionProps {
|
||||||
|
setI: any;
|
||||||
|
j: number;
|
||||||
|
onSelect: any;
|
||||||
|
highlighted: boolean;
|
||||||
|
suggestion: PrioritizedSuggestionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Suggestion({ setI, j, onSelect, highlighted, suggestion: [priority, kind, name, dynamic] }: SuggestionProps) {
|
||||||
const onMouseEnter = j => () => {
|
const onMouseEnter = j => () => {
|
||||||
setI(j);
|
setI(j);
|
||||||
};
|
};
|
||||||
const onMouseDown = j => () => {
|
const onMouseDown = j => () => {
|
||||||
setI(j);
|
setI(j);
|
||||||
onSelect(suggestions[i]);
|
onSelect([priority, kind, name, dynamic]);
|
||||||
};
|
};
|
||||||
return <>{(suggestions.length > 0) &&
|
return <div
|
||||||
<div className={"suggestions"}>
|
key={`${j}_${name}`}
|
||||||
{suggestions.map(([priority, kind, name, dynamic], j) =>
|
className={(highlighted ? " selected" : "")}
|
||||||
<div
|
onMouseEnter={onMouseEnter(j)}
|
||||||
key={`${j}_${name}`}
|
onMouseDown={onMouseDown(j)}>
|
||||||
className={(i === j ? " selected" : "")}
|
({priority}) ({kind}) {name} :: <Type type={getType(dynamic)} />
|
||||||
onMouseEnter={onMouseEnter(j)}
|
</div>
|
||||||
onMouseDown={onMouseDown(j)}>
|
|
||||||
({priority}) ({kind}) {name} :: <Type type={getType(dynamic)} />
|
|
||||||
</div>)}
|
|
||||||
</div>
|
|
||||||
}</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SuggestionMemo = memo<SuggestionProps>(Suggestion);
|
||||||
|
|
@ -4,7 +4,7 @@ import { growEnv } from "dope2";
|
||||||
|
|
||||||
import { Editor, type EditorState } from "./Editor";
|
import { Editor, type EditorState } from "./Editor";
|
||||||
import { EnvContext } from "./EnvContext";
|
import { EnvContext } from "./EnvContext";
|
||||||
import { DeepError, evalEditorBlock, type ResolvedType } from "./eval";
|
import { evalEditorBlock, type ResolvedType } from "./eval";
|
||||||
import { type State2Props } from "./Editor";
|
import { type State2Props } from "./Editor";
|
||||||
import { autoInputWidth } from "./util/dom_trickery";
|
import { autoInputWidth } from "./util/dom_trickery";
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ interface LetInBlockProps extends State2Props<LetInBlockState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeInnerEnv(env, name: string, value: ResolvedType) {
|
export function makeInnerEnv(env, name: string, value: ResolvedType) {
|
||||||
if (value && !(value instanceof DeepError)) {
|
if (value.kind === "value") {
|
||||||
return growEnv(env)(name)(value)
|
return growEnv(env)(name)(value)
|
||||||
}
|
}
|
||||||
return env;
|
return env;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set} from "dope2";
|
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set, Unit} from "dope2";
|
||||||
|
|
||||||
import "./Value.css";
|
import "./Value.css";
|
||||||
|
|
||||||
|
|
@ -14,6 +14,9 @@ export function Value({dynamic}) {
|
||||||
if (eqType(type)(Bool)) {
|
if (eqType(type)(Bool)) {
|
||||||
return <ValueBool val={inst}/>;
|
return <ValueBool val={inst}/>;
|
||||||
}
|
}
|
||||||
|
if (eqType(type)(Unit)) {
|
||||||
|
return <ValueUnit/>;
|
||||||
|
}
|
||||||
|
|
||||||
const symbol = getSymbol(type);
|
const symbol = getSymbol(type);
|
||||||
switch (symbol) {
|
switch (symbol) {
|
||||||
|
|
@ -47,9 +50,6 @@ function ValueFunction() {
|
||||||
function ValueBool({val}) {
|
function ValueBool({val}) {
|
||||||
return <span className="valuePrimitive">{val.toString()}</span>;
|
return <span className="valuePrimitive">{val.toString()}</span>;
|
||||||
}
|
}
|
||||||
// function Sum({val, elemType}) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
function ValueList({val, elemType}) {
|
function ValueList({val, elemType}) {
|
||||||
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
|
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||||
}
|
}
|
||||||
|
|
@ -57,9 +57,10 @@ function ValueSet({val, elemType}) {
|
||||||
return <span className="setType">{'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}{'}'}</span>;
|
return <span className="setType">{'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}{'}'}</span>;
|
||||||
}
|
}
|
||||||
function ValueDict({val, keyType, valueType}) {
|
function ValueDict({val, keyType, valueType}) {
|
||||||
return <span className="dictType">{'{'}{set.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => <span key={i}>
|
return <span className="dictType">{'{'}{dict.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => <span key={i}>
|
||||||
<Value key={i} dynamic={{i:key, t:keyType}}/>
|
<Value dynamic={{i:key, t:keyType}}/>
|
||||||
<Value key={i} dynamic={{i:value, t:valueType}}/>
|
⇒
|
||||||
|
<Value dynamic={{i:value, t:valueType}}/>
|
||||||
</span>)}{'}'}</span>;
|
</span>)}{'}'}</span>;
|
||||||
}
|
}
|
||||||
function ValueSum({val, leftType, rightType}) {
|
function ValueSum({val, leftType, rightType}) {
|
||||||
|
|
@ -70,23 +71,6 @@ function ValueSum({val, leftType, rightType}) {
|
||||||
function ValueProduct({val, leftType, rightType}) {
|
function ValueProduct({val, leftType, rightType}) {
|
||||||
return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>, <Value dynamic={{i:getRight(val), t:rightType}} />)</span>;
|
return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>, <Value dynamic={{i:getRight(val), t:rightType}} />)</span>;
|
||||||
}
|
}
|
||||||
// function ValueDict({val, keyType, valueType}) {
|
function ValueUnit() {
|
||||||
// let i=0;
|
return <>{'()'}</>;
|
||||||
// return <span className="dictType">{'{'}<>{
|
}
|
||||||
// dict.fold
|
|
||||||
// (acc => key => value => {
|
|
||||||
// console.log({acc, key, value});
|
|
||||||
// return acc.concat([<>
|
|
||||||
// <Value key={i++} dynamic={{i: key, t: keyType}}/>
|
|
||||||
// ⇒
|
|
||||||
// <Value key={i++} dynamic={{i: value, t: valueType}}/>
|
|
||||||
// </>]);
|
|
||||||
// })
|
|
||||||
// ([])
|
|
||||||
// (val)
|
|
||||||
// .map(result => {
|
|
||||||
// console.log(result);
|
|
||||||
// return result;
|
|
||||||
// })
|
|
||||||
// }</>{'}'}</span>;
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
150
src/eval.ts
150
src/eval.ts
|
|
@ -1,27 +1,32 @@
|
||||||
import { apply, Double, Int, NotAFunctionError, trie, UnifyError } from "dope2";
|
import { apply, assignFn, Double, getSymbol, Int, makeGeneric, NotAFunctionError, prettyT, symbolFunction, trie, UnifyError } from "dope2";
|
||||||
|
|
||||||
import type { EditorState } from "./Editor";
|
import type { EditorState } from "./Editor";
|
||||||
import type { InputValueType } from "./InputBlock";
|
import type { InputValueType } from "./InputBlock";
|
||||||
import { makeInnerEnv } from "./LetInBlock";
|
import { makeInnerEnv } from "./LetInBlock";
|
||||||
import { parseDouble, parseInt } from "./util/parse";
|
|
||||||
|
|
||||||
export class DeepError {
|
export interface DeepError {
|
||||||
|
kind: "error";
|
||||||
e: Error;
|
e: Error;
|
||||||
depth: number;
|
depth: number;
|
||||||
constructor(e, depth) {
|
t: any;
|
||||||
this.e = e;
|
}
|
||||||
this.depth = depth;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// a dynamically typed value = tuple (instance, type)
|
// a dynamically typed value = tuple (instance, type)
|
||||||
export interface Dynamic {
|
export interface Dynamic {
|
||||||
|
kind: "value",
|
||||||
i: any;
|
i: any;
|
||||||
t: any;
|
t: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Unknown {
|
||||||
|
kind: "unknown";
|
||||||
|
t: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const entirelyUnknown: Unknown = { kind: "unknown", t: makeGeneric(a => a) };
|
||||||
|
|
||||||
// the value of every block is either known (Dynamic), an error, or unknown
|
// the value of every block is either known (Dynamic), an error, or unknown
|
||||||
export type ResolvedType = Dynamic | DeepError | undefined;
|
export type ResolvedType = Dynamic | DeepError | Unknown;
|
||||||
|
|
||||||
export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
|
export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
|
||||||
if (s.kind === "input") {
|
if (s.kind === "input") {
|
||||||
|
|
@ -39,8 +44,9 @@ export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
|
||||||
}
|
}
|
||||||
if (s.kind === "lambda") {
|
if (s.kind === "lambda") {
|
||||||
const expr = evalEditorBlock(s.expr, env);
|
const expr = evalEditorBlock(s.expr, env);
|
||||||
return undefined; // todo
|
// todo
|
||||||
}
|
}
|
||||||
|
return entirelyUnknown; // todo
|
||||||
};
|
};
|
||||||
|
|
||||||
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
|
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
|
||||||
|
|
@ -48,45 +54,115 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv
|
||||||
return parseLiteral(text, value.type);
|
return parseLiteral(text, value.type);
|
||||||
}
|
}
|
||||||
else if (value.kind === "name") {
|
else if (value.kind === "name") {
|
||||||
return trie.get(env.name2dyn)(text);
|
const found = trie.get(env.name2dyn)(text);
|
||||||
|
if (found) {
|
||||||
|
return { kind: "value", ...found };
|
||||||
|
} else {
|
||||||
|
return entirelyUnknown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else { // kind === "text" -> unresolved
|
else { // kind === "text" -> unresolved
|
||||||
return;
|
return entirelyUnknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function evalCallBlock(fn: ResolvedType, input: ResolvedType) {
|
export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedType {
|
||||||
if (haveValue(input) && haveValue(fn)) {
|
if (getSymbol(fn.t) !== symbolFunction) {
|
||||||
try {
|
if (fn.kind === "unknown") {
|
||||||
const outputResolved = apply(input)(fn); // may throw
|
return entirelyUnknown; // don't flash everything red, giving the user a heart attack
|
||||||
return outputResolved; // success
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return new DeepError(e, 0); // eval error
|
|
||||||
}
|
}
|
||||||
|
// worst outcome: we know nothing about the result!
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
e: new NotAFunctionError(`${prettyT(fn.t)} is not a function type!`),
|
||||||
|
t: entirelyUnknown.t,
|
||||||
|
depth: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else if (input instanceof DeepError) {
|
try {
|
||||||
return input; // bubble up the error
|
// fn is a function...
|
||||||
}
|
const outType = assignFn(fn.t, input.t); // may throw
|
||||||
else if (fn instanceof DeepError) {
|
|
||||||
return new DeepError(fn.e, fn.depth+1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseLiteral(text: string, type: string) {
|
if (input.kind === "error") {
|
||||||
// dirty
|
return {
|
||||||
if (type === "Int") {
|
kind: "error",
|
||||||
return { i: parseInt(text), t: Int };
|
e: input.e, // bubble up the error
|
||||||
|
depth: 0,
|
||||||
|
t: outType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (fn.kind === "error") {
|
||||||
|
// also bubble up
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
e: fn.e,
|
||||||
|
depth: fn.depth+1,
|
||||||
|
t: outType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// if the above statement did not throw => types are compatible...
|
||||||
|
if (input.kind === "value" && fn.kind === "value") {
|
||||||
|
const outValue = fn.i(input.i);
|
||||||
|
return { kind: "value", i: outValue, t: outType };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// we don't know the value, but we do know the type:
|
||||||
|
return { kind: "unknown", t: outType };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (type === "Double") {
|
catch (e) {
|
||||||
return { i: parseDouble(text), t: Double };
|
if ((e instanceof UnifyError)) {
|
||||||
|
// even though fn was incompatible with the given parameter, we can still suppose that our output-type will be that of fn...?
|
||||||
|
const outType = fn.t.params[1](fn.t);
|
||||||
|
return {
|
||||||
|
kind: "error",
|
||||||
|
e,
|
||||||
|
depth: 0,
|
||||||
|
t: outType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function haveValue(resolved: ResolvedType) {
|
export function haveValue(resolved: ResolvedType) {
|
||||||
return resolved && !(resolved instanceof DeepError);
|
// return resolved && !(resolved instanceof DeepError);
|
||||||
|
return resolved.kind === "value";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseLiteral(text: string, type: string): ResolvedType {
|
||||||
|
// dirty
|
||||||
|
if (type === "Int") {
|
||||||
|
return parseAsInt(text);
|
||||||
|
}
|
||||||
|
if (type === "Double") {
|
||||||
|
return parseAsDouble(text);
|
||||||
|
}
|
||||||
|
return entirelyUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAsDouble(text: string): ResolvedType {
|
||||||
|
if (text !== '') {
|
||||||
|
const num = Number(text);
|
||||||
|
if (!Number.isNaN(num)) {
|
||||||
|
return { kind: "value", i: num, t: Double };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entirelyUnknown;
|
||||||
|
}
|
||||||
|
function parseAsInt(text: string): ResolvedType {
|
||||||
|
if (text !== '') {
|
||||||
|
try {
|
||||||
|
return { kind: "value", i: BigInt(text), t: Int }; // may throw
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
return entirelyUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const literalParsers = [parseAsDouble, parseAsInt];
|
||||||
|
|
||||||
|
export function attemptParseLiteral(text: string): Dynamic[] {
|
||||||
|
return literalParsers.map(parseFn => parseFn(text))
|
||||||
|
.filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1 @@
|
||||||
|
|
||||||
// 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) { };
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue