correspondence.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import * as React from "react";
  2. import * as Mantine from "@mantine/core";
  3. import * as Icons from "@tabler/icons";
  4. import {newOnion} from "./single_model";
  5. import {emptyGraph} from "../d3graph/d3graph";
  6. import {D3GraphUpdater} from "../d3graph/reducers/onion_graph";
  7. import {RountangleParser} from "../../parser/rountangle_parser";
  8. import {Version} from "onion-core";
  9. import {GraphState} from "onion-core";
  10. import {PrimitiveDelta} from "onion-core";
  11. export const undoButtonHelpTextCorr =
  12. `Navigating to a version in the correspondence model,
  13. will also navigate to the embedded concrete and abstract syntax model versions.`;
  14. // Pure function
  15. // Replays all deltas in a version to compute the graph state of that version.
  16. function getGraphState(version: Version, listener?): GraphState {
  17. const graphState = new GraphState();
  18. for (const d of [...version].reverse()) {
  19. if (listener !== undefined) {
  20. graphState.exec(d, listener);
  21. }
  22. else {
  23. graphState.exec(d);
  24. }
  25. }
  26. return graphState;
  27. }
  28. export function newCorrespondence({deltaRegistry, generateUUID, versionRegistry}) {
  29. const {useOnion, ...onion} = newOnion({readonly: true, deltaRegistry, versionRegistry});
  30. // All versions that are part of this correspondence
  31. // We need this to know if some CS/AS version has already been parsed/rendered.
  32. const corrVersions = new Set<Version>([versionRegistry.initialVersion]);
  33. const isPartOfThisCorrespondence = v => corrVersions.has(v);
  34. function useCorrespondence(
  35. {state: {version: csVersion}, reducer: csReducer},
  36. {state: {version: asVersion}, reducer: asReducer},
  37. ) {
  38. // "override" gotoVersion from corr-onion.
  39. const gotoVersion = (version: Version) => {
  40. const csVersion = version.embeddings.get("cs")?.version;
  41. const asVersion = version.embeddings.get("as")?.version;
  42. if (version === versionRegistry.initialVersion) {
  43. reducer.gotoVersion(version);
  44. csReducer.gotoVersion(version);
  45. asReducer.gotoVersion(version);
  46. }
  47. if (csVersion !== undefined && asVersion !== undefined) {
  48. // user clicked on a correspondence model version
  49. reducer.gotoVersion(version);
  50. csReducer.gotoVersion(csVersion);
  51. asReducer.gotoVersion(asVersion);
  52. return;
  53. }
  54. // if (version.reverseEmbeddings.get("cs").some(isPartOfThisCorrespondence)) {
  55. // csReducer.gotoVersion(version);
  56. // return;
  57. // }
  58. // if (version.reverseEmbeddings.get("as").some(isPartOfThisCorrespondence)) {
  59. // asReducer.gotoVersion(version);
  60. // return;
  61. // }
  62. };
  63. const {state, reducer, components} = useOnion(reducer => ({
  64. onUndoClicked: gotoVersion,
  65. onRedoClicked: gotoVersion,
  66. onVersionClicked: gotoVersion,
  67. onMerge: (versions: Version[]) => {
  68. versions.forEach(v => corrVersions.add(v));
  69. reducer.appendVersions(versions);
  70. csReducer.appendVersions(versions.map(v => v.embeddings.get("cs")?.version).filter(v => v!==undefined));
  71. asReducer.appendVersions(versions.map(v => v.embeddings.get("as")?.version).filter(v => v!==undefined));
  72. }
  73. }));
  74. // Helper
  75. const filterCorrParents = (corrParentVersions: Version[]) => {
  76. // when parsing/rendering, if one CORR-parent is a parent of another CORR-parent, then drop this one.
  77. return corrParentVersions.filter(parent => !corrParentVersions.some(child => child !== parent && parent.findDescendant(child) !== undefined));
  78. }
  79. // Reducer
  80. const parseExistingVersion = (csVersion: Version) => {
  81. for (const [csParentVersion, csTx] of csVersion.parents) {
  82. const csDeltas = [...csTx.iterPrimitiveDeltas()];
  83. const description = csTx.description;
  84. // Recursively parse parent versions, if not parsed yet.
  85. if (!csParentVersion.reverseEmbeddings.get("cs")?.some(isPartOfThisCorrespondence)) {
  86. parseExistingVersion(csParentVersion);
  87. }
  88. const corrParentVersions = csParentVersion.getReverseEmbeddings("cs").filter(isPartOfThisCorrespondence);
  89. if (corrParentVersions.length === 0) {
  90. throw new Error("Assertion failed: CS has a parent, but this parent is not yet part of a CORR version.");
  91. }
  92. // Even though parsing is deterministic, it is still possible that the same CS version is part of multiple CORR versions.
  93. // For instance, when merging at the level of CS, and then rendering the change (with a conflict between the CORR-parents), results in a single CS version embedded in multiple CORR versions.
  94. const filteredCorrParentVersions = filterCorrParents(corrParentVersions);
  95. // And if then, there are still multiple CORR versions to choose from, we just pick one:
  96. // (it would be better to ask the user which one, but whatever)
  97. for (const corrParentVersion of filteredCorrParentVersions) {
  98. const asParentVersion = corrParentVersion.getEmbedding("as").version;
  99. if (asParentVersion === undefined) {
  100. throw new Error("Assertion failed: CS's parent is part of a CORR version, but that CORR version does not embed an AS version.");
  101. }
  102. const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(v => getGraphState(v));
  103. const parser = new RountangleParser(deltaRegistry, generateUUID);
  104. const {corrDeltas, asDeltas, csOverrides, asOverrides} = parser.parse(csDeltas, csGS, corrGS, asGS);
  105. const asVersion = asDeltas.length > 0 ? asReducer.createAndGotoNewVersion(asDeltas, "as:"+description, asParentVersion) : asParentVersion;
  106. reducer.appendVersions([csVersion, asVersion]);
  107. const corrVersion = reducer.createAndGotoNewVersion(corrDeltas, "corr:"+description, corrParentVersion,
  108. () => new Map([
  109. ["cs", {version: csVersion, overridings: csOverrides}],
  110. ["as", {version: asVersion, overridings: asOverrides}],
  111. ]));
  112. corrVersions.add(corrVersion);
  113. }
  114. }
  115. };
  116. const renderExistingVersion = async (asVersion: Version, setManualRendererState) => {
  117. for (const [asParentVersion, asTx] of asVersion.parents) {
  118. const asDeltas = [...asTx.iterPrimitiveDeltas()];
  119. const description = asTx.description;
  120. const render = () => {
  121. const corrParentVersions = asParentVersion.getReverseEmbeddings("as").filter(isPartOfThisCorrespondence);
  122. if (corrParentVersions.length === 0) {
  123. throw new Error("Assertion failed: AS has a parent, but this parent is not yet part of a CORR version.");
  124. }
  125. // AS version may be embedded into multiple CORR versions
  126. // If one CORR version is a child of another (parent), then we only render based on the child:
  127. const filteredCorrParentVersions = filterCorrParents(corrParentVersions);
  128. // And if then, there are still multiple CORR versions to choose from, we just pick one:
  129. // (it would be better to ask the user which one, but whatever)
  130. const [corrParentVersion] = filteredCorrParentVersions;
  131. console.log({corrParentVersion, filteredCorrParentVersions, corrParentVersions});
  132. const csParentVersion = corrParentVersion.getEmbedding("cs").version;
  133. if (csParentVersion === undefined) {
  134. console.log("Cannot parse - no parent CS version");
  135. return;
  136. }
  137. const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(v => getGraphState(v));
  138. const parser = new RountangleParser(deltaRegistry, generateUUID);
  139. const {corrDeltas, csDeltas, complete, csOverrides, asOverrides} = parser.render(asDeltas, csGS, corrGS, asGS);
  140. function finishRender({corrDeltas, csDeltas}) {
  141. const csVersion = csDeltas.length > 0 ? csReducer.createAndGotoNewVersion(csDeltas, "cs:"+description, csParentVersion) : csParentVersion;
  142. reducer.appendVersions([csVersion, asVersion]);
  143. const corrVersion = reducer.createAndGotoNewVersion(corrDeltas, "corr:"+description, corrParentVersion,
  144. () => new Map([
  145. ["cs", {version: csVersion, overridings: csOverrides}],
  146. ["as", {version: asVersion, overridings: asOverrides}],
  147. ]));
  148. corrVersions.add(corrVersion);
  149. }
  150. if (complete) {
  151. finishRender({corrDeltas, csDeltas});
  152. return Promise.resolve();
  153. }
  154. else {
  155. function getD3State(version: Version, additionalDeltas: PrimitiveDelta[] = []) {
  156. let graph = emptyGraph;
  157. const setGraph = callback => (graph = callback(graph));
  158. const d3Updater = new D3GraphUpdater(setGraph, 0, 0);
  159. const graphState = getGraphState(version, d3Updater);
  160. for (const d of additionalDeltas) {
  161. graphState.exec(d, d3Updater);
  162. }
  163. return {graph, graphState};
  164. }
  165. const {graph: asGraph, graphState: asGraphState} = getD3State(asVersion);
  166. const {graph: csGraph, graphState: csGraphState} = getD3State(csParentVersion, csDeltas);
  167. const csToAs = new Map();
  168. const asToCs = new Map();
  169. for (const corrNode of corrGS.nodes.values()) {
  170. const csNode = corrNode.outgoing.get("cs");
  171. if (csNode?.type !== "node") {
  172. continue; // corrNode is not a correspondence node
  173. }
  174. const asNode = corrNode.outgoing.get("as");
  175. if (asNode?.type !== "node") {
  176. continue; // corrNode is not a correspondence node
  177. }
  178. csToAs.set(csNode.creation.id, asNode.creation.id);
  179. asToCs.set(asNode.creation.id, csNode.creation.id);
  180. }
  181. return new Promise((resolve: (csDeltas: PrimitiveDelta[]) => void, reject: () => void) => {
  182. setManualRendererState({
  183. asGraph,
  184. csGraph,
  185. asGraphState,
  186. csGraphState,
  187. csToAs,
  188. asToCs,
  189. asDeltasToRender: asDeltas,
  190. done: resolve,
  191. cancel: reject,
  192. });
  193. })
  194. .then((additionalCsDeltas: PrimitiveDelta[]) => {
  195. finishRender({
  196. corrDeltas: corrDeltas.concat(additionalCsDeltas),
  197. csDeltas: csDeltas.concat(additionalCsDeltas),
  198. });
  199. })
  200. .catch()
  201. .finally(() => setManualRendererState(null));
  202. }
  203. }
  204. if (asParentVersion.getReverseEmbeddings("as").filter(isPartOfThisCorrespondence).length === 0) {
  205. return renderExistingVersion(asParentVersion, setManualRendererState).then(render);
  206. }
  207. else {
  208. return render();
  209. }
  210. }
  211. };
  212. // React components
  213. const getParseButton = (dir: "left"|"right" = "right") => {
  214. return <Mantine.Button compact
  215. disabled={(csVersion.reverseEmbeddings.get("cs") || []).some(v => corrVersions.has(v))}
  216. onClick={() => parseExistingVersion(csVersion)}
  217. rightIcon={dir === "right" ? <Icons.IconChevronsRight/>: null}
  218. leftIcon={dir === "left" ? <Icons.IconChevronsLeft/>: null}
  219. >Parse</Mantine.Button>;
  220. };
  221. const getRenderButton = (setManualRendererState, dir: "left"|"right" = "left") => {
  222. return <Mantine.Button compact
  223. disabled={(asVersion.reverseEmbeddings.get("as") || []).some(v => corrVersions.has(v))}
  224. onClick={() => renderExistingVersion(asVersion, setManualRendererState)}
  225. rightIcon={dir === "right" ? <Icons.IconChevronsRight/>: null}
  226. leftIcon={dir === "left" ? <Icons.IconChevronsLeft/>: null}
  227. >Render</Mantine.Button>;
  228. };
  229. const getCaptionWithParseButton = (autoParseState, setAutoParseState, dir: "left"|"right" = "right") => {
  230. const sw = <Mantine.Switch label="Auto"
  231. labelPosition={dir}
  232. checked={autoParseState}
  233. onChange={(event) => setAutoParseState(event.currentTarget.checked)}
  234. />;
  235. const button = getParseButton(dir);
  236. if (dir === "left") {
  237. return <Mantine.Group>{button}{sw}</Mantine.Group>;
  238. }
  239. else {
  240. return <Mantine.Group>{sw}{button}</Mantine.Group>;
  241. }
  242. };
  243. const getCaptionWithRenderButton = (autoRenderState, setAutoRenderState, setManualRendererState, dir: "left"|"right" = "left") => {
  244. const sw = <Mantine.Switch label="Auto"
  245. labelPosition={dir}
  246. checked={autoRenderState}
  247. onChange={(event) => setAutoRenderState(event.currentTarget.checked)}
  248. />
  249. const button = getRenderButton(setManualRendererState, dir);
  250. if (dir === "left") {
  251. return <Mantine.Group>{button}{sw}</Mantine.Group>;
  252. }
  253. else {
  254. return <Mantine.Group>{sw}{button}</Mantine.Group>;
  255. }
  256. };
  257. return {
  258. state,
  259. reducer: {
  260. ... reducer,
  261. parseExistingVersion,
  262. renderExistingVersion,
  263. gotoVersion,
  264. },
  265. components: {
  266. ... components,
  267. getParseButton,
  268. getRenderButton,
  269. getCaptionWithParseButton,
  270. getCaptionWithRenderButton,
  271. }
  272. };
  273. }
  274. return {
  275. ... onion,
  276. useCorrespondence,
  277. };
  278. }