Przeglądaj źródła

(BROKEN: WIP: Implement parser in frontend)

Joeri Exelmans 2 lat temu
rodzic
commit
f155c5a85e
3 zmienionych plików z 297 dodań i 227 usunięć
  1. 280 223
      src/frontend/app.tsx
  2. 14 1
      src/onion/version.ts
  3. 3 3
      src/parser/trivial_parser.test.ts

+ 280 - 223
src/frontend/app.tsx

@@ -1,21 +1,23 @@
-import * as React from "react";
+import { Buffer } from "buffer"; // NodeJS library
 
+import * as React from "react";
 import {Grid, Text, Title, Group, Stack, SimpleGrid, Button, Space, Textarea, Tabs, HoverCard, ActionIcon} from "@mantine/core";
 import {IconPlayerTrackPrev, IconPlayerTrackNext, IconInfoCircle} from "@tabler/icons";
 
-import {d3Types, Graph} from "./graph"
-import {EditableGraph, GraphType, NodeType, LinkType} from "./editable_graph";
 import {GraphDeltaExecutor} from "../onion/delta_executor"; 
-import {D3StateManipulator} from "./d3_state_manipulator";
-
+import {CompositeDelta, CompositeLevel} from "../onion/composite_delta";
+import {Version, initialVersion, VersionRegistry} from "../onion/version";
+import {NodeCreation, NodeDeletion, EdgeCreation, EdgeUpdate} from "../onion/primitive_delta";
 import {PrimitiveValue, UUID} from "../onion/types";
 import {mockUuid} from "../onion/test_helpers";
-import {Version, initialVersion, VersionRegistry} from "../onion/version";
 import {Delta} from "../onion/delta";
-import {CompositeDelta, CompositeLevel} from "../onion/composite_delta";
-import {NodeCreation, NodeDeletion, EdgeCreation, EdgeUpdate} from "../onion/primitive_delta";
-import {RountangleEditor} from "./rountangleEditor/RountangleEditor";
 
+import {TrivialParser} from "../parser/trivial_parser";
+
+import {d3Types, Graph} from "./graph"
+import {EditableGraph, GraphType, NodeType, LinkType} from "./editable_graph";
+import {D3StateManipulator} from "./d3_state_manipulator";
+import {RountangleEditor} from "./rountangleEditor/RountangleEditor";
 import {
   HistoryGraphType,
   DependencyGraphType,
@@ -32,14 +34,12 @@ import {
   addDeltaAndActivate,
 } from "./app_state";
 
-import { Buffer } from "buffer"; // NodeJS library
 
 const emptyGraph: d3Types.d3Graph<any,any> = {
   nodes: [],
   links: [],
 };
 
-
 // "physics" stuff (for graph layout)
 const historyGraphForces = {charge: -600, center: 0.1, link: 2};
 const depGraphForces = {charge: -200, center: 0.1, link: 0.2};
@@ -59,11 +59,11 @@ interface VersionedModelState {
   dependencyGraphL0: DependencyGraphType; // the state of what is displayed in the rightmost panel
 }
 
-interface VersionedModelCallbacks {
-  setGraph: (callback: (GraphType) => GraphType) => void;
-  newVersionHandler: (version: Version) => void;
-  newDeltaHandler: (delta: CompositeDelta) => void;
-}
+// interface VersionedModelCallbacks {
+//   setGraph: (callback: (GraphType) => GraphType) => void;
+//   newVersionHandler: (version: Version) => void;
+//   newDeltaHandler: (delta: CompositeDelta) => void;
+// }
 
 const initialModel: VersionedModelState = {
   version: initialVersion,
@@ -77,8 +77,11 @@ export interface VersionedModelProps {
   title: string;
   generateUUID: () => UUID;
 
-  model: VersionedModelState;
-  callbacks: VersionedModelCallbacks;
+  // model: VersionedModelState;
+  // callbacks: VersionedModelCallbacks;
+
+  state: VersionedModelState;
+  setState: (callback: (VersionedModelState) => VersionedModelState) => void;
 }
 
 class VersionedModel extends React.Component<VersionedModelProps, {}> {
@@ -95,15 +98,16 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
     // this.state = 
 
     // 'Glue' callback
-    // const setGraph = (callback: (GraphType) => GraphType) => {
-    //   this.props.setState(oldState => ({
-    //     graph: callback(oldState.graph),
-    //   }));
-    // };
+    const setGraph = (callback: (GraphType) => GraphType) => {
+      this.props.setState(({graph, ...rest}) => ({
+        graph: callback(graph),
+        ...rest,
+      }));
+    };
 
     this.textarearef = React.createRef();
 
-    this.manipulator = new D3StateManipulator(this.props.callbacks.setGraph);
+    this.manipulator = new D3StateManipulator(setGraph);
     this.graphDeltaExecutor = new GraphDeltaExecutor(this.manipulator);
     this.compositeLevel = new CompositeLevel();
     this.versionRegistry = new VersionRegistry();
@@ -111,171 +115,157 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
 
   render() {
 
-    // // appends a new version to the history graph
-    // // and sets the "current version" to that version.
-    // // PRECONDITION: the version is not yet part of the history graph, but all of its parents are!
-    // const newVersionHandler = (version: Version) => {
-    //   this.setState(prevState => {
-    //     return {
-    //       version,
-    //       historyGraph: setCurrentVersion(appendToHistoryGraph(prevState.historyGraph, version), prevState.version, version),
-    //     };
-    //   });
-    // };
-
-    // // called whenever there's a new delta (as a result of a user edit)
-    // // this function adds the new delta, its dependencies and conflict links to the dep-graph (if it isn't there already),
-    // // and makes sure that the new delta is highlighted as a "current delta".
-    // const newDeltaHandler = (deltaL1: CompositeDelta) => {
-    //   console.log("New delta:", deltaL1);
+    // appends a new version to the history graph
+    // and sets the "current version" to that version.
+    // PRECONDITION: the version is not yet part of the history graph, but all of its parents are!
+    const newVersionHandler = (version: Version) => {
+      this.props.setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => {
+        return {
+          version,
+          historyGraph: setCurrentVersion(appendToHistoryGraph(prevHistoryGraph, version), prevVersion, version),
+          ...rest,
+        };
+      });
+    };
 
-    //   this.setState(prevState => {
-    //     const prevDepGraphL0 = prevState.dependencyGraphL0
-    //     const prevDepGraphL1 = prevState.dependencyGraphL1;
-    //     return {
-    //       dependencyGraphL1: addDeltaAndActivate(prevDepGraphL1, deltaL1),
-    //       dependencyGraphL0: deltaL1.deltas.reduce((prevDepGraphL0, delta) => addDeltaAndActivate(prevDepGraphL0, delta), prevDepGraphL0),
-    //     }
-    //   });
-    // };
+    // called whenever there's a new delta (as a result of a user edit)
+    // this function adds the new delta, its dependencies and conflict links to the dep-graph (if it isn't there already),
+    // and makes sure that the new delta is highlighted as a "current delta".
+    const newDeltaHandler = (deltaL1: CompositeDelta) => {
+      console.log("New delta:", deltaL1);
+
+      this.props.setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => {
+        return {
+          dependencyGraphL1: addDeltaAndActivate(prevDepGraphL1, deltaL1),
+          dependencyGraphL0: deltaL1.deltas.reduce((prevDepGraphL0, delta) => addDeltaAndActivate(prevDepGraphL0, delta), prevDepGraphL0),
+          ...rest,
+        }
+      });
+    };
 
     const historyMouseUpHandler = (event, {x,y}, node: d3Types.d3Node<Version|null> | undefined) => {
-      // if (node !== undefined) {
-      //   // the user clicked on a version -> the clicked version becomes the "current version"
-      //   const versionClicked = node.obj;
-      //   if (versionClicked !== null) {
-      //     if (this.textarearef.current) {
-      //       this.textarearef.current.value += node.id + '\n';
-      //     }
-
-      //     const path = this.state.version.findPathTo(versionClicked);
-      //     // console.log("path to clicked version:", path);
-      //     if (path === undefined) {
-      //       throw new Error("Could not find path to clicked version!");
-      //     }
-
-      //     this.manipulator.x = 0;
-      //     this.manipulator.y = 0;
-
-      //     for (const [linkType, delta] of path) {
-      //       if (linkType === 'p') {
-      //         undo(delta);
-      //       }
-      //       else if (linkType === 'c') {
-      //         redo(delta);
-      //       }
-      //     }
-
-      //     this.setState(prevState => ({
-      //       version: versionClicked,
-      //       historyGraph: setCurrentVersion(prevState.historyGraph, prevState.version, versionClicked),
-      //     }));
-      //   }
-      // }
+      if (node !== undefined) {
+        // the user clicked on a version -> the clicked version becomes the "current version"
+        const versionClicked = node.obj;
+        if (versionClicked !== null) {
+          if (this.textarearef.current) {
+            this.textarearef.current.value += node.id + '\n';
+          }
+
+          const path = this.props.state.version.findPathTo(versionClicked);
+          // console.log("path to clicked version:", path);
+          if (path === undefined) {
+            throw new Error("Could not find path to clicked version!");
+          }
+
+          this.manipulator.x = 0;
+          this.manipulator.y = 0;
+
+          for (const [linkType, delta] of path) {
+            if (linkType === 'p') {
+              undo(delta);
+            }
+            else if (linkType === 'c') {
+              redo(delta);
+            }
+          }
+
+          this.props.setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+            version: versionClicked,
+            historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, versionClicked),
+            ...rest,
+          }));
+        }
+      }
     }
 
     const undo = deltaToUndo => {
-      // this.manipulator.x = 0;
-      // this.manipulator.y = 0;
-      // this.graphDeltaExecutor.unexec(deltaToUndo);
-      // this.setState(prevState => ({
-      //   dependencyGraphL1: setDeltaInactive(prevState.dependencyGraphL1, deltaToUndo),
-      //   dependencyGraphL0: deltaToUndo.deltas.reduce((prevDepGraphL0, delta) => setDeltaInactive(prevDepGraphL0, delta), prevState.dependencyGraphL0),
-      // }));
+      this.manipulator.x = 0;
+      this.manipulator.y = 0;
+      this.graphDeltaExecutor.unexec(deltaToUndo);
+      this.props.setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
+        dependencyGraphL1: setDeltaInactive(prevDepGraphL1, deltaToUndo),
+        dependencyGraphL0: deltaToUndo.deltas.reduce((prevDepGraphL0, delta) => setDeltaInactive(prevDepGraphL0, delta), prevDepGraphL0),
+        ...rest,
+      }));
     };
     const redo = deltaToRedo => {
-      // this.manipulator.x = 0;
-      // this.manipulator.y = 0;
-      // this.graphDeltaExecutor.exec(deltaToRedo);
-      // this.setState(prevState => ({
-      //   dependencyGraphL1: setDeltaActive(prevState.dependencyGraphL1, deltaToRedo),
-      //   dependencyGraphL0: deltaToRedo.deltas.reduce((prevDepGraphL0, delta) => setDeltaActive(prevDepGraphL0, delta), prevState.dependencyGraphL0),
-      // }));
+      this.manipulator.x = 0;
+      this.manipulator.y = 0;
+      this.graphDeltaExecutor.exec(deltaToRedo);
+      this.props.setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
+        dependencyGraphL1: setDeltaActive(prevDepGraphL1, deltaToRedo),
+        dependencyGraphL0: deltaToRedo.deltas.reduce((prevDepGraphL0, delta) => setDeltaActive(prevDepGraphL0, delta), prevDepGraphL0),
+        ...rest,
+      }));
     };
 
     const onUndoClicked = (parentVersion, deltaToUndo) => {
-      // undo(deltaToUndo);
-      // this.setState(prevState => ({
-      //   version: parentVersion,
-      //   historyGraph: setCurrentVersion(prevState.historyGraph, prevState.version, parentVersion),
-      // }));
+      undo(deltaToUndo);
+      this.props.setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+        version: parentVersion,
+        historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, parentVersion),
+        ...rest,
+      }));
     };
     const onRedoClicked = (childVersion, deltaToRedo) => {
-      // redo(deltaToRedo);
-      // this.setState(prevState => ({
-      //   version: childVersion,
-      //   historyGraph: setCurrentVersion(prevState.historyGraph, prevState.version, childVersion),
-      // }));
+      redo(deltaToRedo);
+      this.props.setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+        version: childVersion,
+        historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, childVersion),
+        ...rest,
+      }));
     };
 
-    const undoButtons = this.props.model.version.parents.map(([parentVersion,deltaToUndo]) => {
-      return (
-        <div key={fullVersionId(parentVersion)}>
-          <Button fullWidth={true} compact={true} leftIcon={<IconPlayerTrackPrev size={18}/>} onClick={onUndoClicked.bind(null, parentVersion,deltaToUndo)}>
-            UNDO {deltaToUndo.getDescription()}
-          </Button>
-          <Space h="xs"/>
-        </div>
-      );
-    });
-    const redoButtons = this.props.model.version.children.map(([childVersion,deltaToRedo]) => {
-      return (
-        <div key={fullVersionId(childVersion)}>
-          <Button style={{width: "100%"}} compact={true} rightIcon={<IconPlayerTrackNext size={18}/>} onClick={onRedoClicked.bind(null, childVersion,deltaToRedo)}>
-            REDO {deltaToRedo.getDescription()}
-          </Button>
-          <Space h="xs"/>
-        </div>
-      );
-    });
 
     const onMergeClicked = () => {
-      // const text = this.textarearef.current?.value;
-      // if (text) {
-      //   const lines = text.split('\n');
-
-      //   const versionIds: Buffer[] = lines.map(s => {
-      //     try {
-      //       return Buffer.from(s, 'base64');
-      //     } catch (e) {
-      //       return Buffer.alloc(0);
-      //     }
-      //   }).filter(buf => buf.length !== 0);
-
-      //   let versionsToMerge;
-      //   try {
-      //     versionsToMerge = versionIds.map((buf: Buffer) => {
-      //       return this.versionRegistry.lookup(buf);
-      //     });
-      //   } catch (e) {
-      //     alert("Input error:" + e.toString());
-      //     return;
-      //   }
-
-      //   const mergedVersions = this.versionRegistry.merge(versionsToMerge);
-
-      //   console.log("mergedVersions:", mergedVersions);
-
-      //   const add = v => {
-      //     this.setState(prevState => {
-      //       return {
-      //         historyGraph: appendToHistoryGraph(prevState.historyGraph, v),
-      //       };
-      //     });
-      //   };
-
-      //   // add all mergedVersions (and their parents) to history graph:
-      //   // this may be overkill (and not so scalable), but it does the job :)
-      //   mergedVersions.forEach(v => {
-      //     const addParents = v => {
-      //       v.parents.forEach(([v]) => {
-      //         addParents(v);
-      //       });
-      //       add(v);
-      //     }
-      //     addParents(v);
-      //   });
-      // }
+      const text = this.textarearef.current?.value;
+      if (text) {
+        const lines = text.split('\n');
+
+        const versionIds: Buffer[] = lines.map(s => {
+          try {
+            return Buffer.from(s, 'base64');
+          } catch (e) {
+            return Buffer.alloc(0);
+          }
+        }).filter(buf => buf.length !== 0);
+
+        let versionsToMerge;
+        try {
+          versionsToMerge = versionIds.map((buf: Buffer) => {
+            return this.versionRegistry.lookup(buf);
+          });
+        } catch (e) {
+          alert("Input error:" + e.toString());
+          return;
+        }
+
+        const mergedVersions = this.versionRegistry.merge(versionsToMerge);
+
+        console.log("mergedVersions:", mergedVersions);
+
+        const add = v => {
+          this.props.setState(({historyGraph: prevHistoryGraph, ...rest}) => {
+            return {
+              historyGraph: appendToHistoryGraph(prevHistoryGraph, v),
+              ...rest,
+            };
+          });
+        };
+
+        // add all mergedVersions (and their parents) to history graph:
+        // this may be overkill (and not so scalable), but it does the job :)
+        mergedVersions.forEach(v => {
+          const addParents = v => {
+            v.parents.forEach(([v]) => {
+              addParents(v);
+            });
+            add(v);
+          }
+          addParents(v);
+        });
+      }
     }
 
     function makeInfoHoverCardIcon(contents) {
@@ -311,14 +301,14 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
             <Text>Mouse wheel: Zoom.</Text>
           </>)}
           <EditableGraph
-            graph={this.props.model.graph}
+            graph={this.props.state.graph}
             graphDeltaExecutor={this.graphDeltaExecutor}
             forces={historyGraphForces}
             generateUUID={this.props.generateUUID}
             setNextNodePosition={(x,y)=>{this.manipulator.x = x; this.manipulator.y = y;}}
-            newVersionHandler={this.props.callbacks.newVersionHandler}
-            newDeltaHandler={this.props.callbacks.newDeltaHandler}
-            version={this.props.model.version}
+            newVersionHandler={newVersionHandler}
+            newDeltaHandler={newDeltaHandler}
+            version={this.props.state.version}
             compositeLvl={this.compositeLevel}
             versionRegistry={this.versionRegistry}
           />
@@ -329,14 +319,14 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
             <Text>New Rountangle by Alt + Click</Text>
           )}
           <RountangleEditor
-              graph={this.props.model.graph}
+              graph={this.props.state.graph}
               graphDeltaExecutor={this.graphDeltaExecutor}
               forces={historyGraphForces}
               generateUUID={this.props.generateUUID}
               setNextNodePosition={(x,y)=>{this.manipulator.x = x; this.manipulator.y = y;}}
-              newVersionHandler={this.props.callbacks.newVersionHandler}
-              newDeltaHandler={this.props.callbacks.newDeltaHandler}
-              version={this.props.model.version}
+              newVersionHandler={newVersionHandler}
+              newDeltaHandler={newDeltaHandler}
+              version={this.props.state.version}
               compositeLvl={this.compositeLevel}
               versionRegistry={this.versionRegistry}
           />
@@ -346,14 +336,14 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
           {makeInfoHoverCardIcon(
             <Text>Active deltas are bold.</Text>
           )}
-          <Graph graph={this.props.model.dependencyGraphL1} forces={depGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={()=>{}} />
+          <Graph graph={this.props.state.dependencyGraphL1} forces={depGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={()=>{}} />
         </Tabs.Panel>
 
         <Tabs.Panel value="dependencyL0">
           {makeInfoHoverCardIcon(
             <Text>Active deltas are bold.</Text>
           )}
-          <Graph graph={this.props.model.dependencyGraphL0} forces={depGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={()=>{}} />
+          <Graph graph={this.props.state.dependencyGraphL0} forces={depGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={()=>{}} />
         </Tabs.Panel>
 
         <Tabs.Panel value="history">
@@ -361,20 +351,7 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
             <Text>All links are parent links.</Text>
             <Text>Right or middle mouse button: Load version.</Text>
           </>)}
-          <Graph graph={this.props.model.historyGraph} forces={historyGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={historyMouseUpHandler} />
-          <SimpleGrid cols={2}>
-            <div>
-              {undoButtons}
-            </div>
-            <div>
-              {redoButtons}
-            </div>
-          </SimpleGrid>
-          <Space h="xs"/>
-          <Textarea size="xs" label="Versions to merge:" ref={this.textarearef} autosize></Textarea>
-          <Button onClick={() => {if (this.textarearef.current) this.textarearef.current.value = "";}}>CLEAR</Button>
-          &nbsp;
-          <Button onClick={onMergeClicked}>MERGE</Button>
+          <Graph graph={this.props.state.historyGraph} forces={historyGraphForces} mouseDownHandler={()=>{}} mouseUpHandler={historyMouseUpHandler} />
         </Tabs.Panel>
       </Tabs>
     );
@@ -386,6 +363,12 @@ class VersionedModel extends React.Component<VersionedModelProps, {}> {
           {makeConcreteSyntaxTabs("editor")}
           {makeConcreteSyntaxTabs("state")}
         </Stack>
+
+        <Textarea size="xs" label="Versions to merge:" ref={this.textarearef}
+          autosize placeholder="Right-click on version in History Graph to add it to this textbox"></Textarea>
+        <Button onClick={() => {if (this.textarearef.current) this.textarearef.current.value = "";}}>CLEAR</Button>
+        &nbsp;
+        <Button onClick={onMergeClicked}>MERGE</Button>
       </>
     );
   }
@@ -397,36 +380,110 @@ interface AppState {
   as: VersionedModelState;
 }
 
-export class App extends React.Component<{}, AppState> {
-  generateUUID = mockUuid();
+// export class App extends React.Component<{}, AppState> {
+//   generateUUID = mockUuid();
 
-  constructor(props) {
-    super(props);
-    this.state = {
-      cs: initialModel,
-      corr: initialModel,
-      as: initialModel,
-    };
-  }
+//   constructor(props) {
+//     super(props);
+//     this.state = {
+//       cs: initialModel,
+//       corr: initialModel,
+//       as: initialModel,
+//     };
+//   }
 
-  render() {
+//   render() {
 
-    return (<>
-      <Grid grow>
-        <Grid.Col span={1}>
-          <VersionedModel title="Concrete Syntax" generateUUID={this.generateUUID} model={this.state.cs} callbacks={{
-            setGraph: (callback: (GraphType) => GraphType) => {
-              this.setState(oldState => ({
-                cs: {
-                  graph: callback(oldState.cs.graph),
-                },
-              }));
-            },
-          }} />
-        </Grid.Col>
-      </Grid>
-    </>);
-  }
+const generateUUID = mockUuid();
+
+export function App() {
+  const [cs, setCs] = React.useState<VersionedModelState>(initialModel);
+  const [corr, setCorr] = React.useState<VersionedModelState>(initialModel);
+  const [as, setAs] = React.useState<VersionedModelState>(initialModel);
+
+  const [corrVersion, setCorrVersion] = React.useState<Version>(initialVersion);
+
+  const handleNewCsVersion = (cs: Version) => {
+    
+  };
+
+  const csCallbacks = {
+    // setGraph: (callback: (GraphType) => GraphType) => {
+
+    //   this.setState(({cs: {graph, ...rest}}) => ({
+    //     cs: {
+    //       graph: callback(graph),
+    //       ...rest,
+    //     },
+    //   }));
+    // },
+    // newVersionHandler: (newVersion: Version) => {
+    //   this.setState(({cs: {version, historyGraph, ...rest}}) => ({
+    //     cs: {
+    //       version: newVersion,
+    //       historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), version, newVersion),
+    //       ...rest,
+    //     },
+    //   }));
+    // },
+    // newDeltaHandler: (deltaL1:CompositeDelta) => {
+    //   this.setState(({cs: {dependencyGraphL0, dependencyGraphL1, ...rest}}) => {
+    //     return {
+    //       cs: {
+    //         dependencyGraphL1: addDeltaAndActivate(dependencyGraphL1, deltaL1),
+    //         dependencyGraphL0: deltaL1.deltas.reduce((dependencyGraphL0, delta) => addDeltaAndActivate(dependencyGraphL0, delta), dependencyGraphL0),
+    //         ...rest,
+    //       },
+    //     };
+    //   });
+    // },
+  };
+
+
+  const undoButtons = corrVersion.parents.map(([parentVersion,deltaToUndo]) => {
+    return (
+      <div key={fullVersionId(parentVersion)}>
+        <Button fullWidth={true} compact={true} leftIcon={<IconPlayerTrackPrev size={18}/>} onClick={onUndoClicked.bind(null, parentVersion,deltaToUndo)}>
+          UNDO {deltaToUndo.getDescription()}
+        </Button>
+        <Space h="xs"/>
+      </div>
+    );
+  });
+  const redoButtons = corrVersion.children.map(([childVersion,deltaToRedo]) => {
+    return (
+      <div key={fullVersionId(childVersion)}>
+        <Button style={{width: "100%"}} compact={true} rightIcon={<IconPlayerTrackNext size={18}/>} onClick={onRedoClicked.bind(null, childVersion,deltaToRedo)}>
+          REDO {deltaToRedo.getDescription()}
+        </Button>
+        <Space h="xs"/>
+      </div>
+    );
+  });
+
+  return (<>
+    <Grid grow>
+      <Grid.Col span={1}>
+        <VersionedModel title="Concrete Syntax" generateUUID={generateUUID} version={corrVersion.getEmbeddedOrThrow("cs").embedded} state={cs} setState={setCs} />
+      </Grid.Col>
+      <Grid.Col span={1}>
+        <VersionedModel title="Correspondence" generateUUID={generateUUID} version={corrVersion} state={corr} setState={setCorr} />
+
+        <SimpleGrid cols={2}>
+          <div>
+            {undoButtons}
+          </div>
+          <div>
+            {redoButtons}
+          </div>
+        </SimpleGrid>
+
+      </Grid.Col>
+      <Grid.Col span={1}>
+        <VersionedModel title="Abstract Syntax" generateUUID={generateUUID} version={corrVersion.getEmbeddedOrThrow("as").embedded} state={as} setState={setAs} />
+      </Grid.Col>
+    </Grid>
+  </>);
 }
 
 // export function App() {

+ 14 - 1
src/onion/version.ts

@@ -117,6 +117,14 @@ export class Version {
   getEmbedded(id: string): EmbeddedVersion | undefined {
     return this.embeddings.get(id);
   }
+
+  getEmbeddedOrThrow(id: string): EmbeddedVersion {
+    const result = this.embeddings.get(id);
+    if (result === undefined) {
+      throw new Error("No such embedding: " + id);
+    }
+    return result;
+  }
 }
 
 const initialHash = Buffer.alloc(32); // all zeros
@@ -134,7 +142,12 @@ export class InitialVersion extends Version {
   }
 
   // override: we pretend that the initial version has all other empty models (also represented by the initial version) embedded into it.
-  getEmbedded(id: string): EmbeddedVersion | undefined {
+  getEmbedded(id: string): EmbeddedVersion| undefined {
+    return InitialVersion.embedded;
+  }
+
+  // override: we pretend that the initial version has all other empty models (also represented by the initial version) embedded into it.
+  getEmbeddedOrThrow(id: string): EmbeddedVersion {
     return InitialVersion.embedded;
   }
 }

+ 3 - 3
src/parser/trivial_parser.test.ts

@@ -19,9 +19,9 @@ import {assert} from "../util/assert";
 describe("Trivial Parser", () => {
   it("Parse CS creation and deletion", () => {
     const registry = new VersionRegistry();
-    const csLvl = new CompositeLevel();
-    const asLvl = new CompositeLevel();
-    const corrLvl = new CompositeLevel();
+    const csLvl = new CompositeLevel(); // L1
+    const asLvl = new CompositeLevel(); // L1
+    const corrLvl = new CompositeLevel(); // L1
     const getUuid = mockUuid();
     const parser = new TrivialParser(getUuid, registry, csLvl, asLvl, corrLvl);