change the way text suggestions are rendered + option to disable syntactic sugar

This commit is contained in:
Joeri Exelmans 2025-05-15 22:22:45 +02:00
parent ea8c015eff
commit 2d81e42447
12 changed files with 357 additions and 291 deletions

52
pnpm-lock.yaml generated
View file

@ -10,7 +10,7 @@ importers:
dependencies: dependencies:
dope2: dope2:
specifier: git+https://deemz.org/git/joeri/dope2.git specifier: git+https://deemz.org/git/joeri/dope2.git
version: git+https://deemz.org/git/joeri/dope2.git#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

View file

@ -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>

View file

@ -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}/>;
&nbsp;&#119891;&#119899;&nbsp;
<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">
&nbsp;&#119891;&#119899;&nbsp;
<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
// )}
// />;
// }

View file

@ -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);

View file

@ -4,4 +4,5 @@
.commandInput { .commandInput {
width: 90px; width: 90px;
margin-left: 10px;
} }

View file

@ -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)) &nbsp;::&nbsp;<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={() => {}} />
</>; </>;
} }

View file

@ -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;

View file

@ -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>)} const SuggestionMemo = memo<SuggestionProps>(Suggestion);
</div>
}</>;
}

View file

@ -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;

View file

@ -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}}/> &rArr;
<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}}/>,&nbsp;<Value dynamic={{i:getRight(val), t:rightType}} />)</span>; return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>,&nbsp;<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}}/>
// &rArr;
// <Value key={i++} dynamic={{i: value, t: valueType}}/>
// </>]);
// })
// ([])
// (val)
// .map(result => {
// console.log(result);
// return result;
// })
// }</>{'}'}</span>;
// }

View file

@ -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[];
}

View file

@ -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) { };
}