From 12ae0e17cda0f7462867aad2fc553f1c537041ab Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 30 Mar 2023 19:31:04 +0100 Subject: [PATCH 1/4] refactor: Use more precise type for node data in safe ReactFlow wrapper This ensures that we _can't_ use any data from the node, instead of treating the data as `any`, which is the default type parameter for `RFNode` and `RFEdge`. Signed-off-by: George Thomas --- src/components/TreeReactFlow/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TreeReactFlow/index.tsx b/src/components/TreeReactFlow/index.tsx index 98a2d3bb..4941f06b 100644 --- a/src/components/TreeReactFlow/index.tsx +++ b/src/components/TreeReactFlow/index.tsx @@ -762,8 +762,8 @@ export const TreeReactFlowOne = (p: TreeReactFlowOneProps) => { * check that we register its subtypes correctly with ReactFlow, * and safely act on that type in handlers. */ export const ReactFlowSafe = < - N extends RFNode & { type: string }, - E extends RFEdge & { type: string } + N extends RFNode & { type: string }, + E extends RFEdge & { type: string } >( p: Omit[0], "onNodeClick" | "edgeTypes"> & { nodes: N[]; From 06c0528070eb1e71ed92593da96caa4fdcee3404 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Wed, 31 May 2023 22:01:18 +0100 Subject: [PATCH 2/4] refactor: Use stronger type for edge handles Signed-off-by: George Thomas --- src/components/TreeReactFlow/Types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/TreeReactFlow/Types.ts b/src/components/TreeReactFlow/Types.ts index 9c2bd884..76da1ad4 100644 --- a/src/components/TreeReactFlow/Types.ts +++ b/src/components/TreeReactFlow/Types.ts @@ -7,6 +7,7 @@ import { NodeType, } from "@/primer-api"; import { unzip } from "fp-ts/lib/Array"; +import { Position } from "reactflow"; import { NodeFlavor } from "./Flavor"; /** A generic graph. */ @@ -168,8 +169,8 @@ export type PrimerEdge = { id: string; source: string; target: string; - sourceHandle: string; - targetHandle: string; + sourceHandle: Position; + targetHandle: Position; zIndex: number; } & ({ type: "primer"; data: PrimerEdgeProps } | { type: "primer-def-name" }); From 98407627f1b9825f0db172956f47defc984747ab Mon Sep 17 00:00:00 2001 From: George Thomas Date: Fri, 31 Mar 2023 15:13:56 +0100 Subject: [PATCH 3/4] refactor: Further split up tree rendering We separate out an initial `Def -> Tree` step, then do most of the interesting processing on `Tree`s. This largely unifies `TreeReactFlow` and `TreeReactFlowOne`. It will also prevent a third duplication for typedefs, and maybe more for future features such as previews. Signed-off-by: George Thomas --- src/components/TreeReactFlow/index.tsx | 361 ++++++++++++------------- src/util.ts | 21 +- 2 files changed, 192 insertions(+), 190 deletions(-) diff --git a/src/components/TreeReactFlow/index.tsx b/src/components/TreeReactFlow/index.tsx index 4941f06b..06f7c48b 100644 --- a/src/components/TreeReactFlow/index.tsx +++ b/src/components/TreeReactFlow/index.tsx @@ -23,9 +23,8 @@ import { useReactFlow, } from "reactflow"; import "./reactflow.css"; -import { MutableRefObject, useEffect, useId, useState } from "react"; +import { MutableRefObject, useId } from "react"; import classNames from "classnames"; -import { unzip } from "fp-ts/lib/Array"; import { combineGraphs, PrimerGraph, @@ -60,6 +59,7 @@ import { } from "./Flavor"; import { ZoomBar } from "./ZoomBar"; import { WasmLayoutType } from "@zxch3n/tidy/wasm_dist"; +import { usePromise } from "@/util"; export type ScrollToDef = (defName: string) => void; @@ -71,25 +71,24 @@ type NodeParams = { selection?: Selection; level: Level; }; +type DefParams = { + nameNodeMultipliers: { width: number; height: number }; +}; export type TreeReactFlowProps = { defs: Def[]; onNodeClick?: ( event: React.MouseEvent, - node: Positioned + node: Positioned> ) => void; treePadding: number; forestLayout: "Horizontal" | "Vertical"; - defNameNodeSizeMultipliers: { width: number; height: number }; + defParams: DefParams; layout: LayoutParams; scrollToDefRef: MutableRefObject; } & NodeParams; export const defaultTreeReactFlowProps: Pick< TreeReactFlowProps, - | "treePadding" - | "forestLayout" - | "defNameNodeSizeMultipliers" - | "layout" - | keyof NodeParams + "treePadding" | "forestLayout" | "defParams" | "layout" | keyof NodeParams > = { level: "Expert", forestLayout: "Horizontal", @@ -97,7 +96,7 @@ export const defaultTreeReactFlowProps: Pick< nodeWidth: 80, nodeHeight: 35, boxPadding: 50, - defNameNodeSizeMultipliers: { width: 3, height: 2 }, + defParams: { nameNodeMultipliers: { width: 3, height: 2 } }, layout: { type: WasmLayoutType.Tidy, margins: { child: 25, sibling: 18 }, @@ -529,135 +528,172 @@ const makePrimerNode = async ( } }; -type PrimerNodeWithDefNoPos = PrimerNode<{ def: GlobalName }>; -type PrimerNodeWithDef = Positioned; +type PrimerNodeWithNested = PrimerNode< + N & { nested: Graph>, PrimerEdge>[] } +>; +type PrimerNodeWithNestedAndDef = PrimerNodeWithNested<{ def: GlobalName }>; -// TreeReactFlow renders multiple definitions on one canvas. -// For each definition, it displays three things: -// - the definition's name -// - the definition's type -// - the definition's body (a term) -// It ensures that these are clearly displayed as "one atomic thing", -// i.e. to avoid confused readings that group the type of 'foo' with the body of 'bar' (etc) -export const TreeReactFlow = (p: TreeReactFlowProps) => { - const [{ nodes, edges }, setLayout] = useState< - Graph - >({ - nodes: [], - edges: [], - }); +const defToTree = async ( + def: Def, + p: DefParams & NodeParams & { layout: LayoutParams } +): Promise> => { + const defNodeId = defNameToNodeId(def.name.baseName); + const sigEdgeId = "def-sig-" + def.name.baseName; + const bodyEdgeId = "def-body-" + def.name.baseName; + const defNameNode: PrimerNodeWithNestedAndDef = { + id: defNodeId, + data: { + def: def.name, + width: p.nodeWidth * p.nameNodeMultipliers.width, + height: p.nodeHeight * p.nameNodeMultipliers.height, + selected: deepEqual(p.selection?.def, def.name) && !p.selection?.node, + nested: [], + }, + type: "primer-def-name", + zIndex: 0, + }; + const defEdge = async ( + tree: APITree, + nodeType: NodeType, + edgeId: string + ): Promise<[Tree, PrimerEdge]> => + augmentTree(tree, (n0) => + makePrimerNode(n0, p, p.layout, 0, nodeType).then(([n, e, nested]) => [ + primerNodeWith(n, { + def: def.name, + nested: nested.map((g) => + graphMap(g, ({ position, ...n }) => ({ + ...primerNodeWith(n, { def: def.name }), + position, + })) + ), + }), + e, + ]) + ).then((t) => [ + t, + { + id: edgeId, + source: defNodeId, + target: t.node.id, + type: "primer-def-name", + zIndex: 0, + sourceHandle: Position.Bottom, + targetHandle: Position.Top, + }, + ]); + const sigTree = await defEdge(def.type_, "SigNode", sigEdgeId); + const bodyTree = await (def.term + ? defEdge(def.term, "BodyNode", bodyEdgeId) + : undefined); + return { + node: defNameNode, + childTrees: [sigTree, ...(bodyTree ? [bodyTree] : [])], + }; +}; - useEffect(() => { - (async () => { - const [trees, nested] = await Promise.all( - p.defs.map< - Promise< - [ - Tree, - Graph[] - ] - > - >(async (def) => { - const defNodeId = defNameToNodeId(def.name.baseName); - const sigEdgeId = "def-sig-" + def.name.baseName; - const bodyEdgeId = "def-body-" + def.name.baseName; - const defNameNode: PrimerNode = { - id: defNodeId, - data: { - def: def.name, - width: p.nodeWidth * p.defNameNodeSizeMultipliers.width, - height: p.nodeHeight * p.defNameNodeSizeMultipliers.height, - selected: - deepEqual(p.selection?.def, def.name) && !p.selection?.node, - }, - type: "primer-def-name", - zIndex: 0, - }; - const defEdge = async ( - tree: APITree, - nodeParams: NodeParams, - nodeType: NodeType, - edgeId: string - ): Promise<{ - subtree: [Tree, PrimerEdge]; - nested: Graph[]; - }> => { - const t = await augmentTree(tree, (n0) => - makePrimerNode(n0, nodeParams, p.layout, 0, nodeType).then( - ([n, e, nested]) => [primerNodeWith(n, { nested }), e] - ) - ); - const nested = treeNodes(t).flatMap((n) => n.data.nested); - return { - subtree: [ - treeMap(t, (n) => primerNodeWith(n, { def: def.name })), - { - id: edgeId, - source: defNodeId, - target: tree.nodeId, - type: "primer-def-name", - zIndex: 0, - sourceHandle: Position.Bottom, - targetHandle: Position.Top, - }, - ], - nested: nested.map((g) => - graphMap(g, ({ position, ...n }) => ({ - position, - ...primerNodeWith(n, { def: def.name }), +/** Renders multiple definitions on one canvas. + * For each definition, it displays three things: + * - the definition's name + * - the definition's type + * - the definition's body (a term) + * It ensures that these are clearly displayed as "one atomic thing", + * i.e. to avoid confused readings that group the type of 'foo' with the body of 'bar' (etc). + */ +export const TreeReactFlow = (p: TreeReactFlowProps) => ( + + defToTree(def, { ...p.defParams, ...p }).then((t) => + layoutTree(t, p.layout) + ) + ) + ).then( + // Space out the forest. + (sizedTrees) => + sizedTrees.reduce< + [Tree, PrimerEdge>[], number] + >( + ([trees, offset], { tree, width, height }) => { + const { increment, offsetVector } = (() => { + switch (p.forestLayout) { + case "Horizontal": + return { + increment: width, + offsetVector: { x: offset, y: 0 }, + }; + case "Vertical": + return { + increment: height, + offsetVector: { x: 0, y: offset }, + }; + } + })(); + return [ + trees.concat( + treeMap(tree, (n) => ({ + ...n, + position: { + x: n.position.x + p.layout.margins.sibling + offsetVector.x, + y: n.position.y + p.layout.margins.child + offsetVector.y, + }, })) ), - }; - }; - const sigTree = await defEdge(def.type_, p, "SigNode", sigEdgeId); - const bodyTree = await (def.term - ? defEdge(def.term, p, "BodyNode", bodyEdgeId) - : undefined); - return [ - { - node: defNameNode, - childTrees: [ - sigTree.subtree, - ...(bodyTree ? [bodyTree.subtree] : []), - ], - }, - [...sigTree.nested, ...(bodyTree ? bodyTree.nested : [])], - ]; - }) - ).then(unzip); - const ts = await Promise.all(trees.map((t) => layoutTree(t, p.layout))); - const graphs = ts.reduce< - [Graph[], number] - >( - ([gs, offset], { tree, width, height }) => { - const { nodes, edges } = treeToGraph(tree); - const { increment, offsetVector } = (() => { - switch (p.forestLayout) { - case "Horizontal": - return { increment: width, offsetVector: { x: offset, y: 0 } }; - case "Vertical": - return { increment: height, offsetVector: { x: 0, y: offset } }; - } - })(); - return [ - gs.concat({ - edges, - nodes: nodes.map((n) => ({ - ...n, - position: { - x: n.position.x + p.layout.margins.sibling + offsetVector.x, - y: n.position.y + p.layout.margins.child + offsetVector.y, - }, - })), - }), - offset + increment + p.treePadding, - ]; - }, - [[], 0] - )[0]; - setLayout(combineGraphs([...graphs, ...nested.flat()])); - })(); - }, [p]); + offset + increment + p.treePadding, + ]; + }, + [[], 0] + )[0] + )} + {...(p.onNodeClick && { onNodeClick: p.onNodeClick })} + scrollToDefRef={p.scrollToDefRef} + > +); +export default TreeReactFlow; + +export type TreeReactFlowOneProps = { + tree?: APITree; + onNodeClick?: (event: React.MouseEvent, node: Positioned) => void; + layout: LayoutParams; + scrollToDefRef: MutableRefObject; +} & NodeParams; + +/** Renders one `APITree` (e.g. one type or one term) on its own individual canvas. + * This is essentially a much simpler version of `TreeReactFlow`. + */ +export const TreeReactFlowOne = (p: TreeReactFlowOneProps) => ( + + makePrimerNode(n0, p, p.layout, 0, "BodyNode").then( + ([n, e, nested]) => [primerNodeWith(n, { nested }), e] + ) + ) + .then((t) => layoutTree(t, p.layout)) + .then(({ tree }) => [tree]) + : new Promise(() => []) + } + {...(p.onNodeClick && { onNodeClick: p.onNodeClick })} + scrollToDefRef={p.scrollToDefRef} + > +); + +// The core of our interaction with ReactFlow: take some abstract trees, and render them. +// This is not exported, but various wrappers around it are. +const Trees = (p: { + makeTrees: Promise>, PrimerEdge>[]>; + onNodeClick?: ( + event: React.MouseEvent, + node: Positioned> + ) => void; + scrollToDefRef: MutableRefObject; +}): JSX.Element => { + const trees = usePromise([], p.makeTrees); + const { nodes, edges } = combineGraphs([ + ...trees.map(treeToGraph), + ...trees.flatMap((tree) => treeNodes(tree).flatMap((n) => n.data.nested)), + ]); // ReactFlow requires a unique id to be passed in if there are // multiple flows on one page. We simply get react to generate @@ -665,7 +701,7 @@ export const TreeReactFlow = (p: TreeReactFlowProps) => { const id = useId(); return ( - + >, PrimerEdge> id={id} {...(p.onNodeClick && { onNodeClick: p.onNodeClick })} nodes={nodes} @@ -704,59 +740,6 @@ const SetTreeReactFlowCallbacks = ({ return <>; }; -export default TreeReactFlow; - -export type TreeReactFlowOneProps = { - tree?: APITree; - onNodeClick?: (event: React.MouseEvent, node: PrimerNode) => void; - layout: LayoutParams; -} & NodeParams; - -// TreeReactFlowOne renders one Tree (i.e. one type or one term) on its own individual canvas. -// It is essentially a much simpler version of TreeReactFlow. -export const TreeReactFlowOne = (p: TreeReactFlowOneProps) => { - const [{ nodes, edges }, setLayout] = useState({ - nodes: [], - edges: [], - }); - - useEffect(() => { - const pt = p.tree; - pt && - (async () => { - const tree = await augmentTree(pt, (n0) => - makePrimerNode(n0, p, p.layout, 0, "BodyNode").then( - ([n, e, nested]) => [primerNodeWith(n, { nested }), e] - ) - ); - const nested = treeNodes(tree).flatMap((n) => n.data.nested); - const t = await layoutTree(tree, p.layout); - const graph = treeToGraph(t.tree); - setLayout(combineGraphs([graph, ...nested.flat()])); - })(); - }, [p]); - - // ReactFlow requires a unique id to be passed in if there are - // multiple flows on one page. We simply get react to generate - // a unique id for us. - const id = useId(); - - return ( - , PrimerEdge> - id={id} - {...(p.onNodeClick && { onNodeClick: p.onNodeClick })} - nodes={nodes} - edges={edges} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - proOptions={{ hideAttribution: true, account: "paid-pro" }} - > - - - - ); -}; - /** A more strongly-typed version of the `ReactFlow` component. * This allows us to use a more refined node type, * check that we register its subtypes correctly with ReactFlow, diff --git a/src/util.ts b/src/util.ts index 990a7950..26b3ba00 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,10 @@ -import { RefObject, useMemo, useSyncExternalStore } from "react"; +import { + RefObject, + useEffect, + useMemo, + useState, + useSyncExternalStore, +} from "react"; /** Evaluates to the type `true` when both parameters are equal, and `false` otherwise. * NB. this actually tests mutual extendability, which is mostly a reasonable definition of @@ -34,3 +40,16 @@ export function useDimensions(ref: RefObject) { ); return useMemo(() => JSON.parse(dimensions), [dimensions]); } + +/** Use some initial data, while waiting for an asynchronous update. + * This encapsulates a common React pattern, which may eventually have a built-in solution: + * https://github.com/reactjs/rfcs/pull/229. + * Using this hook also leads to better type inference. + */ +export const usePromise = (initial: T, p: Promise): T => { + const [data, setData] = useState(initial); + useEffect(() => { + p.then(setData); + }, [p]); + return data; +}; From f4b353255190fb65d8844d11fdd7fe10ff418129 Mon Sep 17 00:00:00 2001 From: George Thomas Date: Thu, 1 Jun 2023 17:41:35 +0100 Subject: [PATCH 4/4] chore: Bump Primer pin This brings in support for displaying and editing type definitions. We need to make quite significant changes to accommodate this. Signed-off-by: George Thomas --- argocd/base/statefulset.yaml | 2 +- flake.lock | 8 +- flake.nix | 2 +- src/Actions.tsx | 30 ++ src/App.tsx | 30 +- .../TreeReactFlow/TreeReactFlow.stories.tsx | 143 +++++- src/components/TreeReactFlow/Types.ts | 42 +- src/components/TreeReactFlow/index.tsx | 443 ++++++++++++++++-- src/primer-api/index.ts | 1 - src/primer-api/model/defSelection.ts | 14 + .../model/getActionOptionsAction.ts | 4 + src/primer-api/model/index.ts | 17 +- src/primer-api/model/inputAction.ts | 4 + src/primer-api/model/module.ts | 4 +- src/primer-api/model/noInputAction.ts | 1 + src/primer-api/model/nodeSelection.ts | 2 +- src/primer-api/model/selection.ts | 9 +- src/primer-api/model/selectionOneOf.ts | 14 + src/primer-api/model/selectionOneOfTag.ts | 15 + src/primer-api/model/selectionOneOfThree.ts | 14 + .../model/selectionOneOfThreeTag.ts | 15 + src/primer-api/model/typeDef.ts | 16 + .../model/typeDefConsFieldSelection.ts | 12 + src/primer-api/model/typeDefConsSelection.ts | 14 + src/primer-api/model/typeDefNodeSelection.ts | 11 + .../model/typeDefNodeSelectionOneOf.ts | 13 + .../model/typeDefNodeSelectionOneOfTag.ts | 15 + .../model/typeDefNodeSelectionOneOfThree.ts | 14 + .../typeDefNodeSelectionOneOfThreeTag.ts | 15 + src/primer-api/model/typeDefSelection.ts | 14 + src/primer-api/model/valCon.ts | 14 + src/util.ts | 6 + 32 files changed, 873 insertions(+), 85 deletions(-) create mode 100644 src/primer-api/model/defSelection.ts create mode 100644 src/primer-api/model/selectionOneOf.ts create mode 100644 src/primer-api/model/selectionOneOfTag.ts create mode 100644 src/primer-api/model/selectionOneOfThree.ts create mode 100644 src/primer-api/model/selectionOneOfThreeTag.ts create mode 100644 src/primer-api/model/typeDef.ts create mode 100644 src/primer-api/model/typeDefConsFieldSelection.ts create mode 100644 src/primer-api/model/typeDefConsSelection.ts create mode 100644 src/primer-api/model/typeDefNodeSelection.ts create mode 100644 src/primer-api/model/typeDefNodeSelectionOneOf.ts create mode 100644 src/primer-api/model/typeDefNodeSelectionOneOfTag.ts create mode 100644 src/primer-api/model/typeDefNodeSelectionOneOfThree.ts create mode 100644 src/primer-api/model/typeDefNodeSelectionOneOfThreeTag.ts create mode 100644 src/primer-api/model/typeDefSelection.ts create mode 100644 src/primer-api/model/valCon.ts diff --git a/argocd/base/statefulset.yaml b/argocd/base/statefulset.yaml index 040334c7..d5f3dce2 100644 --- a/argocd/base/statefulset.yaml +++ b/argocd/base/statefulset.yaml @@ -26,7 +26,7 @@ spec: # Note: use the *dev* version of the package here, so that # PRs can deploy `primer-service` container images that have # not yet been merged to `primer` `main`. - image: ghcr.io/hackworthltd/primer-service-dev:git-1145eb89eb85354f67ec3cc5e25f59ef092a6c39 + image: ghcr.io/hackworthltd/primer-service-dev:git-f35380015a14db87c5f330a84a5830adc51e1cbb ports: - containerPort: 8081 env: diff --git a/flake.lock b/flake.lock index 81edc1c0..19971d1c 100644 --- a/flake.lock +++ b/flake.lock @@ -1678,17 +1678,17 @@ "pre-commit-hooks-nix": "pre-commit-hooks-nix_4" }, "locked": { - "lastModified": 1685183219, - "narHash": "sha256-kYzYZW/WTGzKz47GctjNTLOawhwC1jSVowaWKIPuoa8=", + "lastModified": 1685528982, + "narHash": "sha256-nxMQSO+SbRXxe/kpRZYfKVeiG5+ok2+wSulUBT35BjM=", "owner": "hackworthltd", "repo": "primer", - "rev": "1145eb89eb85354f67ec3cc5e25f59ef092a6c39", + "rev": "f35380015a14db87c5f330a84a5830adc51e1cbb", "type": "github" }, "original": { "owner": "hackworthltd", "repo": "primer", - "rev": "1145eb89eb85354f67ec3cc5e25f59ef092a6c39", + "rev": "f35380015a14db87c5f330a84a5830adc51e1cbb", "type": "github" } }, diff --git a/flake.nix b/flake.nix index d8b857b5..87099e1f 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ # Note: don't override any of primer's Nix flake inputs, or else # we won't hit its binary cache. - primer.url = github:hackworthltd/primer/1145eb89eb85354f67ec3cc5e25f59ef092a6c39; + primer.url = github:hackworthltd/primer/f35380015a14db87c5f330a84a5830adc51e1cbb; flake-parts.url = "github:hercules-ci/flake-parts"; }; diff --git a/src/Actions.tsx b/src/Actions.tsx index aef8033a..fd7c35d8 100644 --- a/src/Actions.tsx +++ b/src/Actions.tsx @@ -82,6 +82,16 @@ export const actionName = ( return prose("r"); case "RenameDef": return prose("r"); + case "RenameType": + return prose("r"); + case "AddCon": + return prose("+"); + case "RenameCon": + return prose("r"); + case "RenameTypeParam": + return prose("r"); + case "AddConField": + return prose("+"); } }; @@ -171,6 +181,16 @@ export const actionDescription = ( return "Rename this type variable"; case "RenameDef": return "Rename this definition"; + case "RenameType": + return "Rename this type"; + case "AddCon": + return "Add a constructor to this type"; + case "RenameCon": + return "Rename this constructor"; + case "RenameTypeParam": + return "Rename this parameter"; + case "AddConField": + return "Add a parameter to this constructor"; } }; @@ -246,5 +266,15 @@ export const actionType = (action: NoInputAction | InputAction): ActionType => { return "Primary"; case "RenameDef": return "Primary"; + case "RenameType": + return "Primary"; + case "AddCon": + return "Primary"; + case "RenameCon": + return "Primary"; + case "RenameTypeParam": + return "Primary"; + case "AddConField": + return "Primary"; } }; diff --git a/src/App.tsx b/src/App.tsx index d336a706..83e1243d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -228,16 +228,16 @@ const AppNoError = ({ .sort((a, b) => cmpName(a.name, b.name)) .map((d) => d.name.baseName), types: p.module.types - .sort((a, b) => cmpName(a, b)) - .map((t) => t.baseName), + .sort((a, b) => cmpName(a.name, b.name)) + .map((t) => t.name.baseName), importedDefs: p.imports .flatMap((m) => m.defs) .sort((a, b) => cmpName(a.name, b.name)) .map((d) => d.name.baseName), importedTypes: p.imports .flatMap((m) => m.types) - .sort((a, b) => cmpName(a, b)) - .map((t) => t.baseName), + .sort((a, b) => cmpName(a.name, b.name)) + .map((t) => t.name.baseName), }} onClickDef={(defName, _event) => { if (scrollToDefRef.current != undefined) { @@ -318,23 +318,9 @@ const AppNoError = ({ scrollToDefRef={scrollToDefRef} {...defaultTreeReactFlowProps} {...(selection && { selection })} - onNodeClick={(_e, node) => { - if (!("nodeType" in node.data)) { - setSelection({ - def: node.data.def, - }); - } else { - const id = Number(node.id); - // Non-numeric IDs correspond to non-selectable nodes (those with no ID in backend) e.g. pattern constructors. - if (!isNaN(id)) { - setSelection({ - def: node.data.def, - node: { id, nodeType: node.data.nodeType }, - }); - } - } - }} + onNodeClick={(_e, sel) => sel && setSelection(sel)} defs={p.module.defs} + typeDefs={p.module.types} level={level} /> @@ -394,7 +380,9 @@ const AppNoError = ({ ) : null} {showCreateTypeDefModal ? ( t.baseName))} + moduleTypeDefNames={ + new Set(p.module.types.map((t) => t.name.baseName)) + } open={showCreateTypeDefModal} onClose={() => setShowCreateTypeDefModal(false)} onCancel={() => setShowCreateTypeDefModal(false)} diff --git a/src/components/TreeReactFlow/TreeReactFlow.stories.tsx b/src/components/TreeReactFlow/TreeReactFlow.stories.tsx index bdacce8d..ab12dfb9 100644 --- a/src/components/TreeReactFlow/TreeReactFlow.stories.tsx +++ b/src/components/TreeReactFlow/TreeReactFlow.stories.tsx @@ -13,7 +13,7 @@ import { oddEvenTrees, oddEvenTreesMiscStyles, } from "@/components/examples/trees"; -import { Tree } from "@/primer-api"; +import { Tree, TypeDef } from "@/primer-api"; export default { title: "Application/Component Library/TreeReactFlow", @@ -26,7 +26,7 @@ export default { }, } as ComponentMeta; -const props: Omit = { +const props: Omit = { ...defaultTreeReactFlowProps, scrollToDefRef: { current: undefined }, forestLayout: "Vertical", @@ -64,6 +64,87 @@ const def5 = { term: tree5, type_: emptyTypeTree("5"), }; +const typeDef1: TypeDef = { + name: { qualifiedModule: [], baseName: "Either-ish" }, + params: ["a", "b"], + constructors: [ + { + name: { qualifiedModule: [], baseName: "Left" }, + fields: [ + { + nodeId: "1100", + body: { + tag: "TextBody", + contents: { + fst: "TVar", + snd: { baseName: "a" }, + }, + }, + childTrees: [], + }, + ], + }, + { + name: { qualifiedModule: [], baseName: "Right" }, + fields: [ + { + nodeId: "1101", + body: { tag: "NoBody", contents: "APP" }, + childTrees: [ + { + nodeId: "1102", + body: { tag: "NoBody", contents: "APP" }, + childTrees: [ + { + nodeId: "1103", + body: { + tag: "TextBody", + contents: { + fst: "TCon", + snd: { baseName: "Pair" }, + }, + }, + childTrees: [], + }, + { + nodeId: "1104", + body: { + tag: "TextBody", + contents: { + fst: "TCon", + snd: { baseName: "Bool" }, + }, + }, + childTrees: [], + }, + ], + }, + { + nodeId: "1105", + body: { + tag: "TextBody", + contents: { + fst: "TVar", + snd: { baseName: "b" }, + }, + }, + childTrees: [], + }, + ], + }, + { + nodeId: "1106", + body: { + tag: "TextBody", + contents: { fst: "TCon", snd: { baseName: "Int" } }, + }, + childTrees: [], + }, + ], + }, + ], + nameHints: ["x", "y"], +}; const oddEvenDefs = oddEvenTrees.map(([baseName, term]) => { return { name: { qualifiedModule: [], baseName }, @@ -90,7 +171,11 @@ export const Tree1: ComponentStory = ( treeSized({ ...args, defs: [def1], - selection: { def: def1.name, node: { nodeType: "BodyNode", id: 100 } }, + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def1.name, node: { nodeType: "BodyNode", meta: 100 } }, + }, }); export const Tree2: ComponentStory = ( args: TreeReactFlowProps @@ -98,6 +183,7 @@ export const Tree2: ComponentStory = ( treeSized({ ...args, defs: [def2], + typeDefs: [], }); export const Tree3: ComponentStory = ( args: TreeReactFlowProps @@ -105,7 +191,11 @@ export const Tree3: ComponentStory = ( treeSized({ ...args, defs: [def3], - selection: { def: def3.name, node: { nodeType: "BodyNode", id: 301 } }, + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def3.name, node: { nodeType: "BodyNode", meta: 301 } }, + }, }); export const Tree4: ComponentStory = ( args: TreeReactFlowProps @@ -113,7 +203,11 @@ export const Tree4: ComponentStory = ( treeSized({ ...args, defs: [def4], - selection: { def: def4.name, node: { nodeType: "BodyNode", id: 409 } }, + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def4.name, node: { nodeType: "BodyNode", meta: 409 } }, + }, }); export const Tree5: ComponentStory = ( args: TreeReactFlowProps @@ -121,7 +215,26 @@ export const Tree5: ComponentStory = ( treeSized({ ...args, defs: [def5], - selection: { def: def5.name, node: { nodeType: "BodyNode", id: 503 } }, + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def5.name, node: { nodeType: "BodyNode", meta: 503 } }, + }, + }); +export const TreeType1: ComponentStory = ( + args: TreeReactFlowProps +) => + treeSized({ + ...args, + defs: [], + typeDefs: [typeDef1], + selection: { + tag: "SelectionTypeDef", + contents: { + def: typeDef1.name, + node: { tag: "TypeDefParamNodeSelection", contents: "a" }, + }, + }, }); export const AllTrees: ComponentStory = ( args: TreeReactFlowProps @@ -129,7 +242,11 @@ export const AllTrees: ComponentStory = ( treeSized({ ...args, defs: [def1, def2, def3, def4, def5], - selection: { def: def3.name, node: { nodeType: "BodyNode", id: 301 } }, + typeDefs: [typeDef1], + selection: { + tag: "SelectionDef", + contents: { def: def3.name, node: { nodeType: "BodyNode", meta: 301 } }, + }, }); export const OddAndEven: ComponentStory = ( args: TreeReactFlowProps @@ -137,7 +254,11 @@ export const OddAndEven: ComponentStory = ( treeSized({ ...args, defs: oddEvenDefs, - selection: { def: def5.name, node: { nodeType: "BodyNode", id: 5 } }, + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def5.name, node: { nodeType: "BodyNode", meta: 5 } }, + }, }); export const OddAndEvenMiscStyles: ComponentStory = ( args: TreeReactFlowProps @@ -145,5 +266,9 @@ export const OddAndEvenMiscStyles: ComponentStory = ( treeSized({ ...args, defs: oddEvenDefsMiscStyles, - selection: { def: def5.name, node: { nodeType: "BodyNode", id: 5 } }, + typeDefs: [], + selection: { + tag: "SelectionDef", + contents: { def: def5.name, node: { nodeType: "BodyNode", meta: 5 } }, + }, }); diff --git a/src/components/TreeReactFlow/Types.ts b/src/components/TreeReactFlow/Types.ts index 76da1ad4..1f36074d 100644 --- a/src/components/TreeReactFlow/Types.ts +++ b/src/components/TreeReactFlow/Types.ts @@ -121,6 +121,9 @@ export type PrimerNode = { | { type: "primer-simple"; data: PrimerSimpleNodeProps } | { type: "primer-box"; data: PrimerBoxNodeProps } | { type: "primer-def-name"; data: PrimerDefNameNodeProps } + | { type: "primer-typedef-name"; data: PrimerTypeDefNameNodeProps } + | { type: "primer-typedef-param"; data: PrimerTypeDefParamNodeProps } + | { type: "primer-typedef-cons"; data: PrimerTypeDefConsNodeProps } ); export const primerNodeWith = (n: PrimerNode, x: T): PrimerNode => @@ -131,9 +134,25 @@ export const primerNodeWith = (n: PrimerNode, x: T): PrimerNode => data: { ...n1.data, ...x }, }))(n); +/** Data corresponding to a node from the backend. + * This is not used by special nodes, like term definition names or most parts of type definitions, + * but only in places where the backend allows an arbitrarily nested (term or type) expression. + * These are: the bodies and signatures of term defs, and the types of constructor fields in type defs. + * */ +export type NodeData = + | { + tag: "termDefNode"; + nodeType: NodeType; + } + | { + tag: "typeDefFieldNode"; + con: GlobalName; + index: number; + }; + /** Node properties. */ export type PrimerNodeProps = { - nodeType: NodeType; + nodeData: NodeData; syntax: boolean; flavor: NodeFlavorTextBody | NodeFlavorPrimBody | NodeFlavorNoBody; contents: string; @@ -141,13 +160,13 @@ export type PrimerNodeProps = { /** Properties for a simple node. */ export type PrimerSimpleNodeProps = { - nodeType: NodeType; + nodeData: NodeData; flavor: NodeFlavorNoBody; }; /** Properties for a box node. */ export type PrimerBoxNodeProps = { - nodeType: NodeType; + nodeData: NodeData; flavor: NodeFlavorBoxBody; }; @@ -156,6 +175,21 @@ export type PrimerDefNameNodeProps = { def: GlobalName; }; +/** Properties for the root of a type definition. */ +export type PrimerTypeDefNameNodeProps = { + name: GlobalName; +}; + +/** Properties for a typedef parameter node. */ +export type PrimerTypeDefParamNodeProps = { + name: string; +}; + +/** Properties for a constructor node. */ +export type PrimerTypeDefConsNodeProps = { + name: GlobalName; +}; + /** Properties common to every type of node. */ export type PrimerCommonNodeProps = { width: number; @@ -172,7 +206,7 @@ export type PrimerEdge = { sourceHandle: Position; targetHandle: Position; zIndex: number; -} & ({ type: "primer"; data: PrimerEdgeProps } | { type: "primer-def-name" }); +} & ({ type: "primer"; data: PrimerEdgeProps } | { type: "primer-def" }); export type PrimerEdgeProps = { flavor: NodeFlavor }; diff --git a/src/components/TreeReactFlow/index.tsx b/src/components/TreeReactFlow/index.tsx index 06f7c48b..de7a02ca 100644 --- a/src/components/TreeReactFlow/index.tsx +++ b/src/components/TreeReactFlow/index.tsx @@ -6,6 +6,7 @@ import { GlobalName, NodeType, Level, + TypeDef, } from "@/primer-api"; import { ReactFlow, @@ -23,7 +24,7 @@ import { useReactFlow, } from "reactflow"; import "./reactflow.css"; -import { MutableRefObject, useId } from "react"; +import { MutableRefObject, PropsWithChildren, useId } from "react"; import classNames from "classnames"; import { combineGraphs, @@ -44,9 +45,12 @@ import { PrimerCommonNodeProps, treeNodes, PrimerEdgeProps, + PrimerTypeDefConsNodeProps, + PrimerTypeDefParamNodeProps, + PrimerTypeDefNameNodeProps, + NodeData, } from "./Types"; import { LayoutParams, layoutTree } from "./layoutTree"; -import deepEqual from "deep-equal"; import { boxFlavorBackground, commonHoverClasses, @@ -59,7 +63,8 @@ import { } from "./Flavor"; import { ZoomBar } from "./ZoomBar"; import { WasmLayoutType } from "@zxch3n/tidy/wasm_dist"; -import { usePromise } from "@/util"; +import { deepEqualTyped, usePromise } from "@/util"; +import { mapSnd } from "fp-ts/lib/Tuple"; export type ScrollToDef = (defName: string) => void; @@ -76,9 +81,10 @@ type DefParams = { }; export type TreeReactFlowProps = { defs: Def[]; - onNodeClick?: ( + typeDefs: TypeDef[]; + onNodeClick: ( event: React.MouseEvent, - node: Positioned> + selection: Selection | undefined ) => void; treePadding: number; forestLayout: "Horizontal" | "Vertical"; @@ -258,6 +264,105 @@ const nodeTypes = { {handle("source", Position.Bottom)} ), + "primer-typedef-name": ({ + data, + }: { + data: PrimerCommonNodeProps & PrimerTypeDefNameNodeProps; + }) => ( + <> +
+
+ {data.name.baseName} +
+
+ {handle("source", Position.Bottom)} + {handle("source", Position.Right)} + + ), + "primer-typedef-param": ({ + data, + }: { + data: PrimerCommonNodeProps & PrimerTypeDefParamNodeProps; + }) => ( + <> + {handle("target", Position.Left)} +
+ { +
+ {data.name} +
+ } +
+ {handle("source", Position.Bottom)} + {handle("source", Position.Right)} + + ), + "primer-typedef-cons": ({ + data, + }: { + data: PrimerCommonNodeProps & PrimerTypeDefConsNodeProps; + }) => ( + <> + {handle("target", Position.Top)} + {handle("target", Position.Left)} +
+
+ {data.name.baseName} +
+
+ {handle("source", Position.Bottom)} + {handle("source", Position.Right)} + + ), }; const edgeTypes = { @@ -290,7 +395,7 @@ const edgeTypes = { /> ); }, - "primer-def-name": ({ + "primer-def": ({ id, sourceX, sourceY, @@ -354,7 +459,8 @@ const makePrimerNode = async ( p: NodeParams, layout: LayoutParams, zIndex: number, - nodeType: NodeType + nodeData: NodeData, + def?: GlobalName ): Promise< [ PrimerNode, @@ -364,13 +470,16 @@ const makePrimerNode = async ( PrimerGraph[] ] > => { - const selected = p.selection?.node?.id?.toString() == node.nodeId; + const selected = deepEqualTyped( + def && makeSelectionFromNodeData(def, node.nodeId, nodeData), + p.selection + ); const id = node.nodeId; const common = { width: p.nodeWidth, height: p.nodeHeight, selected, - nodeType, + nodeData, }; const edgeCommon = ( child: PrimerNode, @@ -485,7 +594,7 @@ const makePrimerNode = async ( case "BoxBody": { const { fst: flavor, snd: t } = node.body.contents; const bodyTree = await augmentTree(t, (n0) => - makePrimerNode(n0, p, layout, zIndex + 1, nodeType).then( + makePrimerNode(n0, p, layout, zIndex + 1, nodeData, def).then( ([n, e, nested]) => [primerNodeWith(n, { nested }), e] ) ); @@ -528,6 +637,89 @@ const makePrimerNode = async ( } }; +const makeSelectionFromNode = ( + node: PrimerNode<{ def: GlobalName }> +): Selection | undefined => { + if (node.type == "primer-typedef-name") { + return { + tag: "SelectionTypeDef", + contents: { def: node.data.def }, + }; + } else if (node.type == "primer-def-name") { + return { + tag: "SelectionDef", + contents: { def: node.data.def }, + }; + } else if (node.type == "primer-typedef-param") { + return { + tag: "SelectionTypeDef", + contents: { + def: node.data.def, + node: { + tag: "TypeDefParamNodeSelection", + contents: node.data.name, + }, + }, + }; + } else if (node.type == "primer-typedef-cons") { + return { + tag: "SelectionTypeDef", + contents: { + def: node.data.def, + node: { + tag: "TypeDefConsNodeSelection", + contents: { con: node.data.name }, + }, + }, + }; + } else { + return makeSelectionFromNodeData( + node.data.def, + node.id, + node.data.nodeData + ); + } +}; + +const makeSelectionFromNodeData = ( + def: GlobalName, + id0: string, + nodeData: NodeData +): Selection | undefined => { + const id = Number(id0); + // Non-numeric IDs correspond to non-selectable nodes (those with no ID in backend) e.g. pattern constructors. + if (!isNaN(id)) { + if (nodeData.tag == "termDefNode") { + return { + tag: "SelectionDef", + contents: { + def, + node: { meta: id, nodeType: nodeData.nodeType }, + }, + }; + } else { + return { + tag: "SelectionTypeDef", + contents: { + def, + node: { + tag: "TypeDefConsNodeSelection", + contents: { + con: nodeData.con, + field: { + index: nodeData.index, + meta: id, + }, + }, + }, + }, + }; + } + } else { + return undefined; + } +}; + type PrimerNodeWithNested = PrimerNode< N & { nested: Graph>, PrimerEdge>[] } >; @@ -546,7 +738,10 @@ const defToTree = async ( def: def.name, width: p.nodeWidth * p.nameNodeMultipliers.width, height: p.nodeHeight * p.nameNodeMultipliers.height, - selected: deepEqual(p.selection?.def, def.name) && !p.selection?.node, + selected: deepEqualTyped(p.selection, { + tag: "SelectionDef", + contents: { def: def.name }, + }), nested: [], }, type: "primer-def-name", @@ -558,7 +753,14 @@ const defToTree = async ( edgeId: string ): Promise<[Tree, PrimerEdge]> => augmentTree(tree, (n0) => - makePrimerNode(n0, p, p.layout, 0, nodeType).then(([n, e, nested]) => [ + makePrimerNode( + n0, + p, + p.layout, + 0, + { tag: "termDefNode", nodeType }, + def.name + ).then(([n, e, nested]) => [ primerNodeWith(n, { def: def.name, nested: nested.map((g) => @@ -576,7 +778,7 @@ const defToTree = async ( id: edgeId, source: defNodeId, target: t.node.id, - type: "primer-def-name", + type: "primer-def", zIndex: 0, sourceHandle: Position.Bottom, targetHandle: Position.Top, @@ -592,6 +794,162 @@ const defToTree = async ( }; }; +const typeDefToTree = async ( + def: TypeDef, + p: DefParams & NodeParams & { layout: LayoutParams } +): Promise, PrimerEdge>> => { + type N = PrimerNode<{ def: GlobalName }>; + type E = PrimerEdge; + type T = Tree; + + const rootId = "typedef-name-" + def.name.baseName; + const paramsTree = def.params.reduceRight< + [T, (parentId: string) => E] | undefined + >((child, name) => { + const id = "typedef-param-" + name; + const node: N = { + id, + type: "primer-typedef-param", + data: { + def: def.name, + width: p.nodeWidth, + height: p.nodeHeight, + name, + selected: deepEqualTyped(p.selection, { + tag: "SelectionTypeDef", + contents: { + def: def.name, + node: { + tag: "TypeDefParamNodeSelection", + contents: name, + }, + }, + }), + }, + zIndex: 0, + }; + return [ + { + node, + childTrees: [], + ...(child + ? { rightChild: mapSnd((f: (parentId: string) => E) => f(id))(child) } + : {}), + }, + (parentId) => ({ + id: JSON.stringify([parentId, id]), + source: parentId, + target: id, + type: "primer-def", + zIndex: 0, + sourceHandle: Position.Right, + targetHandle: Position.Left, + }), + ]; + }, undefined); + const constructorTrees: [T, E][] = await Promise.all( + (def.constructors ?? []).map(async (cons) => { + const consId = "typedef-cons-" + cons.name.baseName; + const fieldTrees = await Promise.all( + cons.fields.map>((t, index) => + augmentTree(t, (n0) => + makePrimerNode( + n0, + p, + p.layout, + 0, + { tag: "typeDefFieldNode", con: cons.name, index }, + def.name + ).then(([n, e, nested]) => [ + primerNodeWith(n, { + def: def.name, + nested: nested.map((g) => + graphMap(g, ({ position, ...n }) => ({ + ...primerNodeWith(n, { def: def.name }), + position, + })) + ), + }), + e, + ]) + ).then((t) => [ + t, + { + id: JSON.stringify([consId, t.node.id]), + source: consId, + target: t.node.id, + type: "primer-def", + zIndex: 0, + sourceHandle: Position.Bottom, + targetHandle: Position.Top, + }, + ]) + ) + ); + return [ + { + node: { + id: consId, + type: "primer-typedef-cons", + data: { + def: def.name, + name: cons.name, + width: p.nodeWidth, + height: p.nodeHeight, + selected: deepEqualTyped(p.selection, { + tag: "SelectionTypeDef", + contents: { + def: def.name, + node: { + tag: "TypeDefConsNodeSelection", + contents: { con: cons.name }, + }, + }, + }), + }, + zIndex: 0, + }, + childTrees: fieldTrees, + }, + { + id: JSON.stringify([rootId, consId]), + type: "primer-def", + source: rootId, + target: consId, + sourceHandle: Position.Bottom, + targetHandle: Position.Top, + zIndex: 0, + }, + ]; + }) + ); + return { + node: { + id: rootId, + type: "primer-typedef-name", + data: { + def: def.name, + name: def.name, + height: p.nodeHeight * p.nameNodeMultipliers.height, + width: p.nodeWidth * p.nameNodeMultipliers.width, + selected: deepEqualTyped(p.selection, { + tag: "SelectionTypeDef", + contents: { def: def.name }, + }), + }, + zIndex: 0, + }, + childTrees: constructorTrees, + ...(paramsTree + ? { + rightChild: mapSnd((f: (parentId: string) => E) => f(rootId))( + paramsTree + ), + } + : {}), + }; +}; + /** Renders multiple definitions on one canvas. * For each definition, it displays three things: * - the definition's name @@ -602,13 +960,27 @@ const defToTree = async ( */ export const TreeReactFlow = (p: TreeReactFlowProps) => ( + makeTrees={Promise.all([ + ...p.typeDefs.map((def) => + typeDefToTree(def, { ...p.defParams, ...p }).then((t) => + layoutTree(t, p.layout).then(({ tree, width, height }) => ({ + // All we're doing here is adding `nested: []` to all type def nodes. + // We just have to be very explicit here in order to please the typechecker. + width, + height, + tree: treeMap(tree, ({ position, ...n }) => ({ + position, + ...primerNodeWith(n, { nested: [], ...n.data }), + })), + })) + ) + ), + ...p.defs.map((def) => defToTree(def, { ...p.defParams, ...p }).then((t) => layoutTree(t, p.layout) ) - ) - ).then( + ), + ]).then( // Space out the forest. (sizedTrees) => sizedTrees.reduce< @@ -645,9 +1017,12 @@ export const TreeReactFlow = (p: TreeReactFlowProps) => ( [[], 0] )[0] )} - {...(p.onNodeClick && { onNodeClick: p.onNodeClick })} - scrollToDefRef={p.scrollToDefRef} - > + onNodeClick={(mouseEvent, node) => + p.onNodeClick(mouseEvent, makeSelectionFromNode(node)) + } + > + + ); export default TreeReactFlow; @@ -655,7 +1030,6 @@ export type TreeReactFlowOneProps = { tree?: APITree; onNodeClick?: (event: React.MouseEvent, node: Positioned) => void; layout: LayoutParams; - scrollToDefRef: MutableRefObject; } & NodeParams; /** Renders one `APITree` (e.g. one type or one term) on its own individual canvas. @@ -666,29 +1040,30 @@ export const TreeReactFlowOne = (p: TreeReactFlowOneProps) => ( makeTrees={ p.tree ? augmentTree(p.tree, (n0) => - makePrimerNode(n0, p, p.layout, 0, "BodyNode").then( - ([n, e, nested]) => [primerNodeWith(n, { nested }), e] - ) + makePrimerNode(n0, p, p.layout, 0, { + tag: "termDefNode", + nodeType: "BodyNode", + }).then(([n, e, nested]) => [primerNodeWith(n, { nested }), e]) ) .then((t) => layoutTree(t, p.layout)) .then(({ tree }) => [tree]) : new Promise(() => []) } {...(p.onNodeClick && { onNodeClick: p.onNodeClick })} - scrollToDefRef={p.scrollToDefRef} > ); // The core of our interaction with ReactFlow: take some abstract trees, and render them. // This is not exported, but various wrappers around it are. -const Trees = (p: { - makeTrees: Promise>, PrimerEdge>[]>; - onNodeClick?: ( - event: React.MouseEvent, - node: Positioned> - ) => void; - scrollToDefRef: MutableRefObject; -}): JSX.Element => { +const Trees = ( + p: PropsWithChildren<{ + makeTrees: Promise>, PrimerEdge>[]>; + onNodeClick?: ( + event: React.MouseEvent, + node: Positioned> + ) => void; + }> +): JSX.Element => { const trees = usePromise([], p.makeTrees); const { nodes, edges } = combineGraphs([ ...trees.map(treeToGraph), @@ -712,7 +1087,7 @@ const Trees = (p: { > - + {p.children} ); }; diff --git a/src/primer-api/index.ts b/src/primer-api/index.ts index 8df5b35c..a541456e 100644 --- a/src/primer-api/index.ts +++ b/src/primer-api/index.ts @@ -1,3 +1,2 @@ export * from './primer-api'; export * from './model'; - diff --git a/src/primer-api/model/defSelection.ts b/src/primer-api/model/defSelection.ts new file mode 100644 index 00000000..a5c38817 --- /dev/null +++ b/src/primer-api/model/defSelection.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { GlobalName } from './globalName'; +import type { NodeSelection } from './nodeSelection'; + +export interface DefSelection { + def: GlobalName; + node?: NodeSelection; +} diff --git a/src/primer-api/model/getActionOptionsAction.ts b/src/primer-api/model/getActionOptionsAction.ts index 680b9fbe..66ce979f 100644 --- a/src/primer-api/model/getActionOptionsAction.ts +++ b/src/primer-api/model/getActionOptionsAction.ts @@ -29,4 +29,8 @@ export const GetActionOptionsAction = { MakeForall: 'MakeForall', RenameForall: 'RenameForall', RenameDef: 'RenameDef', + RenameType: 'RenameType', + RenameCon: 'RenameCon', + RenameTypeParam: 'RenameTypeParam', + AddCon: 'AddCon', } as const; diff --git a/src/primer-api/model/index.ts b/src/primer-api/model/index.ts index d37387eb..a1d349c5 100644 --- a/src/primer-api/model/index.ts +++ b/src/primer-api/model/index.ts @@ -17,6 +17,7 @@ export * from './applyActionWithInputParams'; export * from './createDefinitionParams'; export * from './createTypeDefBody'; export * from './def'; +export * from './defSelection'; export * from './evalFullParams'; export * from './evalFullResp'; export * from './evalFullRespOneOf'; @@ -65,7 +66,21 @@ export * from './recordPairNodeFlavorBoxBodyTree'; export * from './recordPairNodeFlavorPrimBodyPrimCon'; export * from './recordPairNodeFlavorTextBodyName'; export * from './selection'; +export * from './selectionOneOf'; +export * from './selectionOneOfTag'; +export * from './selectionOneOfThree'; +export * from './selectionOneOfThreeTag'; export * from './session'; export * from './sessionName'; export * from './tree'; -export * from './uuid'; \ No newline at end of file +export * from './typeDef'; +export * from './typeDefConsFieldSelection'; +export * from './typeDefConsSelection'; +export * from './typeDefNodeSelection'; +export * from './typeDefNodeSelectionOneOf'; +export * from './typeDefNodeSelectionOneOfTag'; +export * from './typeDefNodeSelectionOneOfThree'; +export * from './typeDefNodeSelectionOneOfThreeTag'; +export * from './typeDefSelection'; +export * from './uuid'; +export * from './valCon'; \ No newline at end of file diff --git a/src/primer-api/model/inputAction.ts b/src/primer-api/model/inputAction.ts index 03a3d946..8bf98cd6 100644 --- a/src/primer-api/model/inputAction.ts +++ b/src/primer-api/model/inputAction.ts @@ -29,4 +29,8 @@ export const InputAction = { MakeForall: 'MakeForall', RenameForall: 'RenameForall', RenameDef: 'RenameDef', + RenameType: 'RenameType', + RenameCon: 'RenameCon', + RenameTypeParam: 'RenameTypeParam', + AddCon: 'AddCon', } as const; diff --git a/src/primer-api/model/module.ts b/src/primer-api/model/module.ts index 8a8356ac..9a906d65 100644 --- a/src/primer-api/model/module.ts +++ b/src/primer-api/model/module.ts @@ -6,11 +6,11 @@ * OpenAPI spec version: 0.7 */ import type { Def } from './def'; -import type { GlobalName } from './globalName'; +import type { TypeDef } from './typeDef'; export interface Module { defs: Def[]; editable: boolean; modname: string[]; - types: GlobalName[]; + types: TypeDef[]; } diff --git a/src/primer-api/model/noInputAction.ts b/src/primer-api/model/noInputAction.ts index a7109162..f526082f 100644 --- a/src/primer-api/model/noInputAction.ts +++ b/src/primer-api/model/noInputAction.ts @@ -28,4 +28,5 @@ export const NoInputAction = { DeleteType: 'DeleteType', DuplicateDef: 'DuplicateDef', DeleteDef: 'DeleteDef', + AddConField: 'AddConField', } as const; diff --git a/src/primer-api/model/nodeSelection.ts b/src/primer-api/model/nodeSelection.ts index 8a9a7bc6..c157ac89 100644 --- a/src/primer-api/model/nodeSelection.ts +++ b/src/primer-api/model/nodeSelection.ts @@ -8,6 +8,6 @@ import type { NodeType } from './nodeType'; export interface NodeSelection { - id: number; + meta: number; nodeType: NodeType; } diff --git a/src/primer-api/model/selection.ts b/src/primer-api/model/selection.ts index 43b33ce4..c8683606 100644 --- a/src/primer-api/model/selection.ts +++ b/src/primer-api/model/selection.ts @@ -5,10 +5,7 @@ * A backend service implementing a pedagogic functional programming language. * OpenAPI spec version: 0.7 */ -import type { GlobalName } from './globalName'; -import type { NodeSelection } from './nodeSelection'; +import type { SelectionOneOf } from './selectionOneOf'; +import type { SelectionOneOfThree } from './selectionOneOfThree'; -export interface Selection { - def: GlobalName; - node?: NodeSelection; -} +export type Selection = SelectionOneOf | SelectionOneOfThree; diff --git a/src/primer-api/model/selectionOneOf.ts b/src/primer-api/model/selectionOneOf.ts new file mode 100644 index 00000000..bb9b2f2a --- /dev/null +++ b/src/primer-api/model/selectionOneOf.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { DefSelection } from './defSelection'; +import type { SelectionOneOfTag } from './selectionOneOfTag'; + +export type SelectionOneOf = { + contents: DefSelection; + tag: SelectionOneOfTag; +}; diff --git a/src/primer-api/model/selectionOneOfTag.ts b/src/primer-api/model/selectionOneOfTag.ts new file mode 100644 index 00000000..a7baa249 --- /dev/null +++ b/src/primer-api/model/selectionOneOfTag.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ + +export type SelectionOneOfTag = typeof SelectionOneOfTag[keyof typeof SelectionOneOfTag]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SelectionOneOfTag = { + SelectionDef: 'SelectionDef', +} as const; diff --git a/src/primer-api/model/selectionOneOfThree.ts b/src/primer-api/model/selectionOneOfThree.ts new file mode 100644 index 00000000..98ba0b92 --- /dev/null +++ b/src/primer-api/model/selectionOneOfThree.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { TypeDefSelection } from './typeDefSelection'; +import type { SelectionOneOfThreeTag } from './selectionOneOfThreeTag'; + +export type SelectionOneOfThree = { + contents: TypeDefSelection; + tag: SelectionOneOfThreeTag; +}; diff --git a/src/primer-api/model/selectionOneOfThreeTag.ts b/src/primer-api/model/selectionOneOfThreeTag.ts new file mode 100644 index 00000000..71c11c7c --- /dev/null +++ b/src/primer-api/model/selectionOneOfThreeTag.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ + +export type SelectionOneOfThreeTag = typeof SelectionOneOfThreeTag[keyof typeof SelectionOneOfThreeTag]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SelectionOneOfThreeTag = { + SelectionTypeDef: 'SelectionTypeDef', +} as const; diff --git a/src/primer-api/model/typeDef.ts b/src/primer-api/model/typeDef.ts new file mode 100644 index 00000000..b2dcec17 --- /dev/null +++ b/src/primer-api/model/typeDef.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { ValCon } from './valCon'; +import type { GlobalName } from './globalName'; + +export interface TypeDef { + constructors?: ValCon[]; + name: GlobalName; + nameHints: string[]; + params: string[]; +} diff --git a/src/primer-api/model/typeDefConsFieldSelection.ts b/src/primer-api/model/typeDefConsFieldSelection.ts new file mode 100644 index 00000000..2a677c79 --- /dev/null +++ b/src/primer-api/model/typeDefConsFieldSelection.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ + +export interface TypeDefConsFieldSelection { + index: number; + meta: number; +} diff --git a/src/primer-api/model/typeDefConsSelection.ts b/src/primer-api/model/typeDefConsSelection.ts new file mode 100644 index 00000000..f8de3873 --- /dev/null +++ b/src/primer-api/model/typeDefConsSelection.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { GlobalName } from './globalName'; +import type { TypeDefConsFieldSelection } from './typeDefConsFieldSelection'; + +export interface TypeDefConsSelection { + con: GlobalName; + field?: TypeDefConsFieldSelection; +} diff --git a/src/primer-api/model/typeDefNodeSelection.ts b/src/primer-api/model/typeDefNodeSelection.ts new file mode 100644 index 00000000..7c7f743a --- /dev/null +++ b/src/primer-api/model/typeDefNodeSelection.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { TypeDefNodeSelectionOneOf } from './typeDefNodeSelectionOneOf'; +import type { TypeDefNodeSelectionOneOfThree } from './typeDefNodeSelectionOneOfThree'; + +export type TypeDefNodeSelection = TypeDefNodeSelectionOneOf | TypeDefNodeSelectionOneOfThree; diff --git a/src/primer-api/model/typeDefNodeSelectionOneOf.ts b/src/primer-api/model/typeDefNodeSelectionOneOf.ts new file mode 100644 index 00000000..b7d13007 --- /dev/null +++ b/src/primer-api/model/typeDefNodeSelectionOneOf.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { TypeDefNodeSelectionOneOfTag } from './typeDefNodeSelectionOneOfTag'; + +export type TypeDefNodeSelectionOneOf = { + contents: string; + tag: TypeDefNodeSelectionOneOfTag; +}; diff --git a/src/primer-api/model/typeDefNodeSelectionOneOfTag.ts b/src/primer-api/model/typeDefNodeSelectionOneOfTag.ts new file mode 100644 index 00000000..cf912ffb --- /dev/null +++ b/src/primer-api/model/typeDefNodeSelectionOneOfTag.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ + +export type TypeDefNodeSelectionOneOfTag = typeof TypeDefNodeSelectionOneOfTag[keyof typeof TypeDefNodeSelectionOneOfTag]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const TypeDefNodeSelectionOneOfTag = { + TypeDefParamNodeSelection: 'TypeDefParamNodeSelection', +} as const; diff --git a/src/primer-api/model/typeDefNodeSelectionOneOfThree.ts b/src/primer-api/model/typeDefNodeSelectionOneOfThree.ts new file mode 100644 index 00000000..a1551f1a --- /dev/null +++ b/src/primer-api/model/typeDefNodeSelectionOneOfThree.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { TypeDefConsSelection } from './typeDefConsSelection'; +import type { TypeDefNodeSelectionOneOfThreeTag } from './typeDefNodeSelectionOneOfThreeTag'; + +export type TypeDefNodeSelectionOneOfThree = { + contents: TypeDefConsSelection; + tag: TypeDefNodeSelectionOneOfThreeTag; +}; diff --git a/src/primer-api/model/typeDefNodeSelectionOneOfThreeTag.ts b/src/primer-api/model/typeDefNodeSelectionOneOfThreeTag.ts new file mode 100644 index 00000000..e6aa1e38 --- /dev/null +++ b/src/primer-api/model/typeDefNodeSelectionOneOfThreeTag.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ + +export type TypeDefNodeSelectionOneOfThreeTag = typeof TypeDefNodeSelectionOneOfThreeTag[keyof typeof TypeDefNodeSelectionOneOfThreeTag]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const TypeDefNodeSelectionOneOfThreeTag = { + TypeDefConsNodeSelection: 'TypeDefConsNodeSelection', +} as const; diff --git a/src/primer-api/model/typeDefSelection.ts b/src/primer-api/model/typeDefSelection.ts new file mode 100644 index 00000000..43e2ab3b --- /dev/null +++ b/src/primer-api/model/typeDefSelection.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { GlobalName } from './globalName'; +import type { TypeDefNodeSelection } from './typeDefNodeSelection'; + +export interface TypeDefSelection { + def: GlobalName; + node?: TypeDefNodeSelection; +} diff --git a/src/primer-api/model/valCon.ts b/src/primer-api/model/valCon.ts new file mode 100644 index 00000000..15de0b61 --- /dev/null +++ b/src/primer-api/model/valCon.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.16.0 🍺 + * Do not edit manually. + * Primer backend API + * A backend service implementing a pedagogic functional programming language. + * OpenAPI spec version: 0.7 + */ +import type { Tree } from './tree'; +import type { GlobalName } from './globalName'; + +export interface ValCon { + fields: Tree[]; + name: GlobalName; +} diff --git a/src/util.ts b/src/util.ts index 26b3ba00..9d03ec7b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import deepEqual from "deep-equal"; import { RefObject, useEffect, @@ -53,3 +54,8 @@ export const usePromise = (initial: T, p: Promise): T => { }, [p]); return data; }; + +/** Like `deepEqual`, but also statically checks that types are compatible. + * Makes it easier to avoid mistakes. + */ +export const deepEqualTyped = (a: T, b: T) => deepEqual(a, b);