214 lines
No EOL
5.6 KiB
TypeScript
214 lines
No EOL
5.6 KiB
TypeScript
import { memo, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { trie } from "dope2";
|
|
|
|
import { CallContext } from "../../context/CallContext";
|
|
import { GlobalContext } from "../../context/GlobalContext";
|
|
import { inferTypeInput, type StaticEnvironment, type Type, type TypeInfoInput } from "../../eval/infer_type";
|
|
import { getActions } from "../app/actions";
|
|
import { Input } from "../other/Input";
|
|
import { Type as TypeBlock, TypeInfoBlock } from "../other/Type";
|
|
import type { ExprBlockState, State2Props } from "./ExprBlock";
|
|
|
|
import "./InputBlock.css";
|
|
import { evalExpr } from "../../eval/eval";
|
|
|
|
interface Literal {
|
|
kind: "literal";
|
|
type: string; // todo: store (and serialize) real type
|
|
};
|
|
interface Name {
|
|
kind: "name";
|
|
}
|
|
interface Gibberish {
|
|
kind: "gibberish";
|
|
}
|
|
export type InputValueType = Literal | Name | Gibberish;
|
|
|
|
export interface InputBlockState {
|
|
kind: "input";
|
|
text: string;
|
|
value: InputValueType;
|
|
focus?: boolean;
|
|
}
|
|
|
|
export type PrioritizedSuggestionType = [number, Type, InputBlockState];
|
|
|
|
export interface InputBlockProps extends State2Props<InputBlockState,ExprBlockState> {
|
|
onCancel: () => void;
|
|
typeInfo: TypeInfoInput;
|
|
}
|
|
|
|
const attemptLiterals = [
|
|
["Double", text => (text !== '') && !Number.isNaN(Number(text))],
|
|
["Int" , text => /^-?[0-9]+$/.test(text)]
|
|
] as [string, (text: string) => boolean][];
|
|
|
|
const computeSuggestions = (
|
|
text: string,
|
|
env: StaticEnvironment,
|
|
score: InputBlockProps['score'],
|
|
): PrioritizedSuggestionType[] => {
|
|
const startTime = performance.now();
|
|
|
|
const ls = [
|
|
...attemptLiterals
|
|
.filter(([_, test]) => test(text))
|
|
.map(([typename]) => ({
|
|
kind: "input",
|
|
text,
|
|
value: {
|
|
kind: "literal",
|
|
type: typename,
|
|
},
|
|
})),
|
|
...trie.suggest(env.names)(text)(Infinity)
|
|
.map(([name]) => ({
|
|
kind: "input",
|
|
text: name,
|
|
value: {
|
|
kind: "name",
|
|
},
|
|
})),
|
|
];
|
|
const midTime = performance.now();
|
|
|
|
// return []; // <-- uncomment to disable suggestions (useful for debugging)
|
|
const result = ls.map((state) => [score(state), inferTypeInput(state, env).type, state] as PrioritizedSuggestionType)
|
|
.sort(([a],[b]) => b-a);
|
|
|
|
const endTime = performance.now();
|
|
|
|
console.log(`computing suggestions took ${midTime-startTime} + ${endTime - midTime}`);
|
|
|
|
return result;
|
|
}
|
|
|
|
export function InputBlock({ state, setState, score, onCancel, typeInfo, evalResult }: InputBlockProps) {
|
|
const {text, focus} = state;
|
|
const globalContext = useContext(GlobalContext);
|
|
const env = typeInfo.env;
|
|
const callContext = useContext(CallContext);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [i, setI] = useState(0); // selected suggestion idx
|
|
|
|
const singleSuggestion = trie.growPrefix(env.names)(text);
|
|
const suggestions = useMemo(() => computeSuggestions(text, env, score), [text, score, env]);
|
|
|
|
useEffect(() => {
|
|
if (focus) {
|
|
inputRef.current?.focus();
|
|
}
|
|
}, [focus]);
|
|
|
|
useEffect(() => {
|
|
if (suggestions.length >= i) {
|
|
setI(0);
|
|
}
|
|
}, [suggestions.length]);
|
|
|
|
const onTextChange = newText => {
|
|
setState(state => ({...state,
|
|
text: newText,
|
|
value: (trie.get(env.names)(newText) ? {
|
|
kind: "name",
|
|
} : state.value),
|
|
}));
|
|
}
|
|
|
|
const onSelectSuggestion = () => {
|
|
const [_priority, _type, inputState] = suggestions[i];
|
|
setState(_ => inputState);
|
|
};
|
|
|
|
const extraHandlers = {
|
|
ArrowDown: (e) => {
|
|
setI((i + 1) % suggestions.length);
|
|
e.preventDefault();
|
|
},
|
|
ArrowUp: (e) => {
|
|
setI((suggestions.length + i - 1) % suggestions.length);
|
|
e.preventDefault();
|
|
},
|
|
" ": (e) => {
|
|
if (text.length > 0) {
|
|
if (callContext.addParam) {
|
|
callContext.addParam();
|
|
}
|
|
else {
|
|
const actions = getActions(globalContext, setState);
|
|
actions.c();
|
|
}
|
|
}
|
|
e.preventDefault();
|
|
},
|
|
};
|
|
|
|
const err = typeInfo.err || evalResult.err;
|
|
|
|
return <>
|
|
<Input
|
|
placeholder="<name or literal>"
|
|
onCancel={onCancel}
|
|
onEnter={onSelectSuggestion}
|
|
onTextChange={onTextChange}
|
|
text={text}
|
|
suggestion={singleSuggestion}
|
|
extraHandlers={extraHandlers}
|
|
>
|
|
<span className="dropdownContainer">
|
|
<span className="dropdown">
|
|
{(err !== undefined) &&
|
|
(<div className="errorMessage">
|
|
{err.message.trim()}
|
|
</div>)}
|
|
<Suggestions
|
|
suggestions={suggestions}
|
|
onSelect={onSelectSuggestion}
|
|
i={i} setI={setI} />
|
|
</span>
|
|
</span>
|
|
</Input>
|
|
::<TypeInfoBlock typeInfo={typeInfo} />
|
|
</>;
|
|
}
|
|
|
|
function Suggestions({ suggestions, onSelect, i, setI }) {
|
|
return <>{(suggestions.length > 0) &&
|
|
<div className={"suggestionsScrollableBox"}>
|
|
{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, type, {text, value: {kind} }] }: SuggestionProps) {
|
|
const onMouseEnter = j => () => {
|
|
setI(j);
|
|
};
|
|
const onMouseDown = j => () => {
|
|
setI(j);
|
|
onSelect();
|
|
};
|
|
return <div
|
|
key={`${j}_${name}`}
|
|
className={(highlighted ? " selected" : "")}
|
|
onMouseEnter={onMouseEnter(j)}
|
|
onMouseDown={onMouseDown(j)}>
|
|
({priority}) ({kind}) {text} :: <TypeBlock type={type} />
|
|
</div>;
|
|
}
|
|
|
|
const SuggestionMemo = memo<SuggestionProps>(Suggestion); |