don't re-compute values on first render (unnecessary, values are part of state)
This commit is contained in:
parent
174bab79e4
commit
2d0deca127
14 changed files with 274 additions and 115 deletions
|
|
@ -55,3 +55,8 @@ footer a {
|
|||
background-color: dodgerblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.factoryReset {
|
||||
background-color: red;
|
||||
color: black;
|
||||
}
|
||||
66
src/App.tsx
66
src/App.tsx
|
|
@ -1,16 +1,47 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import './App.css'
|
||||
import { Editor, type EditorState } from './Editor'
|
||||
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
|
||||
import { CommandContext } from './CommandContext';
|
||||
import { EnvContext } from './EnvContext';
|
||||
import { deserialize, serialize } from './types';
|
||||
import { extendedEnv } from './EnvContext';
|
||||
import { useEffectBetter } from './util/use_effect_better';
|
||||
|
||||
const commands: [string, string[], string][] = [
|
||||
["call" , ['c' ], "call" ],
|
||||
["eval" , ['e','Tab','Enter'], "eval" ],
|
||||
["transform", ['t', '.' ], "transform" ],
|
||||
["let" , ['l', '=', 'a' ], "let ... in ..."],
|
||||
];
|
||||
|
||||
const examples: [string, EditorState][] = [
|
||||
["empty editor", initialEditorState],
|
||||
["push to list", nonEmptyEditorState],
|
||||
["function w/ 4 params", tripleFunctionCallEditorState]];
|
||||
|
||||
export function App() {
|
||||
// const [history, setHistory] = useState([initialEditorState]);
|
||||
// const [history, setHistory] = useState([nonEmptyEditorState]);
|
||||
const [history, setHistory] = useState([tripleFunctionCallEditorState]);
|
||||
// const [history, setHistory] = useState([tripleFunctionCallEditorState]);
|
||||
// const [future, setFuture] = useState<EditorState[]>([]);
|
||||
|
||||
const [future, setFuture] = useState<EditorState[]>([]);
|
||||
// load from localStorage
|
||||
const [history, setHistory] = useState<EditorState[]>(
|
||||
localStorage["history"]
|
||||
? JSON.parse(localStorage["history"]).map(s => deserialize(s, extendedEnv))
|
||||
: [initialEditorState]
|
||||
);
|
||||
const [future, setFuture] = useState<EditorState[]>(
|
||||
localStorage["future"]
|
||||
? JSON.parse(localStorage["future"]).map(s => deserialize(s, extendedEnv))
|
||||
: []
|
||||
);
|
||||
|
||||
useEffectBetter(() => {
|
||||
// persist accross reloads
|
||||
localStorage["history"] = JSON.stringify(history.map(serialize));
|
||||
localStorage["future"] = JSON.stringify(future.map(serialize));
|
||||
}, [history, future]);
|
||||
|
||||
const pushHistory = (callback: (p: EditorState) => EditorState) => {
|
||||
const newState = callback(history.at(-1)!);
|
||||
|
|
@ -51,12 +82,6 @@ export function App() {
|
|||
window.onkeydown = onKeyDown;
|
||||
}, []);
|
||||
|
||||
const commands: [string, string[], string][] = [
|
||||
["call" , ['c' ], "call" ],
|
||||
["eval" , ['e','Tab','Enter' ], "eval" ],
|
||||
["transform", ['t', '.' ], "transform" ],
|
||||
["let" , ['l', '=', 'a' ], "let ... in ..."],
|
||||
];
|
||||
|
||||
const [highlighted, setHighlighted] = useState(
|
||||
commands.map(() => false));
|
||||
|
|
@ -68,12 +93,16 @@ export function App() {
|
|||
}];
|
||||
}));
|
||||
|
||||
const onSelectExample = (e: React.SyntheticEvent<HTMLSelectElement>) => {
|
||||
// @ts-ignore
|
||||
pushHistory(_ => examples[e.target.value][1]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) [Ctrl+Z]</button>
|
||||
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) [Ctrl+Shift+Z]</button>
|
||||
Commands:
|
||||
<button disabled={history.length===1} onClick={onUndo}>Undo ({history.length-1}) <kbd>Ctrl</kbd>+<kbd>Z</kbd></button>
|
||||
<button disabled={future.length===0} onClick={onRedo}>Redo ({future.length}) <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></button>
|
||||
{
|
||||
commands.map(([_, keys, descr], i) =>
|
||||
<span key={i} className={'command' + (highlighted[i] ? (' highlighted') : '')}>
|
||||
|
|
@ -81,6 +110,17 @@ export function App() {
|
|||
{descr}
|
||||
</span>)
|
||||
}
|
||||
<select onClick={onSelectExample}>
|
||||
{
|
||||
examples.map(([name], i) => {
|
||||
return <option key={i} value={i}>{name}</option>;
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<button className="factoryReset" onClick={() => {
|
||||
setHistory(_ => [initialEditorState]);
|
||||
setFuture(_ => []);
|
||||
}}>FACTORY RESET</button>
|
||||
</header>
|
||||
|
||||
<main onKeyDown={onKeyDown}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
display: inline-block;
|
||||
margin: 4px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.functionName {
|
||||
|
|
|
|||
|
|
@ -2,21 +2,11 @@ import { apply, assignFn, getSymbol, getType, NotAFunctionError, symbolFunction,
|
|||
|
||||
import { Editor, type EditorState } from "./Editor";
|
||||
import { Value } from "./Value";
|
||||
import type { Dynamic, SetStateFn, State2Props } from "./util/extra";
|
||||
import { DeepError, type ResolvedType, type SetStateFn, type State2Props } from "./types";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import "./CallBlock.css";
|
||||
|
||||
export class DeepError {
|
||||
e: Error;
|
||||
depth: number;
|
||||
constructor(e, depth) {
|
||||
this.e = e;
|
||||
this.depth = depth;
|
||||
}
|
||||
};
|
||||
|
||||
type ResolvedType = Dynamic | DeepError | undefined;
|
||||
import { useEffectBetter } from "./util/use_effect_better";
|
||||
|
||||
export interface CallBlockState<
|
||||
FnState=EditorState,
|
||||
|
|
@ -34,6 +24,26 @@ interface CallBlockProps<
|
|||
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
|
||||
}
|
||||
|
||||
export function resolveCallBlock(fn: ResolvedType, input: ResolvedType) {
|
||||
if (have(input) && have(fn)) {
|
||||
try {
|
||||
const outputResolved = apply(input)(fn); // may throw
|
||||
return outputResolved; // success
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
|
||||
throw e;
|
||||
}
|
||||
return new DeepError(e, 0); // eval error
|
||||
}
|
||||
}
|
||||
else if (input instanceof DeepError) {
|
||||
return input; // bubble up the error
|
||||
}
|
||||
else if (fn instanceof DeepError) {
|
||||
return new DeepError(fn.e, fn.depth+1);
|
||||
}
|
||||
}
|
||||
|
||||
function have(resolved: ResolvedType) {
|
||||
return resolved && !(resolved instanceof DeepError);
|
||||
|
|
@ -50,35 +60,13 @@ function headlessCallBlock({state, setState}: CallBlockProps) {
|
|||
const setResolved = (callback: SetStateFn<ResolvedType>) => {
|
||||
setState(state => ({...state, resolved: callback(state.resolved)}));
|
||||
}
|
||||
useEffect(() => {
|
||||
|
||||
useEffectBetter(() => {
|
||||
// Here we do something spooky: we update the state in response to state change...
|
||||
// The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle.
|
||||
if (have(input.resolved) && have(fn.resolved)) {
|
||||
try {
|
||||
const outputResolved = apply(input.resolved)(fn.resolved); // may throw
|
||||
setResolved(() => outputResolved); // success
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
|
||||
throw e;
|
||||
}
|
||||
setResolved(() => new DeepError(e, 0)); // eval error
|
||||
}
|
||||
}
|
||||
else if (input.resolved instanceof DeepError) {
|
||||
setResolved(() => input.resolved); // bubble up the error
|
||||
}
|
||||
else if (fn.resolved instanceof DeepError) {
|
||||
setResolved(() => {
|
||||
// @ts-ignore
|
||||
return new DeepError(fn.resolved.e, fn.resolved.depth+1);
|
||||
}); // bubble up the error
|
||||
}
|
||||
else {
|
||||
// no errors and at least one is undefined:
|
||||
setResolved(() => undefined); // chill out
|
||||
}
|
||||
}, [input.resolved, fn.resolved]);
|
||||
setResolved(() => resolveCallBlock(fn.resolved, input.resolved));
|
||||
}, [fn.resolved, input.resolved]);
|
||||
|
||||
const onFnCancel = () => {
|
||||
setState(state => state.input); // we become our input
|
||||
}
|
||||
|
|
@ -109,7 +97,9 @@ export function CallBlock({ state, setState }: CallBlockProps) {
|
|||
/>
|
||||
{/* Output (or Error) */}
|
||||
{ state.resolved instanceof DeepError && state.resolved.e.toString()
|
||||
|| state.resolved && <><Value dynamic={state.resolved} />☑</>}
|
||||
|| state.resolved && <><Value dynamic={state.resolved} />
|
||||
{/* ☑ */}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
</span>;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { getSymbol, getType, symbolFunction } from "dope2";
|
||||
|
||||
import { useContext, useEffect, useReducer, useRef, useState } from "react";
|
||||
import { CallBlock, DeepError, type CallBlockState } from "./CallBlock";
|
||||
import { CallBlock, type CallBlockState } from "./CallBlock";
|
||||
import { InputBlock, type InputBlockState } from "./InputBlock";
|
||||
import { Type } from "./Type";
|
||||
import { type Dynamic, type State2Props } from "./util/extra";
|
||||
import { DeepError, type Dynamic, type State2Props } from "./types";
|
||||
|
||||
import "./Editor.css";
|
||||
import { LetInBlock, type LetInBlockState } from "./LetInBlock";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|||
import { Type } from "./Type";
|
||||
|
||||
import "./InputBlock.css";
|
||||
import type { Dynamic, State2Props } from "./util/extra";
|
||||
import type { Dynamic, State2Props } from "./types";
|
||||
import { EnvContext } from "./EnvContext";
|
||||
|
||||
export interface InputBlockState {
|
||||
|
|
@ -30,7 +30,7 @@ const computeSuggestions = (text, env, filter) => {
|
|||
... (asInt ? [[asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
|
||||
... trie.suggest(env.name2dyn)(text)(Infinity),
|
||||
]
|
||||
return ls;
|
||||
// return ls;
|
||||
return [
|
||||
...ls.filter(filter), // ones that match filter come first
|
||||
...ls.filter(s => !filter(s)),
|
||||
|
|
@ -158,7 +158,7 @@ export function InputBlock({ state, setState, filter, onCancel }: InputBlockProp
|
|||
spellCheck={false}/>
|
||||
{/* Single 'grey' suggestion */}
|
||||
<span className="text-block suggest">{singleSuggestion}</span>
|
||||
{ resolved && <>☑</>}
|
||||
{/* { resolved && <>☑</>} */}
|
||||
</span>
|
||||
</span>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import type { EditorState } from "./Editor";
|
||||
import type { Dynamic } from "./util/extra";
|
||||
import type { Dynamic, ResolvedType } from "./types";
|
||||
|
||||
|
||||
export interface LambdaBlockState {
|
||||
kind: "lambda";
|
||||
env: any;
|
||||
paramName: string;
|
||||
expr: EditorState;
|
||||
resolved: undefined | Dynamic;
|
||||
resolved: ResolvedType;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,39 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { Editor, type EditorState } from "./Editor";
|
||||
import { EnvContext } from "./EnvContext";
|
||||
import type { Dynamic, State2Props } from "./util/extra";
|
||||
import { DeepError, type ResolvedType, type State2Props } from "./types";
|
||||
import { growEnv } from "dope2";
|
||||
import { autoInputWidth } from "./util/dom_trickery";
|
||||
|
||||
import "./LetInBlock.css";
|
||||
import { useEffectBetter } from "./util/use_effect_better";
|
||||
|
||||
export interface LetInBlockState {
|
||||
kind: "let";
|
||||
name: string;
|
||||
value: EditorState;
|
||||
inner: EditorState;
|
||||
resolved: undefined | Dynamic;
|
||||
resolved: ResolvedType;
|
||||
}
|
||||
|
||||
interface LetInBlockProps extends State2Props<LetInBlockState> {
|
||||
}
|
||||
|
||||
export function makeInnerEnv(env, name: string, value: ResolvedType) {
|
||||
if (value && !(value instanceof DeepError)) {
|
||||
return growEnv(env)(name)(value)
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
export function LetInBlock({state, setState}: LetInBlockProps) {
|
||||
const {name, value, inner} = state;
|
||||
const env = useContext(EnvContext);
|
||||
const innerEnv = makeInnerEnv(env, name, value.resolved);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const setInner = inner => setState(state => ({...state, inner}));
|
||||
const setValue = value => setState(state => ({...state, value}));
|
||||
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
|
||||
const setValue = callback => setState(state => ({...state, value: callback(state.value)}));
|
||||
|
||||
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState(state => ({...state, name: e.target.value}));
|
||||
|
|
@ -35,10 +43,14 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
|
|||
nameRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => autoInputWidth(nameRef, name), [nameRef, name]);
|
||||
useEffectBetter(() => {
|
||||
// bubble up
|
||||
setState(state => ({...state, resolved: inner.resolved}));
|
||||
}, [inner.resolved])
|
||||
|
||||
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
|
||||
|
||||
|
||||
const innerEnv = (name !== '') && value.resolved
|
||||
&& growEnv(env)(name)(value.resolved) || env;
|
||||
return <span className="letIn">
|
||||
<div className="decl">
|
||||
<span className="keyword">let</span>
|
||||
|
|
@ -46,7 +58,7 @@ export function LetInBlock({state, setState}: LetInBlockProps) {
|
|||
ref={nameRef}
|
||||
className='editable'
|
||||
value={name}
|
||||
placeholder="<variable name>"
|
||||
placeholder="<name>"
|
||||
onChange={onChangeName}
|
||||
/>
|
||||
<span className="keyword">=</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool} from "dope2";
|
||||
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set} from "dope2";
|
||||
|
||||
import "./Value.css";
|
||||
|
||||
|
|
@ -19,21 +19,14 @@ export function Value({dynamic}) {
|
|||
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 <ValueDict val={inst} keyType={type.params[0](type)} valueType={type.params[1](type)}/>;
|
||||
|
||||
// return <BinaryType type={type} cssClass="dictType" infix="⇒" prefix="{" suffix="}"/>;
|
||||
// case symbolSet:
|
||||
// return <UnaryType type={type} cssClass="setType" prefix="{" suffix="}" />;
|
||||
case symbolSet:
|
||||
return <ValueSet val={inst} elemType={type.params[0](type)} />;
|
||||
case symbolList:
|
||||
return <ValueList val={inst} elemType={type.params[0](type)} />;
|
||||
|
||||
|
|
@ -60,6 +53,15 @@ function ValueBool({val}) {
|
|||
function ValueList({val, elemType}) {
|
||||
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
|
||||
}
|
||||
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>;
|
||||
}
|
||||
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}>
|
||||
<Value key={i} dynamic={{i:key, t:keyType}}/>
|
||||
<Value key={i} dynamic={{i:value, t:valueType}}/>
|
||||
</span>)}{'}'}</span>;
|
||||
}
|
||||
function ValueSum({val, leftType, rightType}) {
|
||||
return match(val)
|
||||
(l => <span className="sumType">L <Value dynamic={{i:l, t:leftType}}/></span>)
|
||||
|
|
@ -68,23 +70,23 @@ function ValueSum({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>;
|
||||
}
|
||||
function ValueDict({val, keyType, valueType}) {
|
||||
let i=0;
|
||||
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>;
|
||||
}
|
||||
// function ValueDict({val, keyType, valueType}) {
|
||||
// let i=0;
|
||||
// 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>;
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -93,4 +93,3 @@ export const tripleFunctionCallEditorState: EditorState = {
|
|||
},
|
||||
resolved: apply(fourtyFive)(apply(fourtyFour)(apply(fourtyThree)(apply(fourtyTwo)(functionWith4Params)))),
|
||||
};
|
||||
|
||||
|
|
|
|||
108
src/types.ts
Normal file
108
src/types.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Double, getDefaultTypeParser, Int, prettyT, trie } from "dope2";
|
||||
import type { EditorState } from "./Editor";
|
||||
import { resolveCallBlock } from "./CallBlock";
|
||||
import { makeInnerEnv } from "./LetInBlock";
|
||||
import { parseDouble, parseInt } from "./util/parse";
|
||||
|
||||
export interface Dynamic {
|
||||
i: any;
|
||||
t: any;
|
||||
}
|
||||
|
||||
export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
|
||||
|
||||
export interface State2Props<InType,OutType=InType> {
|
||||
state: InType;
|
||||
setState: (callback: SetStateFn<InType,OutType>) => void;
|
||||
}
|
||||
|
||||
export class DeepError {
|
||||
e: Error;
|
||||
depth: number;
|
||||
constructor(e, depth) {
|
||||
this.e = e;
|
||||
this.depth = depth;
|
||||
}
|
||||
};
|
||||
|
||||
export type ResolvedType = Dynamic | DeepError | undefined;
|
||||
|
||||
export const serialize = (s: EditorState) => {
|
||||
if (s.kind === "input") {
|
||||
return {
|
||||
...s,
|
||||
// dirty: we write out the type in case the value is a literal and needs to be parsed
|
||||
type: s.resolved && prettyT(s.resolved.t),
|
||||
resolved: undefined,
|
||||
};
|
||||
}
|
||||
if (s.kind === "call") {
|
||||
return {
|
||||
...s,
|
||||
fn: serialize(s.fn),
|
||||
input: serialize(s.input),
|
||||
resolved: undefined,
|
||||
};
|
||||
}
|
||||
if (s.kind === "let") {
|
||||
return {
|
||||
...s,
|
||||
value: serialize(s.value),
|
||||
inner: serialize(s.inner),
|
||||
resolved: undefined,
|
||||
};
|
||||
}
|
||||
if (s.kind === "lambda") {
|
||||
return {
|
||||
...s,
|
||||
expr: serialize(s.expr),
|
||||
resolved: undefined,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseLiteral(text: string, type: string) {
|
||||
// dirty
|
||||
if (type === "Int") {
|
||||
return {i: parseInt(text), t: Int};
|
||||
}
|
||||
if (type === "Double") {
|
||||
return {i: parseDouble(text), t: Double};
|
||||
}
|
||||
}
|
||||
|
||||
export const deserialize = (s, env) => {
|
||||
if (s.kind === "input") {
|
||||
return {
|
||||
...s,
|
||||
resolved: trie.get(env.name2dyn)(s.text) || parseLiteral(s.text, s.type),
|
||||
};
|
||||
}
|
||||
if (s.kind === "call") {
|
||||
const fn = deserialize(s.fn, env);
|
||||
const input = deserialize(s.input, env);
|
||||
return {
|
||||
...s,
|
||||
fn,
|
||||
input,
|
||||
resolved: resolveCallBlock(fn.resolved, input.resolved),
|
||||
};
|
||||
}
|
||||
if (s.kind === "let") {
|
||||
const value = deserialize(s.value, env);
|
||||
const inner = deserialize(s.inner, makeInnerEnv(env, s.name, value.resolved));
|
||||
return {
|
||||
...s,
|
||||
value,
|
||||
inner,
|
||||
resolved: inner.resolved,
|
||||
};
|
||||
}
|
||||
if (s.kind === "lambda") {
|
||||
return {
|
||||
...s,
|
||||
expr: deserialize(s.expr, env),
|
||||
resolved: undefined,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -49,8 +49,8 @@ export function focusPrevElement() {
|
|||
}
|
||||
}
|
||||
|
||||
export const autoInputWidth = (inputRef: React.RefObject<HTMLInputElement| null>, text) => {
|
||||
export const autoInputWidth = (inputRef: React.RefObject<HTMLInputElement| null>, text, emptyWidth=150) => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.width = `${text.length === 0 ? 150 : (text.length*8.7)}px`;
|
||||
inputRef.current.style.width = `${text.length === 0 ? emptyWidth : (text.length*8.7)}px`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import type { EditorState } from "../Editor";
|
||||
|
||||
export interface Dynamic {
|
||||
i: any;
|
||||
t: any;
|
||||
}
|
||||
|
||||
export type SetStateFn<InType=EditorState,OutType=InType> = (state: InType) => OutType;
|
||||
|
||||
export interface State2Props<InType,OutType=InType> {
|
||||
state: InType;
|
||||
setState: (callback: SetStateFn<InType,OutType>) => void;
|
||||
}
|
||||
16
src/util/use_effect_better.ts
Normal file
16
src/util/use_effect_better.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useEffect, useRef } from "react"
|
||||
|
||||
// like useEffect, but doesn't run on first render
|
||||
|
||||
export const useEffectBetter = (callback, deps) => {
|
||||
// detect development mode, where render function is always called twice:
|
||||
const firstRender = useRef(import.meta.env.MODE === "development" ? 2 : 1);
|
||||
useEffect(() => {
|
||||
if (firstRender.current > 0) {
|
||||
firstRender.current -= 1;
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
}, deps);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue