@dxos/react-ui-editor 0.3.10 → 0.3.11-main.0e40963

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 (141) hide show
  1. package/dist/lib/browser/index.mjs +1971 -1744
  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 +14 -10
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  9. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
  11. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  12. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  13. package/dist/types/src/extensions/code.d.ts.map +1 -0
  14. package/dist/types/src/extensions/comments.d.ts +48 -0
  15. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  16. package/dist/types/src/extensions/hr.d.ts +2 -0
  17. package/dist/types/src/extensions/hr.d.ts.map +1 -0
  18. package/dist/types/src/extensions/image.d.ts.map +1 -0
  19. package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +3 -3
  20. package/dist/types/src/extensions/index.d.ts.map +1 -0
  21. package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
  22. package/dist/types/src/extensions/link.d.ts.map +1 -0
  23. package/dist/types/src/extensions/listener.d.ts +10 -0
  24. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  25. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  26. package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -1
  27. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  28. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  29. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  30. package/dist/types/src/extensions/mermaid.d.ts +3 -0
  31. package/dist/types/src/extensions/mermaid.d.ts.map +1 -0
  32. package/dist/types/src/extensions/table.d.ts.map +1 -0
  33. package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
  34. package/dist/types/src/{components/TextEditor/extensions/demo.d.ts → extensions/typewriter.d.ts} +2 -3
  35. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  36. package/dist/types/src/extensions/util.d.ts +5 -0
  37. package/dist/types/src/extensions/util.d.ts.map +1 -0
  38. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +2 -3
  39. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  40. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
  41. package/dist/types/src/hooks/automerge/{PatchSemaphore.d.ts → semaphore.d.ts} +1 -1
  42. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +1 -0
  43. package/dist/types/src/hooks/automerge/update-automerge.d.ts +6 -0
  44. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +1 -0
  45. package/dist/types/src/hooks/automerge/update-codemirror.d.ts +5 -0
  46. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +1 -0
  47. package/dist/types/src/hooks/useTextModel.d.ts +14 -6
  48. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  49. package/dist/types/src/hooks/yjs/yjs.test.d.ts +2 -0
  50. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +1 -0
  51. package/dist/types/src/index.d.ts +3 -0
  52. package/dist/types/src/index.d.ts.map +1 -1
  53. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  54. package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +5 -13
  55. package/dist/types/src/themes/default.d.ts.map +1 -0
  56. package/dist/types/src/themes/index.d.ts.map +1 -0
  57. package/package.json +24 -20
  58. package/src/components/TextEditor/MarkdownEditor.stories.tsx +130 -71
  59. package/src/components/TextEditor/TextEditor.stories.tsx +2 -2
  60. package/src/components/TextEditor/TextEditor.tsx +29 -25
  61. package/src/components/TextEditor/index.ts +0 -3
  62. package/src/{components/TextEditor/extensions → extensions}/code.ts +13 -13
  63. package/src/extensions/comments.ts +391 -0
  64. package/src/extensions/hr.ts +74 -0
  65. package/src/{components/TextEditor/extensions → extensions}/image.ts +3 -1
  66. package/src/{components/TextEditor/extensions → extensions}/index.ts +3 -3
  67. package/src/{components/TextEditor/extensions → extensions}/link.ts +66 -10
  68. package/src/extensions/listener.ts +35 -0
  69. package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +6 -9
  70. package/src/extensions/mermaid.ts +11 -0
  71. package/src/{components/TextEditor/extensions → extensions}/table.ts +14 -1
  72. package/src/{components/TextEditor/extensions/demo.ts → extensions/typewriter.ts} +2 -3
  73. package/src/extensions/util.ts +18 -0
  74. package/src/hooks/automerge/automerge.stories.tsx +2 -3
  75. package/src/hooks/automerge/plugin.ts +1 -1
  76. package/src/hooks/automerge/{PatchSemaphore.ts → semaphore.ts} +4 -3
  77. package/src/hooks/automerge/{translation/codemirror-to-automerge.ts → update-automerge.ts} +3 -3
  78. package/src/hooks/automerge/{translation/automerge-to-codemirror.ts → update-codemirror.ts} +2 -7
  79. package/src/hooks/useTextModel.ts +60 -22
  80. package/src/hooks/yjs/yjs.test.ts +59 -0
  81. package/src/index.ts +3 -0
  82. package/src/testing/replicator.ts +0 -1
  83. package/src/{components/TextEditor/themes → themes}/default.ts +61 -38
  84. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  85. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  86. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  87. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
  88. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +0 -18
  89. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +0 -1
  91. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +0 -10
  92. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
  93. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +0 -24
  94. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +0 -1
  95. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  96. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  97. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  98. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
  99. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  100. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  101. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  102. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  103. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  104. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  105. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  106. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  107. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  108. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  109. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  110. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
  111. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +0 -5
  112. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +0 -1
  113. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +0 -6
  114. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +0 -1
  115. package/dist/types/src/hooks/automerge/translation/index.d.ts +0 -3
  116. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +0 -1
  117. package/src/components/TextEditor/extensions/comments.ts +0 -173
  118. package/src/components/TextEditor/extensions/experimental.tsx +0 -114
  119. package/src/components/TextEditor/extensions/highlight.ts +0 -128
  120. package/src/components/TextEditor/extensions/listener.ts +0 -23
  121. package/src/components/TextEditor/extensions/tooltip.tsx +0 -61
  122. package/src/hooks/automerge/translation/index.ts +0 -6
  123. /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
  124. /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
  125. /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
  126. /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
  127. /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
  128. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
  129. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
  130. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
  131. /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
  132. /package/dist/types/src/{components/TextEditor/extensions → extensions}/tasklist.d.ts +0 -0
  133. /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
  134. /package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +0 -0
  135. /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
  136. /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
  137. /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
  138. /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
  139. /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
  140. /package/src/{components/TextEditor/extensions → extensions}/tasklist.ts +0 -0
  141. /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
@@ -12,7 +12,11 @@ import {
12
12
  StateField,
13
13
  type Transaction,
14
14
  } from '@codemirror/state';
15
- import { Decoration, EditorView, WidgetType } from '@codemirror/view';
15
+ import { Decoration, EditorView, WidgetType, hoverTooltip } from '@codemirror/view';
16
+
17
+ import { tooltipContent } from '@dxos/react-ui-theme';
18
+
19
+ const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
16
20
 
17
21
  // Adapted from:
18
22
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
@@ -20,6 +24,7 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
20
24
 
21
25
  export type LinkOptions = {
22
26
  link?: boolean;
27
+ onHover?: (el: Element, url: string) => void;
23
28
  onRender?: (el: Element, url: string) => void;
24
29
  };
25
30
 
@@ -28,11 +33,62 @@ export type LinkOptions = {
28
33
  * https://codemirror.net/docs/ref/#state.StateField
29
34
  */
30
35
  export const link = (options: LinkOptions = {}): Extension => {
31
- return StateField.define<RangeSet<any>>({
32
- create: (state) => update(state, options),
33
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
34
- provide: (field) => EditorView.decorations.from(field),
35
- });
36
+ const extensions: Extension[] = [
37
+ StateField.define<RangeSet<any>>({
38
+ create: (state) => update(state, options),
39
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
40
+ provide: (field) => EditorView.decorations.from(field),
41
+ }),
42
+ ];
43
+
44
+ if (options.onHover) {
45
+ extensions.push(
46
+ // https://codemirror.net/examples/tooltip
47
+ // https://codemirror.net/docs/ref/#view.hoverTooltip
48
+ // https://github.com/codemirror/view/blob/main/src/tooltip.ts
49
+ hoverTooltip((view, pos) => {
50
+ const { from, text } = view.state.doc.lineAt(pos);
51
+ const p = pos - from;
52
+
53
+ let idx = 0;
54
+ let match;
55
+ do {
56
+ match = text.substring(idx).match(markdownLinkRegexp);
57
+ if (!match) {
58
+ match = undefined;
59
+ break;
60
+ }
61
+
62
+ idx = text.indexOf(match[0]);
63
+ if (p >= idx && p < idx + match[0].length) {
64
+ break;
65
+ }
66
+
67
+ idx += match[0].length;
68
+ } while (true);
69
+
70
+ if (!match) {
71
+ return null;
72
+ }
73
+
74
+ const [, , url] = match ?? [];
75
+
76
+ return {
77
+ pos: idx + from,
78
+ end: idx + from + match[0].length,
79
+ above: true,
80
+ create: () => {
81
+ const el = document.createElement('div');
82
+ el.className = tooltipContent({}, 'pli-2 plb-1');
83
+ options.onHover!(el, url);
84
+ return { dom: el, offset: { x: 0, y: 4 } };
85
+ },
86
+ };
87
+ }),
88
+ );
89
+ }
90
+
91
+ return extensions;
36
92
  };
37
93
 
38
94
  class LinkButton extends WidgetType {
@@ -56,12 +112,12 @@ class LinkButton extends WidgetType {
56
112
  }
57
113
 
58
114
  class LinkText extends WidgetType {
59
- constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
115
+ constructor(private readonly _text: string, private readonly _url?: string) {
60
116
  super();
61
117
  }
62
118
 
63
119
  override eq(other: LinkText) {
64
- return this._pos === other._pos && this._url === other._url;
120
+ return this._url === other._url;
65
121
  }
66
122
 
67
123
  toDOM(view: EditorView) {
@@ -76,7 +132,7 @@ class LinkText extends WidgetType {
76
132
  link.onclick = () => {
77
133
  view.dispatch({
78
134
  selection: {
79
- anchor: this._pos,
135
+ anchor: view.posAtDOM(link),
80
136
  },
81
137
  });
82
138
  };
@@ -112,7 +168,7 @@ const update = (state: EditorState, options: LinkOptions) => {
112
168
  node.from,
113
169
  node.to,
114
170
  Decoration.replace({
115
- widget: new LinkText(node.from, text, options.link ? url : undefined),
171
+ widget: new LinkText(text, options.link ? url : undefined),
116
172
  }),
117
173
  );
118
174
  }
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { EditorView } from '@codemirror/view';
7
+
8
+ export type ListenerOptions = {
9
+ onFocus?: (focused: boolean) => void;
10
+ onChange?: (text: string) => void;
11
+ };
12
+
13
+ /**
14
+ * Event listener.
15
+ */
16
+ export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
17
+ const extensions: Extension[] = [];
18
+
19
+ onFocus &&
20
+ extensions.push(
21
+ EditorView.focusChangeEffect.of((_, focused) => {
22
+ onFocus(focused);
23
+ return null;
24
+ }),
25
+ );
26
+
27
+ onChange &&
28
+ extensions.push(
29
+ EditorView.updateListener.of((update) => {
30
+ onChange(update.state.doc.toString());
31
+ }),
32
+ );
33
+
34
+ return extensions;
35
+ };
@@ -8,21 +8,18 @@ import { tags, styleTags, Tag } from '@lezer/highlight';
8
8
  import { type MarkdownConfig, Table } from '@lezer/markdown';
9
9
  import get from 'lodash.get';
10
10
 
11
- import { mx } from '@dxos/react-ui-theme';
12
-
13
11
  import {
14
12
  blockquote,
15
13
  bold,
16
14
  code,
17
15
  codeMark,
18
16
  heading,
19
- horizontalRule,
20
17
  inlineUrl,
21
18
  italic,
22
19
  mark,
23
20
  strikethrough,
24
21
  tokens,
25
- } from '../../../../styles';
22
+ } from '../../styles';
26
23
 
27
24
  /**
28
25
  * Custom tags defined and processed by the GFM lezer extension.
@@ -35,7 +32,7 @@ export const markdownTags = {
35
32
  CodeText: Tag.define(),
36
33
  EmphasisMark: Tag.define(),
37
34
  HeaderMark: Tag.define(),
38
- HorizontalRule: Tag.define(),
35
+ // HorizontalRule: Tag.define(),
39
36
  InlineCode: Tag.define(),
40
37
  LinkLabel: Tag.define(),
41
38
  LinkReference: Tag.define(),
@@ -182,10 +179,10 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
182
179
  },
183
180
 
184
181
  // TODO(burdon): Replace with extension.
185
- {
186
- tag: [markdownTags.HorizontalRule],
187
- class: mx(horizontalRule, readonly && '-indent-[100rem]'),
188
- },
182
+ // {
183
+ // tag: [markdownTags.HorizontalRule],
184
+ // class: mx(horizontalRule, readonly && '-indent-[100rem]'),
185
+ // },
189
186
 
190
187
  // TODO(burdon): Only if being edited.
191
188
  {
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+
7
+ // TODO(burdon): How will this interact with the code extension?
8
+ // TODO(burdon): Move to mermaid-plugin (which should contribute this extension).
9
+ export const mermaid = (): Extension => {
10
+ return [];
11
+ };
@@ -9,6 +9,9 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
9
  // TODO(burdon): Snippet to create basic table.
10
10
  // https://codemirror.net/docs/ref/#autocomplete.snippet
11
11
 
12
+ // TODO(burdon): Advanced formatting (left/right/center).
13
+ // TODO(burdon): Editor to auto balance columns.
14
+
12
15
  export type TableOptions = {};
13
16
 
14
17
  /**
@@ -73,7 +76,17 @@ const update = (state: EditorState, options: TableOptions) => {
73
76
 
74
77
  tables.forEach((table) => {
75
78
  const hide = state.readOnly || cursor < table.from || cursor > table.to;
76
- hide && builder.add(table.from, table.to, Decoration.replace({ widget: new TableWidget(table) }));
79
+ hide &&
80
+ builder.add(
81
+ table.from,
82
+ table.to,
83
+ Decoration.replace({
84
+ block: true,
85
+ widget: new TableWidget(table),
86
+ }),
87
+ );
88
+
89
+ // Add class for styling.
77
90
  builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
78
91
  });
79
92
 
@@ -15,12 +15,11 @@ export type DemoOptions = {
15
15
  const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
16
16
 
17
17
  /**
18
- * Demo script.
19
18
  * Configurable plugin that let's user cycle through pre-configured input script.
20
19
  */
21
- export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
20
+ export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
22
21
  let t: any;
23
- let idx = 0;
22
+ let idx = 0; // TODO(burdon): Make global.
24
23
 
25
24
  return [
26
25
  keymap.of([
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { log } from '@dxos/log';
6
+
7
+ /**
8
+ * CodeMirror callbacks swallow errors so wrap handlers.
9
+ */
10
+ // TODO(burdon): Figure out how to systematize this.
11
+ export const callbackWrapper = <T extends Function>(fn: T): T =>
12
+ ((...args: any[]) => {
13
+ try {
14
+ return fn(...args);
15
+ } catch (error) {
16
+ log.catch(error);
17
+ }
18
+ }) as unknown as T;
@@ -85,8 +85,7 @@ const Story = () => {
85
85
  export default {
86
86
  title: 'react-ui-editor/Automerge',
87
87
  component: Editor,
88
- };
89
-
90
- export const Default = {
91
88
  render: () => <Story />,
92
89
  };
90
+
91
+ export const Default = {};
@@ -17,8 +17,8 @@ import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '
17
17
  import * as automerge from '@dxos/automerge/automerge';
18
18
  import { type Heads, type Prop } from '@dxos/automerge/automerge';
19
19
 
20
- import { PatchSemaphore } from './PatchSemaphore';
21
20
  import { type IDocHandle } from './handle';
21
+ import { PatchSemaphore } from './semaphore';
22
22
 
23
23
  export type Value = {
24
24
  lastHeads: Heads;
@@ -10,7 +10,8 @@ import { next as automerge } from '@dxos/automerge/automerge';
10
10
 
11
11
  import { type IDocHandle } from './handle';
12
12
  import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
13
- import { automergeToCodemirror, codemirrorToAutomerge } from './translation';
13
+ import { updateAutomerge } from './update-automerge';
14
+ import { updateCodeMirror } from './update-codemirror';
14
15
 
15
16
  type Doc<T> = automerge.Doc<T>;
16
17
  type Heads = automerge.Heads;
@@ -55,7 +56,7 @@ export class PatchSemaphore {
55
56
  }
56
57
 
57
58
  // Apply the unreconciled transactions to the document.
58
- let newHeads = codemirrorToAutomerge(this._field, handle, transactions, view.state);
59
+ let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
59
60
 
60
61
  // NOTE: null and undefined each come from automerge and repo respectively.
61
62
  if (newHeads === null || newHeads === undefined) {
@@ -65,7 +66,7 @@ export class PatchSemaphore {
65
66
 
66
67
  // Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
67
68
  const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.docSync()!, oldHeads, newHeads);
68
- automergeToCodemirror(view, selection, path, diff);
69
+ updateCodeMirror(view, selection, path, diff);
69
70
 
70
71
  view.dispatch({
71
72
  effects: updateHeads(newHeads),
@@ -8,10 +8,10 @@ import { type EditorState, type Text, type Transaction } from '@codemirror/state
8
8
 
9
9
  import { next as am, type Heads } from '@dxos/automerge/automerge';
10
10
 
11
- import { type IDocHandle } from '../handle';
12
- import { type Field } from '../plugin';
11
+ import { type IDocHandle } from './handle';
12
+ import { type Field } from './plugin';
13
13
 
14
- export const codemirrorToAutomerge = (
14
+ export const updateAutomerge = (
15
15
  field: Field,
16
16
  handle: IDocHandle,
17
17
  transactions: Transaction[],
@@ -16,14 +16,9 @@ import {
16
16
  type SpliceTextPatch,
17
17
  } from '@dxos/automerge/automerge';
18
18
 
19
- import { reconcileAnnotationType } from '../plugin';
19
+ import { reconcileAnnotationType } from './plugin';
20
20
 
21
- export const automergeToCodemirror = (
22
- view: EditorView,
23
- selection: EditorSelection,
24
- target: Prop[],
25
- patches: Patch[],
26
- ) => {
21
+ export const updateCodeMirror = (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
27
22
  for (const patch of patches) {
28
23
  const changeSpec = handlePatch(patch, target, view.state);
29
24
  if (changeSpec != null) {
@@ -2,11 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension } from '@codemirror/state';
5
+ import { type Extension, StateField } from '@codemirror/state';
6
6
  import get from 'lodash.get';
7
7
  import { useEffect, useState } from 'react';
8
8
  import { yCollab } from 'y-codemirror.next';
9
9
  import type * as awarenessProtocol from 'y-protocols/awareness';
10
+ import * as Y from 'yjs';
10
11
 
11
12
  import { invariant } from '@dxos/invariant';
12
13
  import {
@@ -19,6 +20,7 @@ import {
19
20
  } from '@dxos/react-client/echo';
20
21
  import { type Identity } from '@dxos/react-client/halo';
21
22
  import type { YText, YXmlFragment } from '@dxos/text-model';
23
+ import { arrayToString, stringToArray } from '@dxos/util';
22
24
 
23
25
  import { automergePlugin } from './automerge';
24
26
  import { SpaceAwarenessProvider } from './yjs';
@@ -26,19 +28,34 @@ import { SpaceAwarenessProvider } from './yjs';
26
28
  // TODO(burdon): Move.
27
29
  type Awareness = awarenessProtocol.Awareness;
28
30
 
29
- export type DocumentRange = {
30
- id: string;
31
+ /**
32
+ * State field makes the model available to other extensions.
33
+ */
34
+ // TODO(burdon): Use facet?
35
+ export const modelState = StateField.define<EditorModel | undefined>({
36
+ create: () => undefined,
37
+ update: (model) => model,
38
+ });
39
+
40
+ export type Range = {
31
41
  from: number;
32
42
  to: number;
33
43
  };
34
44
 
45
+ export type CommentRange = {
46
+ id: string;
47
+ // TODO(burdon): Split into begin/end?
48
+ cursor: string;
49
+ };
50
+
35
51
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
36
52
  export type EditorModel = {
37
53
  id: string;
38
54
  // TODO(burdon): Remove.
39
55
  content: string | YText | YXmlFragment | DocAccessor;
40
56
  text: () => string;
41
- ranges: DocumentRange[];
57
+ getCursorFromRange?: (value: Range) => string;
58
+ getRangeFromCursor?: (cursor: string) => Range | undefined;
42
59
  extension?: Extension;
43
60
  awareness?: Awareness;
44
61
  peer?: {
@@ -48,7 +65,7 @@ export type EditorModel = {
48
65
  };
49
66
 
50
67
  // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
51
- export type UseTextModelOptions = {
68
+ export type UseTextModelProps = {
52
69
  identity?: Identity | null;
53
70
  space?: Space;
54
71
  text?: TextObject;
@@ -56,39 +73,56 @@ export type UseTextModelOptions = {
56
73
 
57
74
  // TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
58
75
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
59
- export const useTextModel = ({ identity, space, text }: UseTextModelOptions): EditorModel | undefined => {
60
- const [model, setModel] = useState<EditorModel | undefined>(() => createModel({ identity, space, text }));
61
- useEffect(() => {
62
- setModel(createModel({ identity, space, text }));
63
- }, [identity, space, text]);
76
+ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
77
+ const { identity, space, text } = props;
78
+ const [model, setModel] = useState<EditorModel | undefined>(() => createModel(props));
79
+ useEffect(() => setModel(createModel(props)), [identity, space, text]);
64
80
  return model;
65
81
  };
66
82
 
67
- const createModel = (options: UseTextModelOptions) => {
68
- const { text } = options;
83
+ const createModel = (props: UseTextModelProps) => {
84
+ const { text } = props;
69
85
  if (isActualAutomergeObject(text)) {
70
- return createAutomergeModel(options);
86
+ return createAutomergeModel(props);
71
87
  } else {
72
88
  if (!text?.doc) {
73
89
  return undefined;
74
90
  }
75
91
 
76
- return createYjsModel(options);
92
+ return createYjsModel(props);
77
93
  }
78
94
  };
79
95
 
80
- const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
96
+ const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
81
97
  invariant(text?.doc && text?.content);
82
98
  const provider = space
83
99
  ? new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
84
100
  : undefined;
85
101
 
86
- return {
102
+ const model: EditorModel = {
87
103
  id: text.doc.guid,
88
104
  content: text.content,
89
105
  text: () => text.content!.toString(),
90
- ranges: [],
91
- extension: yCollab(text.content as YText, provider?.awareness),
106
+ // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
107
+ getCursorFromRange: (range: Range) => {
108
+ const from = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.from));
109
+ const to = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.to, -1));
110
+ return [arrayToString(from), arrayToString(to)].join(':');
111
+ },
112
+ getRangeFromCursor: (cursor: string) => {
113
+ invariant(text.doc);
114
+ const parts = cursor.split(':');
115
+ const from = Y.createAbsolutePositionFromRelativePosition(
116
+ Y.decodeRelativePosition(stringToArray(parts[0])),
117
+ text.doc,
118
+ );
119
+ const to = Y.createAbsolutePositionFromRelativePosition(
120
+ Y.decodeRelativePosition(stringToArray(parts[1])),
121
+ text.doc,
122
+ );
123
+ return from && to ? { from: from.index, to: to.index } : undefined;
124
+ },
125
+ extension: [yCollab(text.content as YText, provider?.awareness), modelState.init(() => model)],
92
126
  awareness: provider?.awareness,
93
127
  peer: identity
94
128
  ? {
@@ -97,18 +131,20 @@ const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorM
97
131
  }
98
132
  : undefined,
99
133
  };
134
+
135
+ return model;
100
136
  };
101
137
 
102
- const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorModel => {
138
+ const createAutomergeModel = ({ identity, text }: UseTextModelProps): EditorModel => {
103
139
  const obj = text as any as AutomergeTextCompat;
104
140
  const doc = getRawDoc(obj, [obj.field]);
105
141
 
106
- return {
142
+ const model: EditorModel = {
107
143
  id: obj.id,
108
144
  content: doc,
109
145
  text: () => get(doc.handle.docSync(), doc.path),
110
- ranges: [],
111
- extension: automergePlugin(doc.handle, doc.path),
146
+ // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
147
+ extension: [automergePlugin(doc.handle, doc.path), modelState.init(() => model)],
112
148
  peer: identity
113
149
  ? {
114
150
  id: identity.identityKey.toHex(),
@@ -116,4 +152,6 @@ const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorMo
116
152
  }
117
153
  : undefined,
118
154
  };
155
+
156
+ return model;
119
157
  };
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { effect } from '@preact/signals-react';
6
+ import { expect } from 'chai';
7
+ import * as Y from 'yjs';
8
+
9
+ import { Trigger } from '@dxos/async';
10
+ import { Expando, setGlobalAutomergePreference, TextObject } from '@dxos/echo-schema';
11
+ import { registerSignalFactory } from '@dxos/echo-signals';
12
+ import { describe, test } from '@dxos/test';
13
+ import { type YText } from '@dxos/text-model';
14
+
15
+ describe('YJS', () => {
16
+ // https://docs.yjs.dev/api/shared-types/y.text
17
+ // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
18
+ test('absolute position', () => {
19
+ const obj = new TextObject('hello world');
20
+ const index = obj.text.indexOf('world');
21
+ const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
22
+
23
+ {
24
+ const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
25
+ expect(cursor!.index).to.equal(index);
26
+ expect(obj.text.substring(cursor!.index)).to.equal('world');
27
+ }
28
+
29
+ (obj.content as YText).insert(index, 'cruel ');
30
+ expect(obj.text).to.equal('hello cruel world');
31
+
32
+ {
33
+ const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
34
+ expect(cursor!.index).not.to.equal(index);
35
+ expect(obj.text.substring(cursor!.index)).to.equal('world');
36
+ }
37
+ });
38
+
39
+ // TODO(burdon): Test deleting end character.
40
+
41
+ test.skip('effect', async () => {
42
+ setGlobalAutomergePreference(true);
43
+ registerSignalFactory();
44
+ const obj = new Expando({ comments: [] });
45
+
46
+ const trigger = new Trigger();
47
+ const unsubscribe = effect(() => {
48
+ // TODO(burdon): BUG: doesn't work with YJS.
49
+ console.log(obj.comments);
50
+ if (obj.comments.length > 0) {
51
+ trigger.wake();
52
+ }
53
+ });
54
+
55
+ obj.comments.push({ message: 'hello' });
56
+ await trigger.wait();
57
+ unsubscribe();
58
+ });
59
+ });
package/src/index.ts CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
6
6
  export { Doc, YText, YXmlFragment } from '@dxos/text-model';
7
+ export { type Extension } from '@codemirror/state';
7
8
 
8
9
  export * from './components';
10
+ export * from './extensions';
9
11
  export * from './hooks';
12
+ export * from './themes';
@@ -169,7 +169,6 @@ export class Replicator {
169
169
  id: doc.guid,
170
170
  text: () => content.toString(),
171
171
  content,
172
- ranges: [],
173
172
  awareness: provider.awareness,
174
173
  peer: { id },
175
174
  };