@dxos/plugin-sheet 0.6.12-main.ed7cda7 → 0.6.12-staging.e11e696

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/lib/browser/{SheetContainer-V4GCCZTX.mjs → SheetContainer-LG77O4RM.mjs} +14 -13
  2. package/dist/lib/browser/SheetContainer-LG77O4RM.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-U2JHW3L6.mjs → chunk-CHQAW4F4.mjs} +206 -53
  4. package/dist/lib/browser/chunk-CHQAW4F4.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-6ZMQVB4Z.mjs → chunk-GSV5QNLD.mjs} +220 -177
  6. package/dist/lib/browser/chunk-GSV5QNLD.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-T3NJFTD4.mjs → chunk-WZMOZKQZ.mjs} +2 -2
  8. package/dist/lib/browser/{chunk-T3NJFTD4.mjs.map → chunk-WZMOZKQZ.mjs.map} +3 -3
  9. package/dist/lib/browser/graph-M4IQ76QX.mjs +33 -0
  10. package/dist/lib/browser/index.mjs +45 -21
  11. package/dist/lib/browser/index.mjs.map +4 -4
  12. package/dist/lib/browser/meta.json +1 -1
  13. package/dist/lib/browser/types.mjs +1 -1
  14. package/dist/lib/node/{SheetContainer-3ZY7MPWJ.cjs → SheetContainer-OZ7DHH4L.cjs} +21 -20
  15. package/dist/lib/node/SheetContainer-OZ7DHH4L.cjs.map +7 -0
  16. package/dist/lib/node/{chunk-OTTD7FBK.cjs → chunk-5FTFZL5W.cjs} +224 -70
  17. package/dist/lib/node/chunk-5FTFZL5W.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-DD6FIXWC.cjs → chunk-5XPK2V4A.cjs} +222 -175
  19. package/dist/lib/node/chunk-5XPK2V4A.cjs.map +7 -0
  20. package/dist/lib/node/{chunk-Q3HBHPRL.cjs → chunk-AOP42UAA.cjs} +5 -5
  21. package/dist/lib/node/{chunk-Q3HBHPRL.cjs.map → chunk-AOP42UAA.cjs.map} +3 -3
  22. package/dist/lib/node/graph-Q3N2X26H.cjs +55 -0
  23. package/dist/lib/node/graph-Q3N2X26H.cjs.map +7 -0
  24. package/dist/lib/node/index.cjs +51 -30
  25. package/dist/lib/node/index.cjs.map +4 -4
  26. package/dist/lib/node/meta.json +1 -1
  27. package/dist/lib/node/types.cjs +8 -8
  28. package/dist/lib/node/types.cjs.map +1 -1
  29. package/dist/lib/node-esm/{SheetContainer-PXSJX6XK.mjs → SheetContainer-4XS2G25Z.mjs} +14 -13
  30. package/dist/lib/node-esm/SheetContainer-4XS2G25Z.mjs.map +7 -0
  31. package/dist/lib/node-esm/{chunk-D6KU5MI7.mjs → chunk-5WPZCXNS.mjs} +220 -177
  32. package/dist/lib/node-esm/chunk-5WPZCXNS.mjs.map +7 -0
  33. package/dist/lib/node-esm/{chunk-7HVSOTGA.mjs → chunk-KK3XL37M.mjs} +206 -53
  34. package/dist/lib/node-esm/chunk-KK3XL37M.mjs.map +7 -0
  35. package/dist/lib/node-esm/{chunk-BMNA27EX.mjs → chunk-RR2AO4SM.mjs} +2 -2
  36. package/dist/lib/node-esm/{chunk-BMNA27EX.mjs.map → chunk-RR2AO4SM.mjs.map} +3 -3
  37. package/dist/lib/node-esm/graph-SMPUMOV2.mjs +34 -0
  38. package/dist/lib/node-esm/index.mjs +45 -21
  39. package/dist/lib/node-esm/index.mjs.map +4 -4
  40. package/dist/lib/node-esm/meta.json +1 -1
  41. package/dist/lib/node-esm/types.mjs +1 -1
  42. package/dist/types/src/SheetPlugin.d.ts.map +1 -1
  43. package/dist/types/src/components/CellEditor/CellEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/CellEditor/extension.d.ts.map +1 -1
  45. package/dist/types/src/components/GridSheet/GridSheet.d.ts +3 -3
  46. package/dist/types/src/components/GridSheet/GridSheet.d.ts.map +1 -1
  47. package/dist/types/src/components/GridSheet/GridSheet.stories.d.ts +1 -1
  48. package/dist/types/src/components/GridSheet/GridSheet.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -1
  50. package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/Sheet/sheet-context.d.ts +3 -3
  52. package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -1
  53. package/dist/types/src/components/SheetContainer.d.ts +1 -1
  54. package/dist/types/src/components/SheetContainer.d.ts.map +1 -1
  55. package/dist/types/src/components/index.d.ts +1 -1
  56. package/dist/types/src/components/index.d.ts.map +1 -1
  57. package/dist/types/src/defs/types.d.ts.map +1 -1
  58. package/dist/types/src/defs/util.d.ts +1 -1
  59. package/dist/types/src/defs/util.d.ts.map +1 -1
  60. package/dist/types/src/extensions/compute.d.ts +3 -2
  61. package/dist/types/src/extensions/compute.d.ts.map +1 -1
  62. package/dist/types/src/extensions/compute.stories.d.ts.map +1 -1
  63. package/dist/types/src/graph/compute-graph-registry.d.ts +34 -0
  64. package/dist/types/src/graph/compute-graph-registry.d.ts.map +1 -0
  65. package/dist/types/src/graph/compute-graph.d.ts +17 -34
  66. package/dist/types/src/graph/compute-graph.d.ts.map +1 -1
  67. package/dist/types/src/graph/compute-graph.stories.d.ts.map +1 -1
  68. package/dist/types/src/graph/compute-graph.test.d.ts +2 -0
  69. package/dist/types/src/graph/compute-graph.test.d.ts.map +1 -0
  70. package/dist/types/src/graph/compute-node.d.ts +9 -2
  71. package/dist/types/src/graph/compute-node.d.ts.map +1 -1
  72. package/dist/types/src/graph/{async-function.d.ts → functions/async-function.d.ts} +13 -4
  73. package/dist/types/src/graph/functions/async-function.d.ts.map +1 -0
  74. package/dist/types/src/graph/functions/edge-function.d.ts +21 -0
  75. package/dist/types/src/graph/functions/edge-function.d.ts.map +1 -0
  76. package/dist/types/src/graph/functions/function-defs.d.ts.map +1 -0
  77. package/dist/types/src/graph/functions/index.d.ts +4 -0
  78. package/dist/types/src/graph/functions/index.d.ts.map +1 -0
  79. package/dist/types/src/graph/index.d.ts +2 -1
  80. package/dist/types/src/graph/index.d.ts.map +1 -1
  81. package/dist/types/src/graph/testing/index.d.ts +3 -0
  82. package/dist/types/src/graph/testing/index.d.ts.map +1 -0
  83. package/dist/types/src/graph/testing/test-builder.d.ts +15 -0
  84. package/dist/types/src/graph/testing/test-builder.d.ts.map +1 -0
  85. package/dist/types/src/graph/testing/test-plugin.d.ts +36 -0
  86. package/dist/types/src/graph/testing/test-plugin.d.ts.map +1 -0
  87. package/dist/types/src/hooks/useComputeGraph.d.ts.map +1 -1
  88. package/dist/types/src/hooks/useSheetModel.d.ts +2 -2
  89. package/dist/types/src/hooks/useSheetModel.d.ts.map +1 -1
  90. package/dist/types/src/model/sheet-model.d.ts +3 -3
  91. package/dist/types/src/model/sheet-model.d.ts.map +1 -1
  92. package/dist/types/src/model/sheet-model.test.d.ts +2 -0
  93. package/dist/types/src/model/sheet-model.test.d.ts.map +1 -0
  94. package/dist/types/src/testing/testing.d.ts +4 -5
  95. package/dist/types/src/testing/testing.d.ts.map +1 -1
  96. package/dist/types/src/types.d.ts +4 -3
  97. package/dist/types/src/types.d.ts.map +1 -1
  98. package/package.json +40 -39
  99. package/src/SheetPlugin.tsx +19 -15
  100. package/src/components/CellEditor/CellEditor.stories.tsx +2 -3
  101. package/src/components/CellEditor/extension.test.ts +0 -1
  102. package/src/components/CellEditor/extension.ts +4 -3
  103. package/src/components/GridSheet/GridSheet.stories.tsx +5 -4
  104. package/src/components/GridSheet/GridSheet.tsx +4 -4
  105. package/src/components/Sheet/Sheet.stories.tsx +21 -20
  106. package/src/components/Sheet/Sheet.tsx +30 -14
  107. package/src/components/Sheet/sheet-context.tsx +4 -4
  108. package/src/components/SheetContainer.tsx +13 -15
  109. package/src/defs/types.ts +1 -0
  110. package/src/defs/util.ts +19 -3
  111. package/src/extensions/compute.stories.tsx +20 -20
  112. package/src/extensions/compute.ts +91 -42
  113. package/src/graph/compute-graph-registry.ts +90 -0
  114. package/src/graph/compute-graph.stories.tsx +4 -3
  115. package/src/graph/compute-graph.test.ts +87 -0
  116. package/src/graph/compute-graph.ts +73 -121
  117. package/src/graph/compute-node.ts +17 -5
  118. package/src/graph/{async-function.ts → functions/async-function.ts} +23 -15
  119. package/src/graph/{edge-function.ts → functions/edge-function.ts} +14 -13
  120. package/src/graph/functions/index.ts +7 -0
  121. package/src/graph/hyperformula.test.ts +1 -2
  122. package/src/graph/index.ts +2 -1
  123. package/src/graph/testing/index.ts +6 -0
  124. package/src/graph/testing/test-builder.ts +54 -0
  125. package/src/graph/{custom-function.ts → testing/test-plugin.ts} +43 -9
  126. package/src/hooks/hooks.stories.tsx +3 -3
  127. package/src/hooks/useComputeGraph.ts +9 -1
  128. package/src/hooks/useSheetModel.ts +4 -7
  129. package/src/model/sheet-model.test.ts +59 -0
  130. package/src/model/sheet-model.ts +47 -30
  131. package/src/testing/testing.tsx +17 -15
  132. package/src/types.ts +3 -3
  133. package/dist/lib/browser/SheetContainer-V4GCCZTX.mjs.map +0 -7
  134. package/dist/lib/browser/chunk-6ZMQVB4Z.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-U2JHW3L6.mjs.map +0 -7
  136. package/dist/lib/browser/graph-T27BOBOV.mjs +0 -21
  137. package/dist/lib/node/SheetContainer-3ZY7MPWJ.cjs.map +0 -7
  138. package/dist/lib/node/chunk-DD6FIXWC.cjs.map +0 -7
  139. package/dist/lib/node/chunk-OTTD7FBK.cjs.map +0 -7
  140. package/dist/lib/node/graph-SPKGX7W4.cjs +0 -43
  141. package/dist/lib/node/graph-SPKGX7W4.cjs.map +0 -7
  142. package/dist/lib/node-esm/SheetContainer-PXSJX6XK.mjs.map +0 -7
  143. package/dist/lib/node-esm/chunk-7HVSOTGA.mjs.map +0 -7
  144. package/dist/lib/node-esm/chunk-D6KU5MI7.mjs.map +0 -7
  145. package/dist/lib/node-esm/graph-U67IO4UC.mjs +0 -22
  146. package/dist/types/src/graph/async-function.d.ts.map +0 -1
  147. package/dist/types/src/graph/compute-graph.browser.test.d.ts +0 -2
  148. package/dist/types/src/graph/compute-graph.browser.test.d.ts.map +0 -1
  149. package/dist/types/src/graph/custom-function.d.ts +0 -21
  150. package/dist/types/src/graph/custom-function.d.ts.map +0 -1
  151. package/dist/types/src/graph/edge-function.d.ts +0 -20
  152. package/dist/types/src/graph/edge-function.d.ts.map +0 -1
  153. package/dist/types/src/graph/function-defs.d.ts.map +0 -1
  154. package/src/graph/compute-graph.browser.test.ts +0 -104
  155. /package/dist/lib/browser/{graph-T27BOBOV.mjs.map → graph-M4IQ76QX.mjs.map} +0 -0
  156. /package/dist/lib/node-esm/{graph-U67IO4UC.mjs.map → graph-SMPUMOV2.mjs.map} +0 -0
  157. /package/dist/types/src/graph/{function-defs.d.ts → functions/function-defs.d.ts} +0 -0
  158. /package/src/graph/{function-defs.ts → functions/function-defs.ts} +0 -0
package/src/defs/util.ts CHANGED
@@ -5,7 +5,15 @@
5
5
  import { randomBytes } from '@dxos/crypto';
6
6
  import { create } from '@dxos/echo-schema';
7
7
 
8
- import { type CellAddress, type CellRange, DEFAULT_COLUMNS, DEFAULT_ROWS, MAX_COLUMNS, MAX_ROWS } from './types';
8
+ import {
9
+ addressFromA1Notation,
10
+ type CellAddress,
11
+ type CellRange,
12
+ DEFAULT_COLUMNS,
13
+ DEFAULT_ROWS,
14
+ MAX_COLUMNS,
15
+ MAX_ROWS,
16
+ } from './types';
9
17
  import { type CreateSheetOptions, type SheetSize, SheetType } from '../types';
10
18
 
11
19
  // TODO(burdon): Factor out from dxos/protocols to new common package.
@@ -52,9 +60,9 @@ export const initialize = (
52
60
  }
53
61
  };
54
62
 
55
- export const createSheet = ({ title, ...size }: CreateSheetOptions = {}): SheetType => {
63
+ export const createSheet = ({ name, cells, ...size }: CreateSheetOptions = {}): SheetType => {
56
64
  const sheet = create(SheetType, {
57
- title,
65
+ name,
58
66
  cells: {},
59
67
  rows: [],
60
68
  columns: [],
@@ -64,6 +72,14 @@ export const createSheet = ({ title, ...size }: CreateSheetOptions = {}): SheetT
64
72
  });
65
73
 
66
74
  initialize(sheet, size);
75
+
76
+ if (cells) {
77
+ Object.entries(cells).forEach(([key, { value }]) => {
78
+ const idx = addressToIndex(sheet, addressFromA1Notation(key));
79
+ sheet.cells[idx] = { value };
80
+ });
81
+ }
82
+
67
83
  return sheet;
68
84
  };
69
85
 
@@ -3,8 +3,9 @@
3
3
  //
4
4
 
5
5
  import '@dxos-theme';
6
- import React, { useEffect, useState } from 'react';
6
+ import React, { useEffect, useMemo } from 'react';
7
7
 
8
+ import { PublicKey } from '@dxos/keys';
8
9
  import { useSpace } from '@dxos/react-client/echo';
9
10
  import { withClientProvider } from '@dxos/react-client/testing';
10
11
  import { useThemeContext } from '@dxos/react-ui';
@@ -13,16 +14,16 @@ import {
13
14
  createMarkdownExtensions,
14
15
  createThemeExtensions,
15
16
  decorateMarkdown,
17
+ documentId,
16
18
  useTextEditor,
17
19
  } from '@dxos/react-ui-editor';
18
20
  import { withTheme, withLayout } from '@dxos/storybook-utils';
19
21
  import { nonNullable } from '@dxos/util';
20
22
 
21
- import { compute } from './compute';
23
+ import { compute, computeGraphFacet } from './compute';
22
24
  import { Sheet } from '../components';
23
- import { type ComputeNode } from '../graph';
24
25
  import { useComputeGraph, useSheetModel } from '../hooks';
25
- import { useTestSheet, withGraphDecorator } from '../testing';
26
+ import { useTestSheet, withComputeGraphDecorator } from '../testing';
26
27
  import { SheetType } from '../types';
27
28
 
28
29
  const str = (...lines: string[]) => lines.join('\n');
@@ -34,20 +35,15 @@ type EditorProps = {
34
35
  // TODO(burdon): Implement named expressions.
35
36
  // https://hyperformula.handsontable.com/guide/cell-references.html
36
37
 
37
- const DOC_NAME = 'Test Doc';
38
+ // TODO(burdon): Inline Adobe eCharts.
39
+
38
40
  const SHEET_NAME = 'Test Sheet';
39
41
 
40
42
  const Editor = ({ text }: EditorProps) => {
43
+ const id = useMemo(() => PublicKey.random(), []);
41
44
  const { themeMode } = useThemeContext();
42
45
  const space = useSpace();
43
- const graph = useComputeGraph(space);
44
- const [node, setNode] = useState<ComputeNode>();
45
- // TODO(burdon): Virtualize SheetModel.
46
- useEffect(() => {
47
- if (graph) {
48
- setNode(graph.getOrCreateNode(DOC_NAME));
49
- }
50
- }, [graph]);
46
+ const computeGraph = useComputeGraph(space);
51
47
  const { parentRef, focusAttributes } = useTextEditor(
52
48
  () => ({
53
49
  initialValue: text,
@@ -55,11 +51,13 @@ const Editor = ({ text }: EditorProps) => {
55
51
  createBasicExtensions(),
56
52
  createMarkdownExtensions({ themeMode }),
57
53
  createThemeExtensions({ themeMode, syntaxHighlighting: true }),
58
- node && compute(node),
54
+ documentId.of(id.toHex()),
55
+ computeGraph && computeGraphFacet.of(computeGraph),
56
+ compute(),
59
57
  decorateMarkdown(),
60
58
  ].filter(nonNullable),
61
59
  }),
62
- [node, themeMode],
60
+ [computeGraph, themeMode],
63
61
  );
64
62
 
65
63
  return <div className='w-[40rem] overflow-hidden' ref={parentRef} {...focusAttributes} />;
@@ -68,21 +66,21 @@ const Editor = ({ text }: EditorProps) => {
68
66
  const Grid = () => {
69
67
  const space = useSpace();
70
68
  const graph = useComputeGraph(space);
71
- const sheet = useTestSheet(space, graph, { title: SHEET_NAME });
72
- const model = useSheetModel(space, sheet);
69
+ const sheet = useTestSheet(space, graph, { name: SHEET_NAME });
70
+ const model = useSheetModel(graph, sheet);
73
71
  useEffect(() => {
74
72
  if (model) {
75
73
  model.setValues({ A1: { value: 100 }, A2: { value: 200 }, A3: { value: 300 }, A5: { value: '=SUM(A1:A3)' } });
76
74
  }
77
75
  }, [model]);
78
76
 
79
- if (!space || !sheet) {
77
+ if (!graph || !sheet) {
80
78
  return null;
81
79
  }
82
80
 
83
81
  return (
84
82
  <div className='flex w-[40rem] overflow-hidden'>
85
- <Sheet.Root space={space} sheet={sheet}>
83
+ <Sheet.Root graph={graph} sheet={sheet}>
86
84
  <Sheet.Main classNames='border border-separator' />
87
85
  </Sheet.Root>
88
86
  </div>
@@ -102,13 +100,14 @@ export default {
102
100
  title: 'plugin-sheet/extensions',
103
101
  decorators: [
104
102
  withClientProvider({ types: [SheetType], createIdentity: true, createSpace: true }),
105
- withGraphDecorator,
103
+ withComputeGraphDecorator(),
106
104
  withTheme,
107
105
  withLayout({ fullscreen: true, classNames: 'justify-center' }),
108
106
  ],
109
107
  parameters: { layout: 'fullscreen' },
110
108
  };
111
109
 
110
+ // TODO(burdon): Inline formulae.
112
111
  export const Default = {
113
112
  render: Editor,
114
113
  args: {
@@ -146,6 +145,7 @@ export const Graph = {
146
145
  `="${SHEET_NAME}"!A5`,
147
146
  '```',
148
147
  '',
148
+ '',
149
149
  ),
150
150
  },
151
151
  };
@@ -14,85 +14,134 @@ import {
14
14
  } from '@codemirror/state';
15
15
  import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
16
16
 
17
- import { type ComputeNode } from '../graph';
17
+ import { type UnsubscribeCallback, debounce } from '@dxos/async';
18
+ import { invariant } from '@dxos/invariant';
19
+ import { documentId, singleValueFacet } from '@dxos/react-ui-editor/state';
20
+
21
+ import { type CellAddress } from '../defs';
22
+ import { type ComputeGraph, type ComputeNode, createSheetName } from '../graph';
23
+ import { type CellScalarValue } from '../types';
18
24
 
19
25
  const LANGUAGE_TAG = 'dx';
20
26
 
21
27
  // TODO(burdon): Create marker just for our decorator?
22
28
  const updateAllDecorations = StateEffect.define<void>();
23
29
 
30
+ export const computeGraphFacet = singleValueFacet<ComputeGraph>();
31
+
24
32
  export type ComputeOptions = {};
25
33
 
26
- export const compute = (computeNode: ComputeNode, options: ComputeOptions = {}): Extension => {
27
- const update = (state: EditorState) => {
28
- const builder = new RangeSetBuilder();
29
- syntaxTree(state).iterate({
30
- enter: (node) => {
31
- if (node.name === 'FencedCode') {
32
- const cursor = state.selection.main.head;
33
- if (state.readOnly || cursor < node.from || cursor > node.to) {
34
- const info = node.node.getChild('CodeInfo');
35
- if (info) {
36
- const type = state.sliceDoc(info.from, info.to);
37
- const text = node.node.getChild('CodeText');
38
- if (type === LANGUAGE_TAG && text) {
39
- const content = state.sliceDoc(text.from, text.to);
40
- // TODO(burdon): Map unique reference onto cell; e.g., track ordered list?
41
- computeNode.setValue({ col: 0, row: 0 }, content);
42
- const value = computeNode.getValue({ col: 0, row: 0 });
43
- builder.add(
44
- node.from,
45
- node.to,
46
- Decoration.replace({
47
- widget: new DxWidget(String(value)),
48
- }),
49
- );
34
+ export const compute = (options: ComputeOptions = {}): Extension => {
35
+ let computeNode: ComputeNode | undefined;
36
+
37
+ const update = (state: EditorState, current?: RangeSet<Decoration>) => {
38
+ const builder = new RangeSetBuilder<Decoration>();
39
+ if (computeNode) {
40
+ computeNode.clear();
41
+ syntaxTree(state).iterate({
42
+ enter: (node) => {
43
+ switch (node.name) {
44
+ case 'FencedCode': {
45
+ const cursor = state.selection.main.head;
46
+ if (state.readOnly || cursor < node.from || cursor > node.to) {
47
+ const info = node.node.getChild('CodeInfo');
48
+ if (info) {
49
+ const type = state.sliceDoc(info.from, info.to);
50
+ const text = node.node.getChild('CodeText');
51
+ if (type === LANGUAGE_TAG && text) {
52
+ const formula = state.sliceDoc(text.from, text.to);
53
+
54
+ const iter = current?.iter(node.node.from);
55
+ if (iter?.value && iter?.value.spec.formula === formula) {
56
+ // Add existing widget.
57
+ builder.add(node.from, node.to, iter.value);
58
+ } else {
59
+ // TODO(burdon): Create ordered list of cells on each decoration run.
60
+ const cell: CellAddress = { col: node.node.from, row: 0 };
61
+ invariant(computeNode);
62
+ // NOTE: This triggers re-render (below).
63
+ computeNode.setValue(cell, formula);
64
+ const value = computeNode.getValue(cell);
65
+ builder.add(
66
+ node.from,
67
+ node.to,
68
+ Decoration.replace({
69
+ widget: new ComputeWidget(formula, value),
70
+ formula,
71
+ }),
72
+ );
73
+ }
74
+ }
75
+ }
50
76
  }
77
+
78
+ break;
51
79
  }
52
80
  }
53
- }
54
- },
55
- });
81
+ },
82
+ });
83
+ }
56
84
 
57
85
  return builder.finish();
58
86
  };
59
87
 
60
88
  return [
61
- // Graph subscription.
62
89
  ViewPlugin.fromClass(
63
90
  class {
64
- private readonly _subscription: any;
91
+ // Graph subscription.
92
+ private _subscription?: UnsubscribeCallback;
65
93
  constructor(view: EditorView) {
66
- this._subscription = computeNode.graph.update.on(() => {
67
- view.dispatch({
68
- effects: updateAllDecorations.of(),
94
+ const id = view.state.facet(documentId);
95
+ const computeGraph = view.state.facet(computeGraphFacet);
96
+ if (id && computeGraph) {
97
+ queueMicrotask(async () => {
98
+ computeNode = computeGraph.getOrCreateNode(createSheetName({ type: '', id }));
99
+ await computeNode.open();
100
+
101
+ // Trigger re-render if values updated.
102
+ // TODO(burdon): Trigger only if formula value updated (currently triggered during render).
103
+ this._subscription = computeNode.update.on(
104
+ debounce(({ type, ...rest }) => {
105
+ if (type === 'valuesUpdated') {
106
+ view.dispatch({
107
+ effects: updateAllDecorations.of(),
108
+ });
109
+ }
110
+ }, 250),
111
+ );
69
112
  });
70
- });
113
+ }
71
114
  }
72
115
 
73
116
  destroy() {
74
- this._subscription();
117
+ this._subscription?.();
118
+ void computeNode?.close();
119
+ computeNode = undefined;
75
120
  }
76
121
  },
77
122
  ),
78
123
 
79
- // Decorations.
80
- StateField.define<RangeSet<any>>({
124
+ StateField.define<RangeSet<Decoration>>({
81
125
  create: (state) => update(state),
82
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state),
126
+ update: (rangeSet: RangeSet<Decoration>, tr: Transaction) => update(tr.state, rangeSet),
83
127
  provide: (field) => EditorView.decorations.from(field),
84
128
  }),
85
129
  ];
86
130
  };
87
131
 
88
- class DxWidget extends WidgetType {
89
- constructor(private readonly value: string) {
132
+ // TODO(burdon): Click to edit.
133
+ class ComputeWidget extends WidgetType {
134
+ constructor(
135
+ private readonly formula: string,
136
+ private readonly value: CellScalarValue,
137
+ ) {
90
138
  super();
91
139
  }
92
140
 
93
- override toDOM(view: EditorView) {
141
+ override toDOM(_view: EditorView) {
94
142
  const div = document.createElement('div');
95
- div.innerText = this.value;
143
+ div.setAttribute('title', this.formula);
144
+ div.innerText = String(this.value);
96
145
  return div;
97
146
  }
98
147
  }
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import type { FunctionPluginDefinition } from 'hyperformula';
6
+ import type { ConfigParams } from 'hyperformula/typings/ConfigParams';
7
+ import type { FunctionTranslationsPackage } from 'hyperformula/typings/interpreter';
8
+ import defaultsDeep from 'lodash.defaultsdeep';
9
+
10
+ import { type SpaceId, type Space } from '@dxos/client/echo';
11
+ import { Resource } from '@dxos/context';
12
+ import { invariant } from '@dxos/invariant';
13
+ import { log } from '@dxos/log';
14
+
15
+ import { HyperFormula } from '#hyperformula';
16
+ import { ComputeGraph } from './compute-graph';
17
+ import { EdgeFunctionPlugin, EdgeFunctionPluginTranslations, type FunctionContextOptions } from './functions';
18
+
19
+ export type ComputeGraphPlugin = {
20
+ plugin: FunctionPluginDefinition;
21
+ translations: FunctionTranslationsPackage;
22
+ };
23
+
24
+ export type ComputeGraphOptions = {
25
+ plugins?: ComputeGraphPlugin[];
26
+ } & Partial<FunctionContextOptions> &
27
+ Partial<ConfigParams>;
28
+
29
+ export const defaultOptions: ComputeGraphOptions = {
30
+ licenseKey: 'gpl-v3',
31
+ };
32
+
33
+ export const defaultPlugins: ComputeGraphPlugin[] = [
34
+ {
35
+ plugin: EdgeFunctionPlugin,
36
+ translations: EdgeFunctionPluginTranslations,
37
+ },
38
+ ];
39
+
40
+ /**
41
+ * Manages a collection of ComputeGraph instances for each space.
42
+ *
43
+ * [ComputePlugin] => [ComputeGraphRegistry] => [ComputeGraph(Space)] => [ComputeNode(Object)]
44
+ *
45
+ * NOTE: The ComputeGraphRegistry manages the hierarchy of resources via its root Context.
46
+ * NOTE: The package.json file defines the packaged #hyperformula module.
47
+ */
48
+ // TODO(burdon): Move graph into separate plugin; isolate HF deps.
49
+ export class ComputeGraphRegistry extends Resource {
50
+ private readonly _graphs = new Map<SpaceId, ComputeGraph>();
51
+
52
+ private readonly _options: ComputeGraphOptions;
53
+
54
+ constructor(options: ComputeGraphOptions = { plugins: defaultPlugins }) {
55
+ super();
56
+ this._options = defaultsDeep({}, options, defaultOptions);
57
+ this._options.plugins?.forEach(({ plugin, translations }) => {
58
+ HyperFormula.registerFunctionPlugin(plugin, translations);
59
+ });
60
+ }
61
+
62
+ getGraph(spaceId: SpaceId): ComputeGraph | undefined {
63
+ return this._graphs.get(spaceId);
64
+ }
65
+
66
+ getOrCreateGraph(space: Space): ComputeGraph {
67
+ let graph = this._graphs.get(space.id);
68
+ if (!graph) {
69
+ log('create graph', { space: space.id });
70
+ graph = this.createGraph(space);
71
+ }
72
+
73
+ return graph;
74
+ }
75
+
76
+ createGraph(space: Space): ComputeGraph {
77
+ invariant(!this._graphs.has(space.id), `ComputeGraph already exists for space: ${space.id}`);
78
+ const hf = HyperFormula.buildEmpty(this._options);
79
+ const graph = new ComputeGraph(hf, space, this._options);
80
+ this._graphs.set(space.id, graph);
81
+ return graph;
82
+ }
83
+
84
+ protected override async _close() {
85
+ for (const graph of this._graphs.values()) {
86
+ await graph.close();
87
+ }
88
+ this._graphs.clear();
89
+ }
90
+ }
@@ -13,9 +13,10 @@ import { Toolbar, Button, Input } from '@dxos/react-ui';
13
13
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
14
14
  import { withTheme } from '@dxos/storybook-utils';
15
15
 
16
+ import { testFunctionPlugins } from './testing';
16
17
  import { createSheet } from '../defs';
17
18
  import { useComputeGraph, useSheetModel } from '../hooks';
18
- import { withGraphDecorator } from '../testing';
19
+ import { withComputeGraphDecorator } from '../testing';
19
20
  import { SheetType } from '../types';
20
21
 
21
22
  const FUNCTION_NAME = 'TEST';
@@ -26,7 +27,7 @@ const Story = () => {
26
27
  const [sheet, setSheet] = useState<SheetType>();
27
28
  const [text, setText] = useState(`${FUNCTION_NAME}(100)`);
28
29
  const [result, setResult] = useState<any>();
29
- const model = useSheetModel(space, sheet);
30
+ const model = useSheetModel(graph, sheet);
30
31
  useEffect(() => {
31
32
  if (space) {
32
33
  const sheet = space.db.add(createSheet());
@@ -83,7 +84,7 @@ export default {
83
84
  title: 'plugin-sheet/functions',
84
85
  decorators: [
85
86
  withClientProvider({ types: [FunctionType, SheetType], createIdentity: true, createSpace: true }),
86
- withGraphDecorator,
87
+ withComputeGraphDecorator({ plugins: testFunctionPlugins }),
87
88
  withTheme,
88
89
  ],
89
90
  render: (args: any) => <Story {...args} />,
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type CellValue } from 'hyperformula';
6
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
7
+
8
+ import { Trigger } from '@dxos/async';
9
+ import { create, fullyQualifiedId } from '@dxos/client/echo';
10
+ import { FunctionType } from '@dxos/plugin-script/types';
11
+
12
+ import { DetailedCellError } from '#hyperformula';
13
+ import { TestBuilder } from './testing';
14
+
15
+ describe('ComputeGraph', () => {
16
+ let testBuilder: TestBuilder;
17
+ beforeEach(async () => {
18
+ testBuilder = new TestBuilder({ types: [FunctionType] });
19
+ await testBuilder.open();
20
+ });
21
+ afterEach(async () => {
22
+ await testBuilder.close();
23
+ });
24
+
25
+ test('map functions', async () => {
26
+ const space = await testBuilder.client.spaces.create();
27
+ const graph = testBuilder.registry.createGraph(space);
28
+ await graph.open();
29
+
30
+ // Create script.
31
+ const trigger = new Trigger();
32
+ graph.update.once(() => trigger.wake());
33
+ const functionObject = space.db.add(create(FunctionType, { version: 1, binding: 'TEST' }));
34
+ await trigger.wait();
35
+ const functions = graph.getFunctions({ echo: true });
36
+ expect(functions).to.toHaveLength(1);
37
+
38
+ const id = graph.mapFunctionBindingToId('TEST()');
39
+ expect(id).to.eq(`${fullyQualifiedId(functionObject)}()`);
40
+ });
41
+
42
+ test('cross-node references', async () => {
43
+ const space = await testBuilder.client.spaces.create();
44
+ const graph = testBuilder.registry.createGraph(space);
45
+
46
+ // Create nodes.
47
+ const node1 = await graph.getOrCreateNode('node-1').open();
48
+ const node2 = await graph.getOrCreateNode('node-2').open();
49
+ expect(graph.hf.getSheetNames()).to.toHaveLength(2);
50
+
51
+ {
52
+ node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, [[100, 200, 300, '=SUM(A1:C1)']]);
53
+ node2.graph.hf.setCellContents({ sheet: node2.sheetId, row: 0, col: 0 }, "='node-1'!D1");
54
+ const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 3, row: 0 });
55
+ const value2 = node2.graph.hf.getCellValue({ sheet: node2.sheetId, col: 0, row: 0 });
56
+ expect(value1).to.eq(value2);
57
+ }
58
+
59
+ // Get updated event.
60
+ const trigger = new Trigger<CellValue>();
61
+ node2.update.on(({ change }) => {
62
+ const value = node2.graph.hf.getCellValue({ sheet: node2.sheetId, col: 0, row: 0 });
63
+ expect(value).to.eq(change?.newValue);
64
+ trigger.wake(value);
65
+ });
66
+
67
+ {
68
+ node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, 400);
69
+ const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 3, row: 0 });
70
+ const value2 = await trigger.wait();
71
+ expect(value1).to.eq(value2);
72
+ }
73
+ });
74
+
75
+ // TODO(burdon): Dynamically load node/model based on dependencies.
76
+ // - Create dependency then close model.
77
+ test('dynamic loading', async () => {
78
+ const space = await testBuilder.client.spaces.create();
79
+ const graph = testBuilder.registry.createGraph(space);
80
+
81
+ const node1 = await graph.getOrCreateNode('node-1').open();
82
+ node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, "='node-2'!A1");
83
+ const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 0, row: 0 });
84
+ expect(value1).to.be.instanceof(DetailedCellError);
85
+ expect((value1 as DetailedCellError).type).to.eq('REF');
86
+ });
87
+ });