@dxos/plugin-sheet 0.6.12-main.ed7cda7 → 0.6.12-main.f9d0246
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.
- package/dist/lib/browser/{SheetContainer-V4GCCZTX.mjs → SheetContainer-VISF3VUB.mjs} +6 -6
- package/dist/lib/browser/{SheetContainer-V4GCCZTX.mjs.map → SheetContainer-VISF3VUB.mjs.map} +3 -3
- package/dist/lib/browser/{chunk-T3NJFTD4.mjs → chunk-WZMOZKQZ.mjs} +2 -2
- package/dist/lib/browser/{chunk-T3NJFTD4.mjs.map → chunk-WZMOZKQZ.mjs.map} +3 -3
- package/dist/lib/browser/{chunk-6ZMQVB4Z.mjs → chunk-Z2XOOC2R.mjs} +81 -62
- package/dist/lib/browser/chunk-Z2XOOC2R.mjs.map +7 -0
- package/dist/lib/browser/{chunk-U2JHW3L6.mjs → chunk-ZLJ2GRE2.mjs} +173 -42
- package/dist/lib/browser/chunk-ZLJ2GRE2.mjs.map +7 -0
- package/dist/lib/browser/{graph-T27BOBOV.mjs → graph-4XFKIHRL.mjs} +4 -4
- package/dist/lib/browser/index.mjs +15 -13
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types.mjs +1 -1
- package/dist/lib/node/{SheetContainer-3ZY7MPWJ.cjs → SheetContainer-2MEALQWW.cjs} +14 -14
- package/dist/lib/node/{SheetContainer-3ZY7MPWJ.cjs.map → SheetContainer-2MEALQWW.cjs.map} +3 -3
- package/dist/lib/node/{chunk-OTTD7FBK.cjs → chunk-6DQABRGJ.cjs} +192 -60
- package/dist/lib/node/chunk-6DQABRGJ.cjs.map +7 -0
- package/dist/lib/node/{chunk-Q3HBHPRL.cjs → chunk-AOP42UAA.cjs} +5 -5
- package/dist/lib/node/{chunk-Q3HBHPRL.cjs.map → chunk-AOP42UAA.cjs.map} +3 -3
- package/dist/lib/node/{chunk-DD6FIXWC.cjs → chunk-P5QYYEHQ.cjs} +86 -67
- package/dist/lib/node/chunk-P5QYYEHQ.cjs.map +7 -0
- package/dist/lib/node/{graph-SPKGX7W4.cjs → graph-2LRDUXBZ.cjs} +14 -14
- package/dist/lib/node/graph-2LRDUXBZ.cjs.map +7 -0
- package/dist/lib/node/index.cjs +25 -24
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types.cjs +8 -8
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/{SheetContainer-PXSJX6XK.mjs → SheetContainer-RPSUSXWS.mjs} +6 -6
- package/dist/lib/node-esm/{SheetContainer-PXSJX6XK.mjs.map → SheetContainer-RPSUSXWS.mjs.map} +3 -3
- package/dist/lib/node-esm/{chunk-D6KU5MI7.mjs → chunk-4MM7THJW.mjs} +81 -62
- package/dist/lib/node-esm/chunk-4MM7THJW.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-7HVSOTGA.mjs → chunk-5RLTCIE2.mjs} +173 -42
- package/dist/lib/node-esm/chunk-5RLTCIE2.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-BMNA27EX.mjs → chunk-RR2AO4SM.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-BMNA27EX.mjs.map → chunk-RR2AO4SM.mjs.map} +3 -3
- package/dist/lib/node-esm/{graph-U67IO4UC.mjs → graph-WG5EKOMO.mjs} +4 -4
- package/dist/lib/node-esm/index.mjs +15 -13
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types.mjs +1 -1
- package/dist/types/src/SheetPlugin.d.ts.map +1 -1
- package/dist/types/src/components/GridSheet/GridSheet.d.ts +3 -3
- package/dist/types/src/components/GridSheet/GridSheet.d.ts.map +1 -1
- package/dist/types/src/components/GridSheet/GridSheet.stories.d.ts +1 -1
- package/dist/types/src/components/GridSheet/GridSheet.stories.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/sheet-context.d.ts +3 -3
- package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -1
- package/dist/types/src/components/SheetContainer.d.ts +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/defs/types.d.ts.map +1 -1
- package/dist/types/src/defs/util.d.ts +1 -1
- package/dist/types/src/defs/util.d.ts.map +1 -1
- package/dist/types/src/extensions/compute.d.ts +5 -1
- package/dist/types/src/extensions/compute.d.ts.map +1 -1
- package/dist/types/src/extensions/compute.stories.d.ts.map +1 -1
- package/dist/types/src/graph/async-function.d.ts +7 -1
- package/dist/types/src/graph/async-function.d.ts.map +1 -1
- package/dist/types/src/graph/compute-graph.d.ts +12 -9
- package/dist/types/src/graph/compute-graph.d.ts.map +1 -1
- package/dist/types/src/graph/compute-graph.stories.d.ts.map +1 -1
- package/dist/types/src/graph/compute-graph.test.d.ts +2 -0
- package/dist/types/src/graph/compute-graph.test.d.ts.map +1 -0
- package/dist/types/src/graph/compute-node.d.ts +9 -2
- package/dist/types/src/graph/compute-node.d.ts.map +1 -1
- package/dist/types/src/graph/edge-function.d.ts.map +1 -1
- package/dist/types/src/graph/{custom-function.d.ts → testing/custom-function.d.ts} +3 -1
- package/dist/types/src/graph/testing/custom-function.d.ts.map +1 -0
- package/dist/types/src/graph/testing/index.d.ts +2 -0
- package/dist/types/src/graph/testing/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useComputeGraph.d.ts.map +1 -1
- package/dist/types/src/hooks/useSheetModel.d.ts +2 -2
- package/dist/types/src/hooks/useSheetModel.d.ts.map +1 -1
- package/dist/types/src/model/sheet-model.d.ts +3 -3
- package/dist/types/src/model/sheet-model.d.ts.map +1 -1
- package/dist/types/src/testing/testing.d.ts +4 -5
- package/dist/types/src/testing/testing.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +4 -3
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/SheetPlugin.tsx +9 -7
- package/src/components/CellEditor/CellEditor.stories.tsx +1 -1
- package/src/components/GridSheet/GridSheet.stories.tsx +5 -4
- package/src/components/GridSheet/GridSheet.tsx +4 -4
- package/src/components/Sheet/Sheet.stories.tsx +21 -20
- package/src/components/Sheet/sheet-context.tsx +4 -4
- package/src/components/SheetContainer.tsx +2 -2
- package/src/defs/types.ts +1 -0
- package/src/defs/util.ts +19 -3
- package/src/extensions/compute.stories.tsx +18 -16
- package/src/extensions/compute.ts +72 -39
- package/src/graph/async-function.ts +13 -6
- package/src/graph/compute-graph.stories.tsx +4 -3
- package/src/graph/compute-graph.test.ts +127 -0
- package/src/graph/compute-graph.ts +64 -41
- package/src/graph/compute-node.ts +16 -5
- package/src/graph/edge-function.ts +1 -2
- package/src/graph/{custom-function.ts → testing/custom-function.ts} +10 -2
- package/src/graph/testing/index.ts +5 -0
- package/src/hooks/hooks.stories.tsx +3 -3
- package/src/hooks/useComputeGraph.ts +2 -1
- package/src/hooks/useSheetModel.ts +4 -7
- package/src/model/sheet-model.ts +44 -29
- package/src/testing/testing.tsx +17 -15
- package/src/types.ts +3 -3
- package/dist/lib/browser/chunk-6ZMQVB4Z.mjs.map +0 -7
- package/dist/lib/browser/chunk-U2JHW3L6.mjs.map +0 -7
- package/dist/lib/node/chunk-DD6FIXWC.cjs.map +0 -7
- package/dist/lib/node/chunk-OTTD7FBK.cjs.map +0 -7
- package/dist/lib/node/graph-SPKGX7W4.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-7HVSOTGA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-D6KU5MI7.mjs.map +0 -7
- package/dist/types/src/graph/compute-graph.browser.test.d.ts +0 -2
- package/dist/types/src/graph/compute-graph.browser.test.d.ts.map +0 -1
- package/dist/types/src/graph/custom-function.d.ts.map +0 -1
- package/src/graph/compute-graph.browser.test.ts +0 -104
- /package/dist/lib/browser/{graph-T27BOBOV.mjs.map → graph-4XFKIHRL.mjs.map} +0 -0
- /package/dist/lib/node-esm/{graph-U67IO4UC.mjs.map → graph-WG5EKOMO.mjs.map} +0 -0
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import React, { type PropsWithChildren, createContext, useContext, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { invariant } from '@dxos/invariant';
|
|
8
|
-
import { type Space } from '@dxos/react-client/echo';
|
|
9
8
|
|
|
10
9
|
import { createDecorations } from './decorations';
|
|
11
10
|
import { type CellAddress, type CellRange } from '../../defs';
|
|
11
|
+
import { type ComputeGraph } from '../../graph';
|
|
12
12
|
import { useSheetModel, useFormattingModel } from '../../hooks';
|
|
13
13
|
import { type FormattingModel, type SheetModel } from '../../model';
|
|
14
14
|
import { type SheetType } from '../../types';
|
|
@@ -45,19 +45,19 @@ export const useSheetContext = (): SheetContextType => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
export type SheetContextProps = {
|
|
48
|
+
graph: ComputeGraph;
|
|
48
49
|
sheet: SheetType;
|
|
49
|
-
space: Space;
|
|
50
50
|
readonly?: boolean;
|
|
51
51
|
} & Pick<SheetContextType, 'onInfo'>;
|
|
52
52
|
|
|
53
53
|
export const SheetContextProvider = ({
|
|
54
54
|
children,
|
|
55
|
+
graph,
|
|
55
56
|
sheet,
|
|
56
|
-
space,
|
|
57
57
|
readonly,
|
|
58
58
|
onInfo,
|
|
59
59
|
}: PropsWithChildren<SheetContextProps>) => {
|
|
60
|
-
const model = useSheetModel(
|
|
60
|
+
const model = useSheetModel(graph, sheet, { readonly });
|
|
61
61
|
const formatting = useFormattingModel(model);
|
|
62
62
|
|
|
63
63
|
// TODO(Zan): Impl. set range and set cursor that scrolls to that cell or range if it is not visible.
|
|
@@ -22,7 +22,7 @@ const attentionFragment = mx(
|
|
|
22
22
|
export const sectionToolbarLayout =
|
|
23
23
|
'bs-[--rail-action] bg-[--sticky-bg] sticky block-start-0 __-block-start-px transition-opacity';
|
|
24
24
|
|
|
25
|
-
const SheetContainer = ({
|
|
25
|
+
const SheetContainer = ({ graph, sheet, role }: SheetRootProps & { role?: string }) => {
|
|
26
26
|
const dispatch = useIntentDispatcher();
|
|
27
27
|
|
|
28
28
|
const id = fullyQualifiedId(sheet);
|
|
@@ -50,7 +50,7 @@ const SheetContainer = ({ sheet, space, role }: SheetRootProps & { role?: string
|
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
<div role='none' className={role === 'article' ? 'row-span-2 grid grid-rows-subgrid' : undefined}>
|
|
53
|
-
<Sheet.Root
|
|
53
|
+
<Sheet.Root graph={graph} sheet={sheet}>
|
|
54
54
|
<div role='none' className={mx('flex flex-0 justify-center overflow-x-auto')}>
|
|
55
55
|
<Toolbar.Root
|
|
56
56
|
onAction={handleAction}
|
package/src/defs/types.ts
CHANGED
|
@@ -34,6 +34,7 @@ export const addressToA1Notation = ({ col, row }: CellAddress): string => {
|
|
|
34
34
|
return `${columnLetter(col)}${row + 1}`;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// TODO(burdon): See simpleCellAddressFromString
|
|
37
38
|
export const addressFromA1Notation = (ref: string): CellAddress => {
|
|
38
39
|
const match = ref.match(/([A-Z]+)(\d+)/);
|
|
39
40
|
invariant(match, `Invalid notation: ${ref}`);
|
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 {
|
|
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 = ({
|
|
63
|
+
export const createSheet = ({ name, cells, ...size }: CreateSheetOptions = {}): SheetType => {
|
|
56
64
|
const sheet = create(SheetType, {
|
|
57
|
-
|
|
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,10 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
|
-
import React, { useEffect
|
|
6
|
+
import React, { useEffect } from 'react';
|
|
7
7
|
|
|
8
8
|
import { useSpace } from '@dxos/react-client/echo';
|
|
9
9
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
10
|
+
import { useAsyncState } from '@dxos/react-hooks';
|
|
10
11
|
import { useThemeContext } from '@dxos/react-ui';
|
|
11
12
|
import {
|
|
12
13
|
createBasicExtensions,
|
|
@@ -18,11 +19,11 @@ import {
|
|
|
18
19
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
19
20
|
import { nonNullable } from '@dxos/util';
|
|
20
21
|
|
|
21
|
-
import { compute } from './compute';
|
|
22
|
+
import { compute, computeNodeFacet } from './compute';
|
|
22
23
|
import { Sheet } from '../components';
|
|
23
24
|
import { type ComputeNode } from '../graph';
|
|
24
25
|
import { useComputeGraph, useSheetModel } from '../hooks';
|
|
25
|
-
import { useTestSheet,
|
|
26
|
+
import { useTestSheet, withComputeGraphDecorator } from '../testing';
|
|
26
27
|
import { SheetType } from '../types';
|
|
27
28
|
|
|
28
29
|
const str = (...lines: string[]) => lines.join('\n');
|
|
@@ -34,6 +35,8 @@ type EditorProps = {
|
|
|
34
35
|
// TODO(burdon): Implement named expressions.
|
|
35
36
|
// https://hyperformula.handsontable.com/guide/cell-references.html
|
|
36
37
|
|
|
38
|
+
// TODO(burdon): Inline Adobe eCharts.
|
|
39
|
+
|
|
37
40
|
const DOC_NAME = 'Test Doc';
|
|
38
41
|
const SHEET_NAME = 'Test Sheet';
|
|
39
42
|
|
|
@@ -41,12 +44,8 @@ const Editor = ({ text }: EditorProps) => {
|
|
|
41
44
|
const { themeMode } = useThemeContext();
|
|
42
45
|
const space = useSpace();
|
|
43
46
|
const graph = useComputeGraph(space);
|
|
44
|
-
const [
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (graph) {
|
|
48
|
-
setNode(graph.getOrCreateNode(DOC_NAME));
|
|
49
|
-
}
|
|
47
|
+
const [computeNode] = useAsyncState<ComputeNode>(async () => {
|
|
48
|
+
return graph ? await graph.getOrCreateNode(DOC_NAME) : undefined;
|
|
50
49
|
}, [graph]);
|
|
51
50
|
const { parentRef, focusAttributes } = useTextEditor(
|
|
52
51
|
() => ({
|
|
@@ -55,11 +54,12 @@ const Editor = ({ text }: EditorProps) => {
|
|
|
55
54
|
createBasicExtensions(),
|
|
56
55
|
createMarkdownExtensions({ themeMode }),
|
|
57
56
|
createThemeExtensions({ themeMode, syntaxHighlighting: true }),
|
|
58
|
-
|
|
57
|
+
computeNode && computeNodeFacet.of(computeNode),
|
|
58
|
+
compute(),
|
|
59
59
|
decorateMarkdown(),
|
|
60
60
|
].filter(nonNullable),
|
|
61
61
|
}),
|
|
62
|
-
[
|
|
62
|
+
[computeNode, themeMode],
|
|
63
63
|
);
|
|
64
64
|
|
|
65
65
|
return <div className='w-[40rem] overflow-hidden' ref={parentRef} {...focusAttributes} />;
|
|
@@ -68,21 +68,21 @@ const Editor = ({ text }: EditorProps) => {
|
|
|
68
68
|
const Grid = () => {
|
|
69
69
|
const space = useSpace();
|
|
70
70
|
const graph = useComputeGraph(space);
|
|
71
|
-
const sheet = useTestSheet(space, graph, {
|
|
72
|
-
const model = useSheetModel(
|
|
71
|
+
const sheet = useTestSheet(space, graph, { name: SHEET_NAME });
|
|
72
|
+
const model = useSheetModel(graph, sheet);
|
|
73
73
|
useEffect(() => {
|
|
74
74
|
if (model) {
|
|
75
75
|
model.setValues({ A1: { value: 100 }, A2: { value: 200 }, A3: { value: 300 }, A5: { value: '=SUM(A1:A3)' } });
|
|
76
76
|
}
|
|
77
77
|
}, [model]);
|
|
78
78
|
|
|
79
|
-
if (!
|
|
79
|
+
if (!graph || !sheet) {
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return (
|
|
84
84
|
<div className='flex w-[40rem] overflow-hidden'>
|
|
85
|
-
<Sheet.Root
|
|
85
|
+
<Sheet.Root graph={graph} sheet={sheet}>
|
|
86
86
|
<Sheet.Main classNames='border border-separator' />
|
|
87
87
|
</Sheet.Root>
|
|
88
88
|
</div>
|
|
@@ -102,13 +102,14 @@ export default {
|
|
|
102
102
|
title: 'plugin-sheet/extensions',
|
|
103
103
|
decorators: [
|
|
104
104
|
withClientProvider({ types: [SheetType], createIdentity: true, createSpace: true }),
|
|
105
|
-
|
|
105
|
+
withComputeGraphDecorator(),
|
|
106
106
|
withTheme,
|
|
107
107
|
withLayout({ fullscreen: true, classNames: 'justify-center' }),
|
|
108
108
|
],
|
|
109
109
|
parameters: { layout: 'fullscreen' },
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// TODO(burdon): Inline formulae.
|
|
112
113
|
export const Default = {
|
|
113
114
|
render: Editor,
|
|
114
115
|
args: {
|
|
@@ -146,6 +147,7 @@ export const Graph = {
|
|
|
146
147
|
`="${SHEET_NAME}"!A5`,
|
|
147
148
|
'```',
|
|
148
149
|
'',
|
|
150
|
+
'',
|
|
149
151
|
),
|
|
150
152
|
},
|
|
151
153
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { Facet } from '@codemirror/state';
|
|
6
7
|
import {
|
|
7
8
|
type EditorState,
|
|
8
9
|
type Extension,
|
|
@@ -14,7 +15,21 @@ import {
|
|
|
14
15
|
} from '@codemirror/state';
|
|
15
16
|
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
16
17
|
|
|
18
|
+
import { type UnsubscribeCallback } from '@dxos/async';
|
|
19
|
+
import { type Space } from '@dxos/client/echo';
|
|
20
|
+
|
|
21
|
+
import { type CellAddress } from '../defs';
|
|
17
22
|
import { type ComputeNode } from '../graph';
|
|
23
|
+
import { type CellScalarValue } from '../types';
|
|
24
|
+
|
|
25
|
+
export const spaceFacet = Facet.define<Space, Space>({
|
|
26
|
+
combine: (values) => values[0],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// TODO(burdon): Create on demand?
|
|
30
|
+
export const computeNodeFacet = Facet.define<ComputeNode, ComputeNode>({
|
|
31
|
+
combine: (values) => values[0],
|
|
32
|
+
});
|
|
18
33
|
|
|
19
34
|
const LANGUAGE_TAG = 'dx';
|
|
20
35
|
|
|
@@ -23,36 +38,46 @@ const updateAllDecorations = StateEffect.define<void>();
|
|
|
23
38
|
|
|
24
39
|
export type ComputeOptions = {};
|
|
25
40
|
|
|
26
|
-
export const compute = (
|
|
27
|
-
const update = (state: EditorState) => {
|
|
28
|
-
const builder = new RangeSetBuilder();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
export const compute = (options: ComputeOptions = {}): Extension => {
|
|
42
|
+
const update = (state: EditorState, rangeSet?: RangeSet<Decoration>) => {
|
|
43
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
44
|
+
const computeNode = state.facet(computeNodeFacet);
|
|
45
|
+
if (computeNode) {
|
|
46
|
+
computeNode.clear();
|
|
47
|
+
syntaxTree(state).iterate({
|
|
48
|
+
enter: (node) => {
|
|
49
|
+
if (node.name === 'FencedCode') {
|
|
50
|
+
const cursor = state.selection.main.head;
|
|
51
|
+
if (state.readOnly || cursor < node.from || cursor > node.to) {
|
|
52
|
+
const info = node.node.getChild('CodeInfo');
|
|
53
|
+
if (info) {
|
|
54
|
+
const type = state.sliceDoc(info.from, info.to);
|
|
55
|
+
const text = node.node.getChild('CodeText');
|
|
56
|
+
if (type === LANGUAGE_TAG && text) {
|
|
57
|
+
const formula = state.sliceDoc(text.from, text.to);
|
|
58
|
+
const iter = rangeSet?.iter(node.node.from);
|
|
59
|
+
if (iter?.value && iter?.value.spec.formula === formula) {
|
|
60
|
+
builder.add(node.from, node.to, iter.value);
|
|
61
|
+
} else {
|
|
62
|
+
const cell: CellAddress = { col: node.node.from, row: 0 };
|
|
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 DxWidget(formula, value),
|
|
70
|
+
formula,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
56
81
|
|
|
57
82
|
return builder.finish();
|
|
58
83
|
};
|
|
@@ -61,38 +86,46 @@ export const compute = (computeNode: ComputeNode, options: ComputeOptions = {}):
|
|
|
61
86
|
// Graph subscription.
|
|
62
87
|
ViewPlugin.fromClass(
|
|
63
88
|
class {
|
|
64
|
-
private readonly _subscription
|
|
89
|
+
private readonly _subscription?: UnsubscribeCallback;
|
|
65
90
|
constructor(view: EditorView) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
const computeNode = view.state.facet(computeNodeFacet);
|
|
92
|
+
if (computeNode) {
|
|
93
|
+
this._subscription = computeNode.update.on(({ type }) => {
|
|
94
|
+
if (type === 'valuesUpdated') {
|
|
95
|
+
view.dispatch({
|
|
96
|
+
effects: updateAllDecorations.of(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
69
99
|
});
|
|
70
|
-
}
|
|
100
|
+
}
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
destroy() {
|
|
74
|
-
this._subscription();
|
|
104
|
+
this._subscription?.();
|
|
75
105
|
}
|
|
76
106
|
},
|
|
77
107
|
),
|
|
78
108
|
|
|
79
|
-
|
|
80
|
-
StateField.define<RangeSet<any>>({
|
|
109
|
+
StateField.define<RangeSet<Decoration>>({
|
|
81
110
|
create: (state) => update(state),
|
|
82
|
-
update: (
|
|
111
|
+
update: (rangeSet: RangeSet<Decoration>, tr: Transaction) => update(tr.state, rangeSet),
|
|
83
112
|
provide: (field) => EditorView.decorations.from(field),
|
|
84
113
|
}),
|
|
85
114
|
];
|
|
86
115
|
};
|
|
87
116
|
|
|
88
117
|
class DxWidget extends WidgetType {
|
|
89
|
-
constructor(
|
|
118
|
+
constructor(
|
|
119
|
+
private readonly formula: string,
|
|
120
|
+
private readonly value: CellScalarValue,
|
|
121
|
+
) {
|
|
90
122
|
super();
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
override toDOM(
|
|
125
|
+
override toDOM(_view: EditorView) {
|
|
94
126
|
const div = document.createElement('div');
|
|
95
|
-
div.
|
|
127
|
+
div.setAttribute('title', this.formula);
|
|
128
|
+
div.innerText = String(this.value);
|
|
96
129
|
return div;
|
|
97
130
|
}
|
|
98
131
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type SimpleCellAddress } from 'hyperformula/typings/Cell';
|
|
5
6
|
import { type InterpreterState } from 'hyperformula/typings/interpreter/InterpreterState';
|
|
6
7
|
import { type InterpreterValue } from 'hyperformula/typings/interpreter/InterpreterValue';
|
|
7
8
|
import { type ProcedureAst } from 'hyperformula/typings/parser';
|
|
@@ -22,6 +23,11 @@ import { CellError, ErrorType, EmptyValue, FunctionPlugin, type HyperFormula } f
|
|
|
22
23
|
// TODO(burdon): Create wrapper.
|
|
23
24
|
export type AsyncFunction = (...args: any) => Promise<InterpreterValue>;
|
|
24
25
|
|
|
26
|
+
export type FunctionUpdateEvent = {
|
|
27
|
+
name: string;
|
|
28
|
+
cell: SimpleCellAddress;
|
|
29
|
+
};
|
|
30
|
+
|
|
25
31
|
export type FunctionOptions = {
|
|
26
32
|
ttl?: number;
|
|
27
33
|
};
|
|
@@ -30,6 +36,7 @@ export type FunctionContextOptions = {
|
|
|
30
36
|
defaultTtl: number;
|
|
31
37
|
recalculationDelay: number;
|
|
32
38
|
remoteFunctionUrl: string;
|
|
39
|
+
onUpdate?: (update: FunctionUpdateEvent) => void;
|
|
33
40
|
};
|
|
34
41
|
|
|
35
42
|
export const defaultFunctionContextOptions: FunctionContextOptions = {
|
|
@@ -66,24 +73,24 @@ export class FunctionContext {
|
|
|
66
73
|
private _invocations: Record<string, number> = {};
|
|
67
74
|
|
|
68
75
|
private readonly _options: FunctionContextOptions;
|
|
69
|
-
|
|
76
|
+
|
|
77
|
+
// Debounced update handler.
|
|
78
|
+
private readonly _onUpdate: (update: FunctionUpdateEvent) => void;
|
|
70
79
|
|
|
71
80
|
constructor(
|
|
72
81
|
private readonly _hf: HyperFormula,
|
|
73
82
|
private readonly _space: Space | undefined,
|
|
74
|
-
onUpdate: (context: FunctionContext) => void,
|
|
75
83
|
_options?: Partial<FunctionContextOptions>,
|
|
76
84
|
) {
|
|
77
85
|
this._options = defaultsDeep(_options ?? {}, defaultFunctionContextOptions);
|
|
78
|
-
this._onUpdate = debounce(() => {
|
|
86
|
+
this._onUpdate = debounce((update) => {
|
|
79
87
|
// TODO(burdon): Better way to trigger recalculation?
|
|
80
88
|
// NOTE: rebuildAndRecalculate resets the undo history.
|
|
81
89
|
this._hf.resumeEvaluation();
|
|
82
|
-
onUpdate(
|
|
90
|
+
this._options.onUpdate?.(update);
|
|
83
91
|
}, this._options.recalculationDelay);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
// TODO(burdon): Remove?
|
|
87
94
|
get space() {
|
|
88
95
|
return this._space;
|
|
89
96
|
}
|
|
@@ -136,7 +143,7 @@ export class FunctionContext {
|
|
|
136
143
|
const value = await cb(...args);
|
|
137
144
|
this._cache.set(invocationKey, { value, ts: Date.now() });
|
|
138
145
|
log('set', { cell, value });
|
|
139
|
-
this._onUpdate();
|
|
146
|
+
this._onUpdate({ name, cell });
|
|
140
147
|
} catch (err) {
|
|
141
148
|
// TODO(burdon): Show error to user.
|
|
142
149
|
log.warn('failed', { cell, err });
|
|
@@ -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 { testPlugins } from './testing';
|
|
16
17
|
import { createSheet } from '../defs';
|
|
17
18
|
import { useComputeGraph, useSheetModel } from '../hooks';
|
|
18
|
-
import {
|
|
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(
|
|
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
|
-
|
|
87
|
+
withComputeGraphDecorator({ plugins: testPlugins }),
|
|
87
88
|
withTheme,
|
|
88
89
|
],
|
|
89
90
|
render: (args: any) => <Story {...args} />,
|
|
@@ -0,0 +1,127 @@
|
|
|
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 { Client } from '@dxos/client';
|
|
10
|
+
import { create, fullyQualifiedId } from '@dxos/client/echo';
|
|
11
|
+
import { Context } from '@dxos/context';
|
|
12
|
+
import { type S } from '@dxos/echo-schema';
|
|
13
|
+
import { FunctionType } from '@dxos/plugin-script/types';
|
|
14
|
+
|
|
15
|
+
import { ComputeGraphRegistry } from './compute-graph';
|
|
16
|
+
import { testPlugins } from './testing';
|
|
17
|
+
import { addressFromA1Notation, createSheet } from '../defs';
|
|
18
|
+
import { SheetModel } from '../model';
|
|
19
|
+
import { type CellScalarValue } from '../types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* NOTE: Browser test required for hyperformula due to raw translation files.
|
|
23
|
+
*/
|
|
24
|
+
describe('compute graph', () => {
|
|
25
|
+
let ctx: Context;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
ctx = new Context();
|
|
28
|
+
});
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await ctx.dispose();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// TODO(burdon): Replace with builder.
|
|
34
|
+
const createModel = async (types?: S.Schema<any>[]) => {
|
|
35
|
+
const client = new Client();
|
|
36
|
+
if (types) {
|
|
37
|
+
// TODO(burdon): Add to config.
|
|
38
|
+
client.addTypes(types);
|
|
39
|
+
}
|
|
40
|
+
await client.initialize();
|
|
41
|
+
await client.halo.createIdentity();
|
|
42
|
+
const space = await client.spaces.create();
|
|
43
|
+
ctx.onDispose(() => client.destroy());
|
|
44
|
+
|
|
45
|
+
const registry = new ComputeGraphRegistry({ plugins: testPlugins });
|
|
46
|
+
await registry.open();
|
|
47
|
+
ctx.onDispose(() => registry.close());
|
|
48
|
+
|
|
49
|
+
const graph = await registry.createGraph(space);
|
|
50
|
+
|
|
51
|
+
const sheet = createSheet({ rows: 5, columns: 5 });
|
|
52
|
+
const model = new SheetModel(graph, sheet);
|
|
53
|
+
await model.open();
|
|
54
|
+
|
|
55
|
+
return { space, graph, model };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
test('map functions', async () => {
|
|
59
|
+
const { space, graph } = await createModel([FunctionType]);
|
|
60
|
+
|
|
61
|
+
// Create script.
|
|
62
|
+
const trigger = new Trigger();
|
|
63
|
+
graph.update.once(() => trigger.wake());
|
|
64
|
+
const fn = space.db.add(create(FunctionType, { version: 1, binding: 'TEST' }));
|
|
65
|
+
await trigger.wait();
|
|
66
|
+
expect(graph.getFunctions({ echo: true })).to.toHaveLength(1);
|
|
67
|
+
|
|
68
|
+
const id = graph.mapFunctionBindingToId('TEST()');
|
|
69
|
+
expect(id).to.eq(`${fullyQualifiedId(fn)}()`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('cross-node references', async () => {
|
|
73
|
+
const { graph } = await createModel();
|
|
74
|
+
|
|
75
|
+
// Create nodes.
|
|
76
|
+
const node1 = await graph.getOrCreateNode('node-1');
|
|
77
|
+
const node2 = await graph.getOrCreateNode('node-2');
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
expect(graph.hf.getSheetNames()).to.toHaveLength(3);
|
|
81
|
+
node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, [[100, 200, 300, '=SUM(A1:C1)']]);
|
|
82
|
+
node2.graph.hf.setCellContents({ sheet: node2.sheetId, row: 0, col: 0 }, "='node-1'!D1");
|
|
83
|
+
const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 3, row: 0 });
|
|
84
|
+
const value2 = node2.graph.hf.getCellValue({ sheet: node2.sheetId, col: 0, row: 0 });
|
|
85
|
+
expect(value1).to.eq(value2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get updated event.
|
|
89
|
+
const trigger = new Trigger<CellValue>();
|
|
90
|
+
node2.update.on(({ change }) => {
|
|
91
|
+
const value = node2.graph.hf.getCellValue({ sheet: node2.sheetId, col: 0, row: 0 });
|
|
92
|
+
expect(value).to.eq(change?.newValue);
|
|
93
|
+
trigger.wake(value);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, 400);
|
|
98
|
+
const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 3, row: 0 });
|
|
99
|
+
const value2 = await trigger.wait();
|
|
100
|
+
expect(value1).to.eq(value2);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('async function', async () => {
|
|
105
|
+
const { graph, model } = await createModel();
|
|
106
|
+
|
|
107
|
+
// Triggers function.
|
|
108
|
+
model.setValue(addressFromA1Notation('A1'), '=TEST()');
|
|
109
|
+
const trigger = new Trigger<CellScalarValue>();
|
|
110
|
+
model.update.once(({ type }) => {
|
|
111
|
+
if (type === 'valuesUpdated') {
|
|
112
|
+
const value = model.getValue(addressFromA1Notation('A1'));
|
|
113
|
+
trigger.wake(value);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Get initial value (null).
|
|
118
|
+
const v1 = model.getValue(addressFromA1Notation('A1'));
|
|
119
|
+
expect(v1).to.be.null;
|
|
120
|
+
expect(graph.context.info.invocations.TEST).to.eq(undefined);
|
|
121
|
+
|
|
122
|
+
// Wait until async update triggered.
|
|
123
|
+
const v2 = await trigger.wait();
|
|
124
|
+
expect(v2).not.to.be.null;
|
|
125
|
+
expect(graph.context.info.invocations.TEST).to.eq(1);
|
|
126
|
+
});
|
|
127
|
+
});
|