move 'env' from state to context
This commit is contained in:
parent
9ef160aeb7
commit
f09261df93
13 changed files with 178 additions and 148 deletions
12
src/App.css
12
src/App.css
|
|
@ -42,3 +42,15 @@ footer {
|
|||
footer a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.command {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
padding: 0 6px 0 6px;
|
||||
margin: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
.command.highlighted {
|
||||
background-color: dodgerblue;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import './App.css'
|
|||
import { Editor, type EditorState } from './Editor'
|
||||
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
|
||||
import { CommandContext } from './CommandContext';
|
||||
import { EnvContext } from './EnvContext';
|
||||
|
||||
export function App() {
|
||||
const [history, setHistory] = useState([initialEditorState]);
|
||||
|
|
@ -52,7 +53,7 @@ export function App() {
|
|||
const commands = [
|
||||
["call" , "[c] call" ],
|
||||
["eval" , "[u] [Tab] [Enter] eval"],
|
||||
["transform", "[t] transform" ],
|
||||
["transform", "[t] [.] transform" ],
|
||||
["let" , "[=] let ... in ..." ],
|
||||
];
|
||||
|
||||
|
|
@ -80,15 +81,14 @@ export function App() {
|
|||
}
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<CommandContext value={doHighlight}>
|
||||
<main onKeyDown={onKeyDown}>
|
||||
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight}}>
|
||||
<Editor
|
||||
state={history.at(-1)!}
|
||||
setState={pushHistory}
|
||||
onResolve={() => {}}
|
||||
onCancel={() => {}}
|
||||
filter={() => true}
|
||||
focus={true}
|
||||
/>
|
||||
</CommandContext>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export interface CallBlockState<
|
|||
InputState=EditorState,
|
||||
> {
|
||||
kind: "call";
|
||||
env: any;
|
||||
fn: FnState;
|
||||
input: InputState;
|
||||
resolved: undefined | Dynamic;
|
||||
|
|
@ -28,7 +27,7 @@ interface CallBlockProps<
|
|||
}
|
||||
|
||||
function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
|
||||
const [unifyError, setUnifyError] = useState<UnifyError | undefined>(undefined);
|
||||
const [unifyError, setUnifyError] = useState<typeof UnifyError | undefined>(undefined);
|
||||
const {fn, input } = state;
|
||||
const setFn = (fn: EditorState) => {
|
||||
setState({...state, fn});
|
||||
|
|
@ -52,7 +51,7 @@ function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
|
|||
if (!(e instanceof UnifyError)) {
|
||||
throw e;
|
||||
}
|
||||
setUnifyError(e);
|
||||
setUnifyError(e as typeof UnifyError);
|
||||
onResolve({
|
||||
...state, resolved: undefined
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
export const CommandContext = createContext<{[key:string]: () => void}>({});
|
||||
interface GlobalActions {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
doHighlight: {[key:string]: () => void};
|
||||
}
|
||||
|
||||
export const CommandContext = createContext<GlobalActions|null>(null);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.command {
|
||||
.commandInput {
|
||||
width: 160px;
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ interface EditorProps extends State2Props<EditorState> {
|
|||
}
|
||||
|
||||
function getCommands(type) {
|
||||
const commands = ['u', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '='];
|
||||
const commands = ['u', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '=', '.'];
|
||||
if (getSymbol(type) === symbolFunction) {
|
||||
commands.push('c');
|
||||
}
|
||||
|
|
@ -34,9 +34,9 @@ function getCommands(type) {
|
|||
}
|
||||
function getShortCommands(type) {
|
||||
if (getSymbol(type) === symbolFunction) {
|
||||
return 'c|Tab|t';
|
||||
return 'c|Tab|.';
|
||||
}
|
||||
return 'Tab|t';
|
||||
return 'Tab|.';
|
||||
}
|
||||
|
||||
function removeFocus(state: EditorState): EditorState {
|
||||
|
|
@ -72,7 +72,7 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
|
|||
}
|
||||
}
|
||||
|
||||
const doHighlight = useContext(CommandContext);
|
||||
const globalContext = useContext(CommandContext);
|
||||
const onCommand = (e: React.KeyboardEvent) => {
|
||||
const type = getType(state.resolved);
|
||||
const commands = getCommands(type);
|
||||
|
|
@ -84,7 +84,7 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
|
|||
// u -> pass Up
|
||||
if (e.key === "u" || e.key === "Enter" || e.key === "Tab" && !e.shiftKey) {
|
||||
onResolve(state);
|
||||
doHighlight.eval();
|
||||
globalContext?.doHighlight.eval();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Tab" && e.shiftKey) {
|
||||
|
|
@ -96,26 +96,24 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
|
|||
// we become CallBlock
|
||||
setState({
|
||||
kind: "call",
|
||||
env: state.env,
|
||||
fn: removeFocus(state),
|
||||
input: initialEditorState,
|
||||
resolved: undefined,
|
||||
});
|
||||
doHighlight.call();
|
||||
globalContext?.doHighlight.call();
|
||||
// focusNextElement();
|
||||
return;
|
||||
}
|
||||
// t -> Transform
|
||||
if (e.key === "t") {
|
||||
if (e.key === "t" || e.key === ".") {
|
||||
// we become CallBlock
|
||||
setState({
|
||||
kind: "call",
|
||||
env: state.env,
|
||||
fn: initialEditorState,
|
||||
input: removeFocus(state),
|
||||
resolved: undefined,
|
||||
});
|
||||
doHighlight.transform();
|
||||
globalContext?.doHighlight.transform();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Backspace" || e.key === "ArrowLeft") {
|
||||
|
|
@ -132,13 +130,12 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
|
|||
// we become LetInBlock
|
||||
setState({
|
||||
kind: "let",
|
||||
env: state.env,
|
||||
inner: initialEditorState,
|
||||
inner: removeFocus(initialEditorState),
|
||||
name: "",
|
||||
value: state,
|
||||
value: removeFocus(state),
|
||||
resolved: undefined,
|
||||
});
|
||||
doHighlight.let();
|
||||
globalContext?.doHighlight.let();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -179,7 +176,7 @@ export function Editor({state, setState, onResolve, onCancel, filter}: EditorPro
|
|||
? <input
|
||||
ref={commandInputRef}
|
||||
spellCheck={false}
|
||||
className="editable command"
|
||||
className="editable commandInput"
|
||||
placeholder={`<command: ${getShortCommands(getType(state.resolved))}>`}
|
||||
onKeyDown={onCommand}
|
||||
value={""}
|
||||
|
|
|
|||
9
src/EnvContext.ts
Normal file
9
src/EnvContext.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { createContext } from "react";
|
||||
import { getDefaultTypeParser, module2Env, ModuleStd } from "dope2";
|
||||
|
||||
const mkType = getDefaultTypeParser();
|
||||
export const extendedEnv = module2Env(ModuleStd.concat([
|
||||
["functionWith3Params", { i: i => j => k => i + j + k, t: mkType("Int->Int->Int->Int") }],
|
||||
]));
|
||||
|
||||
export const EnvContext = createContext(extendedEnv);
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import { Double, getType, Int, newDynamic, trie } from "dope2";
|
||||
import { focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
||||
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
|
||||
import { parseDouble, parseInt } from "./util/parse";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Type } from "./Type";
|
||||
|
||||
import "./InputBlock.css";
|
||||
import type { Dynamic, State2Props } from "./util/extra";
|
||||
import { EnvContext } from "./EnvContext";
|
||||
|
||||
export interface InputBlockState {
|
||||
kind: "input";
|
||||
env: any;
|
||||
text: string;
|
||||
resolved: undefined | Dynamic;
|
||||
focus: boolean
|
||||
|
|
@ -39,7 +39,8 @@ const computeSuggestions = (text, env, filter) => {
|
|||
}
|
||||
|
||||
export function InputBlock({ state, setState, filter, onResolve, onCancel }: InputBlockProps) {
|
||||
const {env, text, resolved, focus} = state;
|
||||
const {text, resolved, focus} = state;
|
||||
const env = useContext(EnvContext);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [i, setI] = useState(0); // selected suggestion idx
|
||||
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
|
||||
|
|
@ -51,12 +52,7 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
|
|||
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
|
||||
const suggestions = useMemo(() => computeSuggestions(text, env, filter), [text]);
|
||||
|
||||
useEffect(() => {
|
||||
setI(0); // reset
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
||||
}
|
||||
}, [text]);
|
||||
useEffect(() => autoInputWidth(inputRef, text), [inputRef, text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focus) {
|
||||
|
|
@ -67,7 +63,6 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
|
|||
const onSelectSuggestion = ([name, dynamic]) => {
|
||||
onResolve({
|
||||
kind: "input",
|
||||
env,
|
||||
text: name,
|
||||
resolved: dynamic,
|
||||
focus: false,
|
||||
|
|
@ -80,7 +75,6 @@ export function InputBlock({ state, setState, filter, onResolve, onCancel }: Inp
|
|||
// un-resolve
|
||||
onResolve({
|
||||
kind: "input",
|
||||
env,
|
||||
text: e.target.value,
|
||||
resolved: undefined,
|
||||
focus: true,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { Editor, type EditorState } from "./Editor";
|
||||
import { EnvContext } from "./EnvContext";
|
||||
import type { Dynamic, State2Props } from "./util/extra";
|
||||
|
||||
import { growEnv } from "dope2";
|
||||
import { autoInputWidth } from "./util/dom_trickery";
|
||||
|
||||
export interface LetInBlockState {
|
||||
kind: "let";
|
||||
env: any;
|
||||
name: string;
|
||||
value: EditorState;
|
||||
inner: EditorState;
|
||||
|
|
@ -17,13 +19,26 @@ interface LetInBlockProps extends State2Props<LetInBlockState> {
|
|||
|
||||
|
||||
export function LetInBlock({state, setState, onResolve}: LetInBlockProps) {
|
||||
const {env, name, value, inner, resolved} = state;
|
||||
const {name, value, inner} = state;
|
||||
const env = useContext(EnvContext);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState({...state, name: e.target.value});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
nameRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => autoInputWidth(nameRef, name), [nameRef, name]);
|
||||
|
||||
const innerEnv = (name !== '') && value.resolved
|
||||
&& growEnv(env)(name)(value.resolved) || env;
|
||||
return <span className="letIn">
|
||||
<div className="decl">
|
||||
let <input
|
||||
ref={nameRef}
|
||||
className='editable'
|
||||
value={name}
|
||||
placeholder="<variable name>"
|
||||
|
|
@ -36,16 +51,18 @@ export function LetInBlock({state, setState, onResolve}: LetInBlockProps) {
|
|||
onCancel={() => {} }
|
||||
setState={(state: EditorState) => {} }
|
||||
/>
|
||||
in
|
||||
in
|
||||
</div>
|
||||
<div className="inner">
|
||||
<Editor
|
||||
state={inner}
|
||||
filter={() => true}
|
||||
onResolve={(state: EditorState) => {} }
|
||||
onCancel={() => {} }
|
||||
setState={(state: EditorState) => {} }
|
||||
/>
|
||||
<EnvContext value={innerEnv}>
|
||||
<Editor
|
||||
state={inner}
|
||||
setState={innerState => setState({...state, inner})}
|
||||
filter={() => true}
|
||||
onResolve={onResolve}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
</EnvContext>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,30 @@
|
|||
import { getDefaultTypeParser, module2Env, ModuleStd, trie, Int, apply } from "dope2";
|
||||
import { apply, Int, trie } from "dope2";
|
||||
import type { EditorState } from "./Editor";
|
||||
|
||||
const mkType = getDefaultTypeParser();
|
||||
export const extendedEnv = module2Env(ModuleStd.concat([
|
||||
["functionWith3Params", { i: i => j => k => i + j + k, t: mkType("Int->Int->Int->Int") }],
|
||||
]));
|
||||
import { extendedEnv } from "./EnvContext";
|
||||
|
||||
export const initialEditorState: EditorState = {
|
||||
kind: "input",
|
||||
env: extendedEnv,
|
||||
text: "",
|
||||
resolved: undefined,
|
||||
focus: true,
|
||||
};
|
||||
|
||||
const listPush = trie.get(initialEditorState.env.name2dyn)("list.push");
|
||||
const listEmptyList = trie.get(initialEditorState.env.name2dyn)("list.emptyList");
|
||||
const listPush = trie.get(extendedEnv.name2dyn)("list.push");
|
||||
const listEmptyList = trie.get(extendedEnv.name2dyn)("list.emptyList");
|
||||
const fourtyTwo = { i: 42n, t: Int };
|
||||
|
||||
export const nonEmptyEditorState: EditorState = {
|
||||
kind: "call",
|
||||
env: initialEditorState.env,
|
||||
fn: {
|
||||
kind: "call",
|
||||
env: initialEditorState.env,
|
||||
fn: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "list.push",
|
||||
resolved: listPush,
|
||||
focus: false,
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "list.emptyList",
|
||||
resolved: listEmptyList,
|
||||
focus: false,
|
||||
|
|
@ -43,7 +34,6 @@ export const nonEmptyEditorState: EditorState = {
|
|||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "42",
|
||||
resolved: fourtyTwo,
|
||||
focus: false,
|
||||
|
|
@ -52,29 +42,24 @@ export const nonEmptyEditorState: EditorState = {
|
|||
resolved: undefined,
|
||||
};
|
||||
|
||||
const functionWith3Params = trie.get(initialEditorState.env.name2dyn)("functionWith3Params");
|
||||
const functionWith3Params = trie.get(extendedEnv.name2dyn)("functionWith3Params");
|
||||
const fourtyThree = { i: 43n, t: Int };
|
||||
const fourtyFour = { i: 44n, t: Int };
|
||||
|
||||
export const tripleFunctionCallEditorState: EditorState = {
|
||||
kind: "call",
|
||||
env: initialEditorState.env,
|
||||
fn: {
|
||||
kind: "call",
|
||||
env: initialEditorState.env,
|
||||
fn: {
|
||||
kind: "call",
|
||||
env: initialEditorState.env,
|
||||
fn: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "functionWith3Params",
|
||||
resolved: functionWith3Params,
|
||||
focus: false,
|
||||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "42",
|
||||
resolved: fourtyTwo,
|
||||
focus: false,
|
||||
|
|
@ -83,7 +68,6 @@ export const tripleFunctionCallEditorState: EditorState = {
|
|||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "43",
|
||||
resolved: fourtyThree,
|
||||
focus: false,
|
||||
|
|
@ -92,7 +76,6 @@ export const tripleFunctionCallEditorState: EditorState = {
|
|||
},
|
||||
input: {
|
||||
kind: "input",
|
||||
env: initialEditorState.env,
|
||||
text: "44",
|
||||
resolved: fourtyFour,
|
||||
focus: false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Ref } from "react";
|
||||
|
||||
// If there is a caret anywhere in the document (user entering text), returns the position of the caret in the focused element
|
||||
export function getCaretPosition(): number | undefined {
|
||||
|
|
@ -46,4 +47,10 @@ export function focusPrevElement() {
|
|||
prevElem.focus();
|
||||
setRightMostCaretPosition(prevElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const autoInputWidth = (inputRef: React.RefObject<HTMLInputElement| null>, text) => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.width = `${text.length === 0 ? 140 : (text.length*8.7)}px`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue