@dxos/react-ui-editor 0.3.11-main.c95bc86 → 0.3.11-main.cb8120f

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 (57) hide show
  1. package/dist/lib/browser/index.mjs +434 -210
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -0
  7. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  8. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +14 -0
  9. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  10. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  11. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  12. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  13. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  14. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  15. package/dist/types/src/extensions/automerge/defs.d.ts +5 -5
  16. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/semaphore.d.ts +2 -1
  18. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  19. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  20. package/dist/types/src/extensions/awareness.d.ts +1 -1
  21. package/dist/types/src/extensions/comments.d.ts +5 -1
  22. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  23. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -1
  24. package/dist/types/src/extensions/markdown/formatting.d.ts +11 -0
  25. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  26. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  27. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  29. package/dist/types/src/hooks/index.d.ts +1 -0
  30. package/dist/types/src/hooks/index.d.ts.map +1 -1
  31. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  32. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  33. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  34. package/package.json +36 -24
  35. package/src/components/TextEditor/MarkdownEditor.stories.tsx +8 -13
  36. package/src/components/TextEditor/TextEditor.tsx +3 -1
  37. package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
  38. package/src/components/Toolbar/Toolbar.tsx +115 -0
  39. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  40. package/src/extensions/automerge/automerge.test.ts +13 -0
  41. package/src/extensions/automerge/automerge.ts +26 -11
  42. package/src/extensions/automerge/cursor.ts +3 -3
  43. package/src/extensions/automerge/defs.ts +9 -9
  44. package/src/extensions/automerge/semaphore.ts +17 -19
  45. package/src/extensions/automerge/update-automerge.ts +12 -6
  46. package/src/extensions/automerge/update-codemirror.ts +5 -5
  47. package/src/extensions/awareness.ts +3 -3
  48. package/src/extensions/comments.ts +112 -64
  49. package/src/extensions/markdown/code.ts +24 -9
  50. package/src/extensions/markdown/formatting.ts +159 -0
  51. package/src/extensions/markdown/heading.ts +1 -1
  52. package/src/extensions/markdown/index.ts +1 -0
  53. package/src/extensions/markdown/link.ts +4 -0
  54. package/src/hooks/index.ts +1 -0
  55. package/src/hooks/useActionHandler.ts +45 -0
  56. package/src/hooks/useTextModel.ts +11 -8
  57. package/src/themes/default.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.3.11-main.c95bc86",
3
+ "version": "0.3.11-main.cb8120f",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -35,53 +35,65 @@
35
35
  "lodash.pick": "^4.4.0",
36
36
  "lodash.sortby": "^4.7.0",
37
37
  "style-mod": "^4.1.0",
38
- "@dxos/async": "0.3.11-main.c95bc86",
39
- "@dxos/automerge": "0.3.11-main.c95bc86",
40
- "@dxos/context": "0.3.11-main.c95bc86",
41
- "@dxos/debug": "0.3.11-main.c95bc86",
42
- "@dxos/display-name": "0.3.11-main.c95bc86",
43
- "@dxos/log": "0.3.11-main.c95bc86",
44
- "@dxos/echo-schema": "0.3.11-main.c95bc86",
45
- "@dxos/invariant": "0.3.11-main.c95bc86",
46
- "@dxos/react-async": "0.3.11-main.c95bc86",
47
- "@dxos/react-ui": "0.3.11-main.c95bc86",
48
- "@dxos/react-ui-theme": "0.3.11-main.c95bc86",
49
- "@dxos/util": "0.3.11-main.c95bc86",
50
- "@dxos/protocols": "0.3.11-main.c95bc86",
51
- "@dxos/text-model": "0.3.11-main.c95bc86"
38
+ "@dxos/async": "0.3.11-main.cb8120f",
39
+ "@dxos/context": "0.3.11-main.cb8120f",
40
+ "@dxos/automerge": "0.3.11-main.cb8120f",
41
+ "@dxos/display-name": "0.3.11-main.cb8120f",
42
+ "@dxos/invariant": "0.3.11-main.cb8120f",
43
+ "@dxos/echo-schema": "0.3.11-main.cb8120f",
44
+ "@dxos/debug": "0.3.11-main.cb8120f",
45
+ "@dxos/log": "0.3.11-main.cb8120f",
46
+ "@dxos/protocols": "0.3.11-main.cb8120f",
47
+ "@dxos/react-async": "0.3.11-main.cb8120f",
48
+ "@dxos/react-ui": "0.3.11-main.cb8120f",
49
+ "@dxos/util": "0.3.11-main.cb8120f",
50
+ "@dxos/text-model": "0.3.11-main.cb8120f",
51
+ "@dxos/react-ui-theme": "0.3.11-main.cb8120f"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@automerge/automerge-repo-network-broadcastchannel": "^1.1.0",
55
55
  "@faker-js/faker": "^8.3.1",
56
56
  "@phosphor-icons/react": "^2.0.5",
57
57
  "@preact/signals-react": "^1.3.6",
58
+ "@testing-library/dom": "^8.17.1",
59
+ "@testing-library/react": "^13.4.0",
60
+ "@testing-library/user-event": "^14.4.3",
58
61
  "@types/lodash.defaultsdeep": "^4.6.6",
59
62
  "@types/lodash.get": "^4.4.7",
60
63
  "@types/lodash.pick": "^4.4.7",
61
64
  "@types/lodash.sortby": "^4.7.7",
62
65
  "@types/react": "^18.0.21",
63
66
  "@types/react-dom": "^18.0.6",
67
+ "@types/react-test-renderer": "^17.0.2",
68
+ "@vitest/ui": "^1.2.1",
64
69
  "deepsignal": "1.4.0-shallow.0",
70
+ "happy-dom": "^13.3.1",
71
+ "jsdom": "^20.0.1",
65
72
  "react": "^18.2.0",
66
73
  "react-dom": "^18.2.0",
67
- "@dxos/echo-typegen": "0.3.11-main.c95bc86",
68
- "@dxos/config": "0.3.11-main.c95bc86",
69
- "@dxos/keyboard": "0.3.11-main.c95bc86",
70
- "@dxos/echo-signals": "0.3.11-main.c95bc86",
71
- "@dxos/react-ui": "0.3.11-main.c95bc86",
72
- "@dxos/react-client": "0.3.11-main.c95bc86",
73
- "@dxos/storybook-utils": "0.3.11-main.c95bc86"
74
+ "react-test-renderer": "^18.2.0",
75
+ "vite": "^5.0.12",
76
+ "vite-plugin-top-level-await": "^1.4.1",
77
+ "vite-plugin-wasm": "^3.3.0",
78
+ "vitest": "^1.2.1",
79
+ "@dxos/config": "0.3.11-main.cb8120f",
80
+ "@dxos/echo-signals": "0.3.11-main.cb8120f",
81
+ "@dxos/echo-typegen": "0.3.11-main.cb8120f",
82
+ "@dxos/keyboard": "0.3.11-main.cb8120f",
83
+ "@dxos/react-ui": "0.3.11-main.cb8120f",
84
+ "@dxos/react-client": "0.3.11-main.cb8120f",
85
+ "@dxos/storybook-utils": "0.3.11-main.cb8120f"
74
86
  },
75
87
  "peerDependencies": {
76
88
  "@phosphor-icons/react": "^2.0.5",
77
89
  "react": "^18.2.0",
78
90
  "react-dom": "^18.2.0",
79
- "@dxos/react-client": "0.3.11-main.c95bc86"
91
+ "@dxos/react-client": "0.3.11-main.cb8120f"
80
92
  },
81
93
  "publishConfig": {
82
94
  "access": "public"
83
95
  },
84
96
  "scripts": {
85
- "test": "true"
97
+ "vitest": "vitest --ui --dir ./src/extensions/automerge"
86
98
  }
87
99
  }
@@ -19,11 +19,14 @@ import { withTheme } from '@dxos/storybook-utils';
19
19
 
20
20
  import { MarkdownEditor, type TextEditorProps } from './TextEditor';
21
21
  import {
22
+ type CommentsOptions,
23
+ type LinkOptions,
22
24
  autocomplete,
23
25
  blast,
24
26
  code,
25
27
  comments,
26
28
  defaultOptions,
29
+ heading,
27
30
  hr,
28
31
  image,
29
32
  link,
@@ -31,17 +34,11 @@ import {
31
34
  table,
32
35
  tasklist,
33
36
  typewriter,
34
- type CommentsOptions,
35
- type LinkOptions,
36
37
  useComments,
37
- heading,
38
+ formatting,
38
39
  } from '../../extensions';
39
40
  import { type Comment, useTextModel } from '../../hooks';
40
41
 
41
- // Extensions:
42
- // TODO(burdon): Table of contents.
43
- // TODO(burdon): Front-matter
44
-
45
42
  faker.seed(101);
46
43
 
47
44
  const str = (...lines: string[]) => lines.join('\n');
@@ -249,13 +246,11 @@ const defaults = [
249
246
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
250
247
  }),
251
248
  code(),
249
+ formatting(),
252
250
  heading(),
253
251
  hr(),
254
252
  image(),
255
253
  link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
256
- // mention({
257
- // onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
258
- // }),
259
254
  table(),
260
255
  tasklist(),
261
256
  ];
@@ -347,18 +342,18 @@ export const Mention = {
347
342
 
348
343
  export const Comments = {
349
344
  render: () => {
350
- const [commentRanges, setCommentRanges] = useState<Comment[]>([]);
345
+ const [_comments, setComments] = useState<Comment[]>([]);
351
346
 
352
347
  return (
353
348
  <Story
354
349
  text={str('# Comments', '', text.paragraphs, text.footer)}
355
- comments={commentRanges}
350
+ comments={_comments}
356
351
  extensions={[
357
352
  comments({
358
353
  onHover: onCommentsHover,
359
354
  onCreate: (range) => {
360
355
  const id = PublicKey.random().toHex();
361
- setCommentRanges((commentRanges) => [...commentRanges, { id, cursor: range }]);
356
+ setComments((commentRanges) => [...commentRanges, { id, cursor: range }]);
362
357
  return id;
363
358
  },
364
359
  onSelect: (state) => {
@@ -21,6 +21,7 @@ import React, {
21
21
  import { log } from '@dxos/log';
22
22
  import { useThemeContext } from '@dxos/react-ui';
23
23
  import { attentionSurface, mx } from '@dxos/react-ui-theme';
24
+ import { nonNullable } from '@dxos/util';
24
25
 
25
26
  import { basicBundle, markdownBundle } from '../../extensions';
26
27
  import { type EditorModel } from '../../hooks';
@@ -132,6 +133,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
132
133
  EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
133
134
 
134
135
  // State.
136
+ EditorView.editable.of(!readonly),
135
137
  EditorState.readOnly.of(!!readonly),
136
138
 
137
139
  // Storage and replication.
@@ -143,7 +145,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
143
145
 
144
146
  // Custom.
145
147
  ...extensions,
146
- ].filter(Boolean) as Extension[],
148
+ ].filter(nonNullable) as Extension[], // TODO(burdon): ???
147
149
  });
148
150
 
149
151
  //
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { faker } from '@faker-js/faker';
8
+ import React, { type FC, useMemo, useState } from 'react';
9
+
10
+ import { TextObject } from '@dxos/echo-schema';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
13
+ import { withTheme } from '@dxos/storybook-utils';
14
+
15
+ import { Toolbar } from './Toolbar';
16
+ import { code, comments, formatting, heading, image, table, tasklist, useComments } from '../../extensions';
17
+ import { type Comment, useActionHandler, useTextEditor, useTextModel } from '../../hooks';
18
+ import { MarkdownEditor } from '../TextEditor';
19
+
20
+ faker.seed(101);
21
+
22
+ const Story: FC<{ content: string }> = ({ content }) => {
23
+ const [item] = useState({ text: new TextObject(content) });
24
+ const [_comments, setComments] = useState<Comment[]>([]);
25
+ const [editorRef, editorView] = useTextEditor();
26
+ const model = useTextModel({ text: item.text });
27
+ const extensions = useMemo(
28
+ () => [
29
+ code(),
30
+ comments({
31
+ onCreate: (cursor) => {
32
+ const id = PublicKey.random().toHex();
33
+ setComments((comments) => [...comments, { id, cursor }]);
34
+ return id;
35
+ },
36
+ }),
37
+ formatting(),
38
+ heading(),
39
+ image(),
40
+ table(),
41
+ tasklist(),
42
+ ],
43
+ [],
44
+ );
45
+
46
+ useComments(editorView, _comments);
47
+ const handleAction = useActionHandler(editorView);
48
+
49
+ if (!model) {
50
+ return null;
51
+ }
52
+
53
+ return (
54
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
55
+ <div className='flex h-full justify-center'>
56
+ <div className='flex flex-col h-full w-[800px]'>
57
+ <Toolbar onAction={handleAction} />
58
+ <MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export default {
66
+ title: 'react-ui-editor/Toolbar',
67
+ component: Toolbar,
68
+ render: (args: any) => <Story {...args} />,
69
+ decorators: [withTheme],
70
+ };
71
+
72
+ const content = [
73
+ 'Demo',
74
+ '',
75
+ 'The editor supports **Markdown** styles.',
76
+ '',
77
+ faker.lorem.paragraph({ min: 5, max: 8 }),
78
+ '',
79
+ '',
80
+ ].join('\n');
81
+
82
+ export const Default = {
83
+ args: {
84
+ content,
85
+ },
86
+ };
@@ -0,0 +1,115 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import {
6
+ type Icon,
7
+ At,
8
+ Brain,
9
+ CaretRight,
10
+ ChatText,
11
+ Code,
12
+ Image,
13
+ Link,
14
+ ListBullets,
15
+ ListChecks,
16
+ ListNumbers,
17
+ TextStrikethrough,
18
+ Table,
19
+ TextB,
20
+ TextItalic,
21
+ } from '@phosphor-icons/react';
22
+ import React, { type FC } from 'react';
23
+
24
+ import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
25
+ import { getSize } from '@dxos/react-ui-theme';
26
+
27
+ export type Action =
28
+ | 'blockquote'
29
+ | 'bold'
30
+ | 'code'
31
+ | 'comment'
32
+ | 'heading'
33
+ | 'image'
34
+ | 'italic'
35
+ | 'link'
36
+ | 'list'
37
+ | 'list-ordered'
38
+ | 'list-tasks'
39
+ | 'mention'
40
+ | 'prompt'
41
+ | 'strikethrough'
42
+ | 'table';
43
+
44
+ export type ToolbarProps = {
45
+ onAction?: (action: { type: Action; data?: any }) => void;
46
+ };
47
+
48
+ const IconButton: FC<{ Icon: Icon; title: string } & NonNullable<Pick<ButtonProps, 'onClick'>>> = ({
49
+ Icon,
50
+ title,
51
+ onClick,
52
+ }) => (
53
+ <Button variant='ghost' classNames='p-2' onClick={onClick} title={title}>
54
+ <Icon className={getSize(5)} />
55
+ </Button>
56
+ );
57
+
58
+ // TODO(burdon): Update state based on current selection?
59
+ export const Toolbar = ({ onAction }: ToolbarProps) => {
60
+ return (
61
+ <DensityProvider density='fine'>
62
+ <div role='toolbar' className='flex w-full p-2 items-center gap-4'>
63
+ <div>
64
+ <Select.Root
65
+ defaultValue='0'
66
+ onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
67
+ >
68
+ <Select.TriggerButton classNames='w-[8rem]' />
69
+ <Select.Portal>
70
+ <Select.Content>
71
+ <Select.Viewport>
72
+ <Select.Option value='0'>Paragraph</Select.Option>
73
+ {[1, 2, 3, 4, 5, 6].map((level) => (
74
+ <Select.Option key={level} value={String(level)}>
75
+ Heading {level}
76
+ </Select.Option>
77
+ ))}
78
+ </Select.Viewport>
79
+ </Select.Content>
80
+ </Select.Portal>
81
+ </Select.Root>
82
+ </div>
83
+ <div>
84
+ <IconButton Icon={TextB} title='Bold' onClick={() => onAction?.({ type: 'bold' })} />
85
+ <IconButton Icon={TextItalic} title='Italic' onClick={() => onAction?.({ type: 'italic' })} />
86
+ <IconButton
87
+ Icon={TextStrikethrough}
88
+ title='Strike-through'
89
+ onClick={() => onAction?.({ type: 'strikethrough' })}
90
+ />
91
+ </div>
92
+ <div>
93
+ <IconButton Icon={ListBullets} title='Bullet list' onClick={() => onAction?.({ type: 'list' })} />
94
+ <IconButton Icon={ListNumbers} title='Numbered list' onClick={() => onAction?.({ type: 'list-ordered' })} />
95
+ <IconButton Icon={ListChecks} title='Task list' onClick={() => onAction?.({ type: 'list-tasks' })} />
96
+ </div>
97
+ <div>
98
+ <IconButton Icon={CaretRight} title='Block quite' onClick={() => onAction?.({ type: 'blockquote' })} />
99
+ <IconButton Icon={Code} title='Code block' onClick={() => onAction?.({ type: 'code' })} />
100
+ <IconButton Icon={Table} title='Table' onClick={() => onAction?.({ type: 'table' })} />
101
+ </div>
102
+ <div>
103
+ <IconButton Icon={Link} title='Link' onClick={() => onAction?.({ type: 'link' })} />
104
+ <IconButton Icon={At} title='Mention' onClick={() => onAction?.({ type: 'mention' })} />
105
+ <IconButton Icon={Image} title='Image' onClick={() => onAction?.({ type: 'image' })} />
106
+ </div>
107
+ <div className='grow' />
108
+ <div>
109
+ <IconButton Icon={Brain} title='AI prompt' onClick={() => onAction?.({ type: 'prompt' })} />
110
+ <IconButton Icon={ChatText} title='Create comment' onClick={() => onAction?.({ type: 'comment' })} />
111
+ </div>
112
+ </div>
113
+ </DensityProvider>
114
+ );
115
+ };
@@ -0,0 +1,76 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { EditorState } from '@codemirror/state';
6
+ import { EditorView } from '@codemirror/view';
7
+ import { render, screen } from '@testing-library/react';
8
+ import chai, { expect } from 'chai';
9
+ import chaiDom from 'chai-dom';
10
+ import get from 'lodash.get';
11
+ import React, { type FC, useEffect, useRef, useState } from 'react';
12
+ import { describe, test } from 'vitest';
13
+
14
+ import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
15
+
16
+ import { automerge } from './automerge';
17
+
18
+ type TestObject = {
19
+ text: string;
20
+ };
21
+
22
+ const path = ['text'];
23
+
24
+ class Generator {
25
+ constructor(private readonly _handle: DocHandle<TestObject>) {}
26
+ update(text: string) {
27
+ this._handle.change((doc: TestObject) => {
28
+ doc.text = text;
29
+ });
30
+ }
31
+ }
32
+
33
+ const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ handle, generator }) => {
34
+ const ref = useRef<HTMLDivElement>(null);
35
+ const [view, setView] = useState<EditorView>();
36
+ useEffect(() => {
37
+ const extensions = [
38
+ automerge({ handle, path }),
39
+ EditorView.updateListener.of((update) => {
40
+ if (view.state.doc.toString() === 'hello!') {
41
+ // Update editor.
42
+ update.view.dispatch({ changes: { from: view.state.doc.length - 1, insert: ' world' } });
43
+ }
44
+ }),
45
+ ];
46
+
47
+ const view = new EditorView({
48
+ state: EditorState.create({ doc: get(handle.docSync()!, path), extensions }),
49
+ parent: ref.current!,
50
+ });
51
+
52
+ setView(view);
53
+ }, []);
54
+
55
+ useEffect(() => {}, [view]);
56
+
57
+ return <div ref={ref} />;
58
+ };
59
+
60
+ chai.use(chaiDom);
61
+
62
+ describe('Automerge', () => {
63
+ test('basic sync', () => {
64
+ const repo = new Repo({ network: [] });
65
+ const handle = repo.create<TestObject>();
66
+ const generator = new Generator(handle);
67
+ render(<Test handle={handle} generator={generator} />);
68
+ expect(screen.getByRole('textbox')).to.have.text('');
69
+
70
+ generator.update('hello!');
71
+ expect(screen.getByRole('textbox')).to.have.text('hello world!');
72
+ });
73
+
74
+ // TODO(burdon): Test history/undo.
75
+ // TODO(burdon): https://testing-library.com/docs/react-testing-library/example-intro/
76
+ });
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { describe, test } from '@dxos/test';
8
+
9
+ describe('Test', () => {
10
+ test('checks sanity', async () => {
11
+ expect(true).to.be.true;
12
+ });
13
+ });
@@ -4,14 +4,14 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { StateField, type Extension } from '@codemirror/state';
7
+ import { invertedEffects } from '@codemirror/commands';
8
+ import { StateField, type Extension, type StateEffect } from '@codemirror/state';
8
9
  import { EditorView, ViewPlugin } from '@codemirror/view';
9
10
 
10
- import { type Prop, next as A } from '@dxos/automerge/automerge';
11
- import { invariant } from '@dxos/invariant';
11
+ import { next as A, type Prop } from '@dxos/automerge/automerge';
12
12
 
13
13
  import { cursorConverter } from './cursor';
14
- import { effectType, type IDocHandle, isReconcileTx, type State } from './defs';
14
+ import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
15
15
  import { PatchSemaphore } from './semaphore';
16
16
  import { Cursor } from '../cursor';
17
17
 
@@ -21,7 +21,7 @@ export type AutomergeOptions = {
21
21
  };
22
22
 
23
23
  export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
24
- const state = StateField.define<State>({
24
+ const syncState = StateField.define<State>({
25
25
  create: () => ({
26
26
  path: path.slice(),
27
27
  lastHeads: A.getHeads(handle.docSync()!),
@@ -37,7 +37,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
37
37
 
38
38
  let clearUnreconciled = false;
39
39
  for (const effect of tr.effects) {
40
- if (effect.is(effectType)) {
40
+ if (effect.is(updateHeadsEffect)) {
41
41
  result.lastHeads = effect.value.newHeads;
42
42
  clearUnreconciled = true;
43
43
  }
@@ -46,7 +46,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
46
46
  if (clearUnreconciled) {
47
47
  result.unreconciledTransactions = [];
48
48
  } else {
49
- if (!isReconcileTx(tr)) {
49
+ if (!isReconcile(tr)) {
50
50
  result.unreconciledTransactions.push(tr);
51
51
  }
52
52
  }
@@ -55,15 +55,16 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
55
55
  },
56
56
  });
57
57
 
58
- const semaphore = new PatchSemaphore(handle, state);
58
+ const semaphore = new PatchSemaphore(handle, syncState);
59
59
 
60
60
  return [
61
61
  Cursor.converter.of(cursorConverter(handle, path)),
62
+ syncState,
63
+
62
64
  // Reconcile external updates.
63
65
  ViewPlugin.fromClass(
64
66
  class {
65
67
  constructor(private readonly _view: EditorView) {
66
- invariant(this._view);
67
68
  handle.addListener('change', this._handleChange.bind(this));
68
69
  }
69
70
 
@@ -76,13 +77,27 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
76
77
  }
77
78
  },
78
79
  ),
80
+
79
81
  // Reconcile local updates.
80
82
  EditorView.updateListener.of(({ view, changes }) => {
81
83
  if (!changes.empty) {
82
- // TODO(burdon): Loses cursor position if auto closing brackets. Call explicitly?
83
84
  semaphore.reconcile(view);
84
85
  }
85
86
  }),
86
- state,
87
+
88
+ // TODO(burdon): Record undo transactions.
89
+ // See https://github.com/yjs/y-codemirror.next/tree/main
90
+ // https://codemirror.net/examples/inverted-effect
91
+ invertedEffects.of((tr) => {
92
+ // Each change results it three transactions: insert, delete, insert.
93
+ const effects: StateEffect<any>[] = [];
94
+ if (!tr.changes.empty) {
95
+ tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
96
+ // effects.push(effectType.of({});
97
+ });
98
+ }
99
+
100
+ return effects;
101
+ }),
87
102
  ];
88
103
  };
@@ -5,7 +5,7 @@
5
5
  import get from 'lodash.get';
6
6
 
7
7
  import type { Prop } from '@dxos/automerge/automerge';
8
- import { next as automerge } from '@dxos/automerge/automerge';
8
+ import { next as A } from '@dxos/automerge/automerge';
9
9
 
10
10
  import { type IDocHandle } from './defs';
11
11
  import { type CursorConverter } from '../cursor';
@@ -24,7 +24,7 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
24
24
  }
25
25
 
26
26
  // NOTE: Slice is needed because getCursor mutates the array.
27
- return automerge.getCursor(doc, path.slice(), pos);
27
+ return A.getCursor(doc, path.slice(), pos);
28
28
  },
29
29
 
30
30
  fromCursor: (cursor) => {
@@ -47,6 +47,6 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
47
47
  }
48
48
 
49
49
  // NOTE: Slice is needed because getCursor mutates the array.
50
- return automerge.getCursorPosition(doc, path.slice(), cursor);
50
+ return A.getCursorPosition(doc, path.slice(), cursor);
51
51
  },
52
52
  });
@@ -8,28 +8,28 @@ import { Annotation, StateEffect, type StateField, type EditorState, type Transa
8
8
 
9
9
  import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
10
10
 
11
- // TODO(burdon): Rename.
12
11
  export type State = {
13
12
  path: Prop[];
14
13
  lastHeads: Heads;
15
14
  unreconciledTransactions: Transaction[];
16
15
  };
17
16
 
17
+ export const getPath = (state: EditorState, field: StateField<State>): Prop[] => state.field(field).path;
18
+ export const getLastHeads = (state: EditorState, field: StateField<State>): Heads => state.field(field).lastHeads;
19
+
18
20
  export type UpdateHeads = {
19
21
  newHeads: Heads;
20
22
  };
21
23
 
22
- export const effectType = StateEffect.define<UpdateHeads>({});
23
-
24
- export const reconcileAnnotationType = Annotation.define<unknown>();
24
+ export const updateHeadsEffect = StateEffect.define<UpdateHeads>({});
25
25
 
26
- export const getPath = (state: EditorState, field: StateField<State>): Prop[] => state.field(field).path;
27
-
28
- export const getLastHeads = (state: EditorState, field: StateField<State>): Heads => state.field(field).lastHeads;
26
+ export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => updateHeadsEffect.of({ newHeads });
29
27
 
30
- export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
28
+ export const reconcileAnnotation = Annotation.define<boolean>();
31
29
 
32
- export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
30
+ export const isReconcile = (tr: Transaction): boolean => {
31
+ return !!tr.annotation(reconcileAnnotation);
32
+ };
33
33
 
34
34
  export type IDocHandle<T = any> = {
35
35
  docSync(): Doc<T> | undefined;