@dxos/react-ui-editor 0.8.4-main.a4bbb77 → 0.8.4-main.ae835ea

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 (194) hide show
  1. package/dist/lib/browser/{chunk-22UMM3QJ.mjs → chunk-HL3YF6WC.mjs} +2 -2
  2. package/dist/lib/browser/chunk-HL3YF6WC.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +4577 -4832
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs.map +2 -2
  7. package/dist/lib/browser/types/index.mjs +1 -1
  8. package/dist/lib/node-esm/{chunk-YXYQPV6R.mjs → chunk-YJZGD3LY.mjs} +2 -2
  9. package/dist/lib/node-esm/chunk-YJZGD3LY.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +4577 -4832
  11. package/dist/lib/node-esm/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  14. package/dist/lib/node-esm/types/index.mjs +1 -1
  15. package/dist/types/src/components/Editor/Editor.d.ts +13 -4
  16. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  17. package/dist/types/src/components/Editor/Editor.stories.d.ts +27 -0
  18. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +17 -2
  20. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  21. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorToolbar/util.d.ts +5 -19
  23. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  24. package/dist/types/src/components/index.d.ts +0 -1
  25. package/dist/types/src/components/index.d.ts.map +1 -1
  26. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  27. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  28. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  29. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  30. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  31. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +20 -0
  32. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  33. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  34. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  35. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  36. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  37. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  38. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  39. package/dist/types/src/extensions/automerge/sync.d.ts +3 -3
  40. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  41. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  42. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  43. package/dist/types/src/extensions/autoscroll.d.ts +2 -2
  44. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  45. package/dist/types/src/extensions/awareness/awareness-provider.d.ts +1 -1
  46. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  47. package/dist/types/src/extensions/factories.d.ts +9 -4
  48. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  49. package/dist/types/src/extensions/index.d.ts +2 -3
  50. package/dist/types/src/extensions/index.d.ts.map +1 -1
  51. package/dist/types/src/extensions/json.d.ts +1 -1
  52. package/dist/types/src/extensions/json.d.ts.map +1 -1
  53. package/dist/types/src/extensions/listener.d.ts +8 -6
  54. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  55. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  56. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -2
  57. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  58. package/dist/types/src/extensions/modes.d.ts +1 -1
  59. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  60. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  61. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  62. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +36 -0
  63. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -0
  64. package/dist/types/src/extensions/popover/index.d.ts +8 -0
  65. package/dist/types/src/extensions/popover/index.d.ts.map +1 -0
  66. package/dist/types/src/extensions/popover/menu-presets.d.ts +4 -0
  67. package/dist/types/src/extensions/popover/menu-presets.d.ts.map +1 -0
  68. package/dist/types/src/extensions/popover/menu.d.ts +24 -0
  69. package/dist/types/src/extensions/popover/menu.d.ts.map +1 -0
  70. package/dist/types/src/extensions/popover/modal.d.ts +7 -0
  71. package/dist/types/src/extensions/popover/modal.d.ts.map +1 -0
  72. package/dist/types/src/extensions/popover/popover.d.ts +47 -0
  73. package/dist/types/src/extensions/popover/popover.d.ts.map +1 -0
  74. package/dist/types/src/extensions/popover/usePopoverMenu.d.ts +34 -0
  75. package/dist/types/src/extensions/popover/usePopoverMenu.d.ts.map +1 -0
  76. package/dist/types/src/extensions/popover/util.d.ts +8 -0
  77. package/dist/types/src/extensions/popover/util.d.ts.map +1 -0
  78. package/dist/types/src/extensions/preview/preview.d.ts +6 -3
  79. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  80. package/dist/types/src/extensions/state.d.ts +2 -0
  81. package/dist/types/src/extensions/state.d.ts.map +1 -0
  82. package/dist/types/src/hooks/useTextEditor.d.ts +2 -6
  83. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  84. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
  85. package/dist/types/src/stories/Comments.stories.d.ts +2 -2
  86. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  87. package/dist/types/src/stories/Experimental.stories.d.ts +2 -2
  88. package/dist/types/src/stories/Markdown.stories.d.ts +2 -2
  89. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  90. package/dist/types/src/stories/{CommandMenu.stories.d.ts → Popover.stories.d.ts} +6 -5
  91. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
  92. package/dist/types/src/stories/Preview.stories.d.ts +2 -2
  93. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  94. package/dist/types/src/stories/TextEditor.stories.d.ts +2 -3
  95. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  96. package/dist/types/src/stories/components/EditorStory.d.ts +3 -3
  97. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  98. package/dist/types/src/styles/theme.d.ts.map +1 -1
  99. package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -1
  100. package/dist/types/src/types/types.d.ts +1 -1
  101. package/dist/types/src/types/types.d.ts.map +1 -1
  102. package/dist/types/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +41 -38
  104. package/src/components/Editor/Editor.stories.tsx +69 -0
  105. package/src/components/Editor/Editor.tsx +25 -17
  106. package/src/components/EditorToolbar/EditorToolbar.tsx +88 -87
  107. package/src/components/EditorToolbar/headings.ts +6 -4
  108. package/src/components/EditorToolbar/util.ts +2 -18
  109. package/src/components/index.ts +0 -1
  110. package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +1 -0
  111. package/src/extensions/autocomplete/index.ts +8 -0
  112. package/src/extensions/autocomplete/match.ts +46 -0
  113. package/src/extensions/{command-menu → autocomplete}/placeholder.ts +21 -17
  114. package/src/extensions/{command-dialog → autocomplete}/typeahead.ts +6 -48
  115. package/src/extensions/automerge/automerge.ts +28 -9
  116. package/src/extensions/automerge/cursor.ts +1 -1
  117. package/src/extensions/automerge/sync.ts +8 -4
  118. package/src/extensions/automerge/update-automerge.ts +1 -1
  119. package/src/extensions/autoscroll.ts +3 -3
  120. package/src/extensions/awareness/awareness-provider.ts +2 -2
  121. package/src/extensions/factories.ts +18 -10
  122. package/src/extensions/hashtag.tsx +2 -2
  123. package/src/extensions/index.ts +2 -3
  124. package/src/extensions/json.ts +1 -1
  125. package/src/extensions/listener.ts +14 -20
  126. package/src/extensions/markdown/bundle.ts +14 -2
  127. package/src/extensions/markdown/formatting.ts +8 -8
  128. package/src/extensions/modes.ts +2 -2
  129. package/src/extensions/{floating-menu.ts → outliner/menu.ts} +7 -5
  130. package/src/extensions/outliner/outliner.ts +2 -2
  131. package/src/extensions/popover/PopoverMenuProvider.tsx +220 -0
  132. package/src/extensions/popover/index.ts +12 -0
  133. package/src/extensions/popover/menu-presets.ts +124 -0
  134. package/src/extensions/popover/menu.ts +67 -0
  135. package/src/extensions/popover/modal.ts +24 -0
  136. package/src/extensions/popover/popover.ts +289 -0
  137. package/src/extensions/popover/usePopoverMenu.ts +173 -0
  138. package/src/extensions/popover/util.ts +29 -0
  139. package/src/extensions/preview/index.ts +1 -1
  140. package/src/extensions/preview/preview.ts +10 -7
  141. package/src/extensions/state.ts +7 -0
  142. package/src/hooks/useTextEditor.ts +21 -21
  143. package/src/stories/CommandDialog.stories.tsx +3 -14
  144. package/src/stories/EditorToolbar.stories.tsx +4 -5
  145. package/src/stories/Outliner.stories.tsx +16 -9
  146. package/src/stories/Popover.stories.tsx +163 -0
  147. package/src/stories/Preview.stories.tsx +15 -8
  148. package/src/stories/TextEditor.stories.tsx +3 -29
  149. package/src/stories/components/EditorStory.tsx +5 -3
  150. package/src/styles/theme.ts +2 -1
  151. package/src/testing/PreviewPopover.tsx +2 -0
  152. package/src/types/types.ts +1 -1
  153. package/dist/lib/browser/chunk-22UMM3QJ.mjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-YXYQPV6R.mjs.map +0 -7
  155. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts +0 -38
  156. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +0 -1
  157. package/dist/types/src/components/CommandMenu/index.d.ts +0 -2
  158. package/dist/types/src/components/CommandMenu/index.d.ts.map +0 -1
  159. package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
  160. package/dist/types/src/extensions/command-dialog/action.d.ts +0 -17
  161. package/dist/types/src/extensions/command-dialog/action.d.ts.map +0 -1
  162. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts +0 -6
  163. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts.map +0 -1
  164. package/dist/types/src/extensions/command-dialog/hint.d.ts +0 -19
  165. package/dist/types/src/extensions/command-dialog/hint.d.ts.map +0 -1
  166. package/dist/types/src/extensions/command-dialog/index.d.ts +0 -4
  167. package/dist/types/src/extensions/command-dialog/index.d.ts.map +0 -1
  168. package/dist/types/src/extensions/command-dialog/state.d.ts +0 -16
  169. package/dist/types/src/extensions/command-dialog/state.d.ts.map +0 -1
  170. package/dist/types/src/extensions/command-dialog/typeahead.d.ts +0 -22
  171. package/dist/types/src/extensions/command-dialog/typeahead.d.ts.map +0 -1
  172. package/dist/types/src/extensions/command-menu/command-menu.d.ts +0 -20
  173. package/dist/types/src/extensions/command-menu/command-menu.d.ts.map +0 -1
  174. package/dist/types/src/extensions/command-menu/index.d.ts +0 -3
  175. package/dist/types/src/extensions/command-menu/index.d.ts.map +0 -1
  176. package/dist/types/src/extensions/command-menu/placeholder.d.ts +0 -10
  177. package/dist/types/src/extensions/command-menu/placeholder.d.ts.map +0 -1
  178. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts +0 -24
  179. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts.map +0 -1
  180. package/dist/types/src/extensions/floating-menu.d.ts +0 -7
  181. package/dist/types/src/extensions/floating-menu.d.ts.map +0 -1
  182. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
  183. package/src/components/CommandMenu/CommandMenu.tsx +0 -348
  184. package/src/components/CommandMenu/index.ts +0 -5
  185. package/src/extensions/command-dialog/action.ts +0 -55
  186. package/src/extensions/command-dialog/command-dialog.ts +0 -34
  187. package/src/extensions/command-dialog/hint.ts +0 -103
  188. package/src/extensions/command-dialog/index.ts +0 -7
  189. package/src/extensions/command-dialog/state.ts +0 -90
  190. package/src/extensions/command-menu/command-menu.ts +0 -210
  191. package/src/extensions/command-menu/index.ts +0 -6
  192. package/src/extensions/command-menu/useCommandMenu.ts +0 -134
  193. package/src/stories/CommandMenu.stories.tsx +0 -158
  194. /package/dist/types/src/extensions/{autocomplete.d.ts → autocomplete/autocomplete.d.ts} +0 -0
@@ -11,20 +11,23 @@ import { clientRectsFor, flattenRect } from '../../util';
11
11
  type Content = string | HTMLElement | ((view: EditorView) => HTMLElement);
12
12
 
13
13
  export type PlaceholderOptions = {
14
- delay?: number;
15
14
  content: Content;
15
+ delay?: number;
16
16
  };
17
17
 
18
- export const placeholder = ({ delay = 3_000, content }: PlaceholderOptions): Extension => {
18
+ /**
19
+ * Shows a transient placeholder at the current cursor position.
20
+ */
21
+ export const placeholder = ({ content, delay = 3_000 }: PlaceholderOptions): Extension => {
19
22
  const plugin = ViewPlugin.fromClass(
20
23
  class {
21
- decorations = Decoration.none;
22
- timeout: ReturnType<typeof setTimeout> | undefined;
24
+ _timeout: ReturnType<typeof setTimeout> | undefined;
25
+ _decorations = Decoration.none;
23
26
 
24
27
  update(update: ViewUpdate) {
25
- if (this.timeout) {
26
- window.clearTimeout(this.timeout);
27
- this.timeout = undefined;
28
+ if (this._timeout) {
29
+ window.clearTimeout(this._timeout);
30
+ this._timeout = undefined;
28
31
  }
29
32
 
30
33
  // Check if the active line (where cursor is) is empty.
@@ -33,10 +36,10 @@ export const placeholder = ({ delay = 3_000, content }: PlaceholderOptions): Ext
33
36
  if (isEmpty) {
34
37
  // Create widget decoration at the start of the current line.
35
38
  const lineStart = activeLine.from;
36
- this.timeout = setTimeout(() => {
37
- this.decorations = Decoration.set([
39
+ this._timeout = setTimeout(() => {
40
+ this._decorations = Decoration.set([
38
41
  Decoration.widget({
39
- widget: new Placeholder(content),
42
+ widget: new PlaceholderWidget(content),
40
43
  side: 1,
41
44
  }).range(lineStart),
42
45
  ]);
@@ -45,18 +48,18 @@ export const placeholder = ({ delay = 3_000, content }: PlaceholderOptions): Ext
45
48
  }, delay);
46
49
  }
47
50
 
48
- this.decorations = Decoration.none;
51
+ this._decorations = Decoration.none;
49
52
  }
50
53
 
51
54
  destroy() {
52
- if (this.timeout) {
53
- clearTimeout(this.timeout);
55
+ if (this._timeout) {
56
+ clearTimeout(this._timeout);
54
57
  }
55
58
  }
56
59
  },
57
60
  {
58
61
  provide: (plugin) => {
59
- return [EditorView.decorations.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none)];
62
+ return [EditorView.decorations.of((view) => view.plugin(plugin)?._decorations ?? Decoration.none)];
60
63
  },
61
64
  },
62
65
  );
@@ -66,7 +69,7 @@ export const placeholder = ({ delay = 3_000, content }: PlaceholderOptions): Ext
66
69
  : plugin;
67
70
  };
68
71
 
69
- class Placeholder extends WidgetType {
72
+ export class PlaceholderWidget extends WidgetType {
70
73
  constructor(readonly content: Content) {
71
74
  super();
72
75
  }
@@ -75,6 +78,7 @@ class Placeholder extends WidgetType {
75
78
  const wrap = document.createElement('span');
76
79
  wrap.className = 'cm-placeholder';
77
80
  wrap.style.pointerEvents = 'none';
81
+ wrap.setAttribute('aria-hidden', 'true');
78
82
  wrap.appendChild(
79
83
  typeof this.content === 'string'
80
84
  ? document.createTextNode(this.content)
@@ -82,7 +86,7 @@ class Placeholder extends WidgetType {
82
86
  ? this.content(view)
83
87
  : this.content.cloneNode(true),
84
88
  );
85
- wrap.setAttribute('aria-hidden', 'true');
89
+
86
90
  return wrap;
87
91
  }
88
92
 
@@ -92,7 +96,7 @@ class Placeholder extends WidgetType {
92
96
  return null;
93
97
  }
94
98
 
95
- const style = window.getComputedStyle(dom.parentNode as HTMLElement);
99
+ const style = getComputedStyle(dom.parentNode as HTMLElement);
96
100
  const rect = flattenRect(rects[0], style.direction !== 'rtl');
97
101
  const lineHeight = parseInt(style.lineHeight);
98
102
  if (rect.bottom - rect.top > lineHeight * 1.5) {
@@ -13,17 +13,16 @@ import {
13
13
  keymap,
14
14
  } from '@codemirror/view';
15
15
 
16
- import { Hint } from './hint';
16
+ import { type CompoetionContext } from './match';
17
+ import { PlaceholderWidget } from './placeholder';
17
18
 
18
- export type TypeaheadContext = { line: string };
19
-
20
- // TODO(burdon): Option to complete only at end of line?
19
+ // TODO(burdon): Option to complete only at end of line.
21
20
  export type TypeaheadOptions = {
22
- onComplete?: (context: TypeaheadContext) => string | undefined;
21
+ onComplete?: (context: CompoetionContext) => string | undefined;
23
22
  };
24
23
 
25
24
  /**
26
- * CodeMirror extension for typeahead completion.
25
+ * Shows a completion placeholder.
27
26
  */
28
27
  export const typeahead = ({ onComplete }: TypeaheadOptions = {}): Extension => {
29
28
  let hint: string | undefined;
@@ -57,7 +56,7 @@ export const typeahead = ({ onComplete }: TypeaheadOptions = {}): Extension => {
57
56
  const str = update.state.sliceDoc(line.from, selection.from);
58
57
  hint = onComplete?.({ line: str });
59
58
  if (hint) {
60
- builder.add(selection.from, selection.to, Decoration.widget({ widget: new Hint(hint) }));
59
+ builder.add(selection.from, selection.to, Decoration.widget({ widget: new PlaceholderWidget(hint) }));
61
60
  }
62
61
  }
63
62
 
@@ -86,44 +85,3 @@ export const typeahead = ({ onComplete }: TypeaheadOptions = {}): Extension => {
86
85
  ),
87
86
  ];
88
87
  };
89
-
90
- type CompletionOptions = {
91
- default?: string;
92
- minLength?: number;
93
- };
94
-
95
- /**
96
- * Util to match current line to a static list of completions.
97
- */
98
- export const staticCompletion =
99
- (completions: string[], options: CompletionOptions = {}) =>
100
- ({ line }: TypeaheadContext) => {
101
- if (line.length === 0 && options.default) {
102
- return options.default;
103
- }
104
-
105
- const parts = line.split(/\s+/).filter(Boolean);
106
- if (parts.length) {
107
- const str = parts.at(-1)!;
108
- if (str.length >= (options.minLength ?? 0)) {
109
- for (const completion of completions) {
110
- const match = matchCompletion(completion, str);
111
- if (match) {
112
- return match;
113
- }
114
- }
115
- }
116
- }
117
- };
118
-
119
- export const matchCompletion = (completion: string, str: string, minLength = 0): string | undefined => {
120
- if (
121
- str.length >= minLength &&
122
- completion.length > str.length &&
123
- completion.startsWith(str)
124
- // TODO(burdon): If case insensitive, need to replace existing chars.
125
- // completion.toLowerCase().startsWith(str.toLowerCase())
126
- ) {
127
- return completion.slice(str.length);
128
- }
129
- };
@@ -5,12 +5,13 @@
5
5
  //
6
6
 
7
7
  import { next as A } from '@automerge/automerge';
8
- import { type Extension, StateField } from '@codemirror/state';
8
+ import { type Extension, StateField, Transaction } from '@codemirror/state';
9
9
  import { EditorView, ViewPlugin } from '@codemirror/view';
10
10
 
11
- import { type DocAccessor } from '@dxos/react-client/echo';
11
+ import { DocAccessor } from '@dxos/client/echo';
12
12
 
13
13
  import { Cursor } from '../../util';
14
+ import { initialSync } from '../state';
14
15
 
15
16
  import { cursorConverter } from './cursor';
16
17
  import { type State, isReconcile, updateHeadsEffect } from './defs';
@@ -18,11 +19,13 @@ import { Syncer } from './sync';
18
19
 
19
20
  export const automerge = (accessor: DocAccessor): Extension => {
20
21
  const syncState = StateField.define<State>({
21
- create: () => ({
22
- path: accessor.path.slice(),
23
- lastHeads: A.getHeads(accessor.handle.doc()!),
24
- unreconciledTransactions: [],
25
- }),
22
+ create: () => {
23
+ return {
24
+ path: accessor.path.slice(),
25
+ lastHeads: A.getHeads(accessor.handle.doc()!),
26
+ unreconciledTransactions: [],
27
+ };
28
+ },
26
29
 
27
30
  update: (value, tr) => {
28
31
  const result: State = {
@@ -64,6 +67,18 @@ export const automerge = (accessor: DocAccessor): Extension => {
64
67
  class {
65
68
  constructor(private readonly _view: EditorView) {
66
69
  accessor.handle.addListener('change', this._handleChange);
70
+
71
+ requestAnimationFrame(() => {
72
+ const value = DocAccessor.getValue<string>(accessor);
73
+ const current = this._view.state.doc.toString();
74
+ if (value !== current) {
75
+ // TODO(burdon): This attempts to set the initial state, but creates problems.
76
+ // this._view.dispatch({
77
+ // changes: { from: 0, to: this._view.state.doc.length, insert: value },
78
+ // annotations: initialSync,
79
+ // });
80
+ }
81
+ });
67
82
  }
68
83
 
69
84
  destroy() {
@@ -77,9 +92,13 @@ export const automerge = (accessor: DocAccessor): Extension => {
77
92
  ),
78
93
 
79
94
  // Reconcile local updates.
80
- EditorView.updateListener.of(({ view, changes }) => {
95
+ EditorView.updateListener.of(({ view, changes, transactions }) => {
81
96
  if (!changes.empty) {
82
- syncer.reconcile(view, true);
97
+ // Only reconcile if it's not an initial sync (to avoid loops)
98
+ const isInitialSync = transactions.some((tr) => tr.annotation(Transaction.userEvent) === initialSync.value);
99
+ if (!isInitialSync) {
100
+ syncer.reconcile(view, true);
101
+ }
83
102
  }
84
103
  }),
85
104
  ];
@@ -2,8 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type DocAccessor, fromCursor, toCursor } from '@dxos/client/echo';
5
6
  import { log } from '@dxos/log';
6
- import { type DocAccessor, fromCursor, toCursor } from '@dxos/react-client/echo';
7
7
 
8
8
  import { type CursorConverter } from '../../util';
9
9
 
@@ -8,7 +8,8 @@ import { next as A } from '@automerge/automerge';
8
8
  import { type StateField } from '@codemirror/state';
9
9
  import { type EditorView } from '@codemirror/view';
10
10
 
11
- import { type IDocHandle } from '@dxos/react-client/echo';
11
+ import { type IDocHandle } from '@dxos/client/echo';
12
+ import { log } from '@dxos/log';
12
13
 
13
14
  import { type State, getLastHeads, getPath, isReconcile, reconcileAnnotation, updateHeads } from './defs';
14
15
  import { updateAutomerge } from './update-automerge';
@@ -27,7 +28,6 @@ export class Syncer {
27
28
  ) {}
28
29
 
29
30
  reconcile(view: EditorView, editor: boolean): void {
30
- // TODO(burdon): Better way to do mutex?
31
31
  if (this._pending) {
32
32
  return;
33
33
  }
@@ -41,7 +41,9 @@ export class Syncer {
41
41
  this._pending = false;
42
42
  }
43
43
 
44
- onEditorChange(view: EditorView): void {
44
+ private onEditorChange(view: EditorView): void {
45
+ log('onEditorChange');
46
+
45
47
  // Apply the unreconciled transactions to the document.
46
48
  const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcile(tx));
47
49
  const newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
@@ -54,7 +56,9 @@ export class Syncer {
54
56
  }
55
57
  }
56
58
 
57
- onAutomergeChange(view: EditorView): void {
59
+ private onAutomergeChange(view: EditorView): void {
60
+ log('onAutomergeChange');
61
+
58
62
  // Get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
59
63
  const oldHeads = getLastHeads(view.state, this._state);
60
64
  const newHeads = A.getHeads(this._handle.doc()!);
@@ -7,7 +7,7 @@
7
7
  import { next as A, type Heads } from '@automerge/automerge';
8
8
  import { type EditorState, type StateField, type Text, type Transaction } from '@codemirror/state';
9
9
 
10
- import { type IDocHandle } from '@dxos/react-client/echo';
10
+ import { type IDocHandle } from '@dxos/client/echo';
11
11
 
12
12
  import { type State } from './defs';
13
13
 
@@ -12,8 +12,8 @@ const lineHeight = 24;
12
12
  export const scrollToBottomEffect = StateEffect.define<any>();
13
13
 
14
14
  export type AutoScrollOptions = {
15
- overscroll: number;
16
- throttle: number;
15
+ overscroll?: number;
16
+ throttle?: number;
17
17
  };
18
18
 
19
19
  /**
@@ -115,7 +115,7 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
115
115
  Domino.of('button')
116
116
  .classNames('dx-button bg-accentSurface')
117
117
  .data('density', 'fine')
118
- .children(Domino.of<any>('dx-icon').attr('icon', 'ph--arrow-down--regular'))
118
+ .children(Domino.of<any>('dx-icon').attributes({ icon: 'ph--arrow-down--regular' }))
119
119
  .on('click', () => {
120
120
  scrollToBottom(view);
121
121
  }),
@@ -3,11 +3,11 @@
3
3
  //
4
4
 
5
5
  import { DeferredTask, Event, sleep } from '@dxos/async';
6
+ import { type Space } from '@dxos/client/echo';
7
+ import { type GossipMessage } from '@dxos/client/mesh';
6
8
  import { Context } from '@dxos/context';
7
9
  import { invariant } from '@dxos/invariant';
8
10
  import { log } from '@dxos/log';
9
- import { type Space } from '@dxos/react-client/echo';
10
- import { type GossipMessage } from '@dxos/react-client/mesh';
11
11
 
12
12
  import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from './awareness';
13
13
 
@@ -4,10 +4,9 @@
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
6
  import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
- import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
+ import { HighlightStyle, bracketMatching, syntaxHighlighting } from '@codemirror/language';
8
8
  import { searchKeymap } from '@codemirror/search';
9
9
  import { type ChangeSpec, EditorState, type Extension, type TransactionSpec } from '@codemirror/state';
10
- import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
11
10
  import {
12
11
  EditorView,
13
12
  type KeyBinding,
@@ -20,13 +19,14 @@ import {
20
19
  placeholder,
21
20
  scrollPastEnd,
22
21
  } from '@codemirror/view';
22
+ import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode';
23
23
  import defaultsDeep from 'lodash.defaultsdeep';
24
24
  import merge from 'lodash.merge';
25
25
 
26
+ import { type DocAccessor, type Space } from '@dxos/client/echo';
27
+ import { type Identity } from '@dxos/client/halo';
26
28
  import { generateName } from '@dxos/display-name';
27
29
  import { log } from '@dxos/log';
28
- import { type DocAccessor, type Space } from '@dxos/react-client/echo';
29
- import { type Identity } from '@dxos/react-client/halo';
30
30
  import { type ThemeMode } from '@dxos/react-ui';
31
31
  import { type HuePalette } from '@dxos/react-ui-theme';
32
32
  import { hexToHue, isTruthy } from '@dxos/util';
@@ -87,11 +87,11 @@ export type BasicExtensionsOptions = {
87
87
  lineNumbers?: boolean;
88
88
  /** If false then do not set a max-width or side margin on the editor. */
89
89
  lineWrapping?: boolean;
90
- monospace?: boolean;
91
90
  placeholder?: string;
92
91
  /** If true user cannot edit the text, but they can still select and copy it. */
93
92
  readOnly?: boolean;
94
93
  search?: boolean;
94
+ /** NOTE: Do not use with stack sections. */
95
95
  scrollPastEnd?: boolean;
96
96
  standardKeymap?: boolean;
97
97
  tabSize?: number;
@@ -135,7 +135,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
135
135
  props.history && history(),
136
136
  props.lineNumbers && [lineNumbers(), editorGutter],
137
137
  props.lineWrapping && EditorView.lineWrapping,
138
- props.monospace && editorMonospace,
139
138
  props.placeholder && placeholder(props.placeholder),
140
139
  props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
141
140
  props.scrollPastEnd && scrollPastEnd(),
@@ -172,6 +171,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
172
171
 
173
172
  export type ThemeExtensionsOptions = {
174
173
  themeMode?: ThemeMode;
174
+ monospace?: boolean;
175
175
  styles?: ThemeStyles;
176
176
  syntaxHighlighting?: boolean;
177
177
  slots?: {
@@ -201,20 +201,28 @@ export const fullWidth: ThemeExtensionsOptions['slots'] = {
201
201
 
202
202
  export const defaultThemeSlots = grow;
203
203
 
204
+ export const defaultStyles = {
205
+ dark: vscodeDarkStyle,
206
+ light: vscodeLightStyle,
207
+ };
208
+
204
209
  /**
205
210
  * https://codemirror.net/examples/styling
206
211
  */
207
212
  export const createThemeExtensions = ({
208
213
  themeMode,
214
+ monospace,
209
215
  styles,
210
- syntaxHighlighting: syntaxHighlightingProps,
211
- slots: _slots,
216
+ syntaxHighlighting: syntaxHighlightingProp,
217
+ slots: slotsParam,
212
218
  }: ThemeExtensionsOptions = {}): Extension => {
213
- const slots = defaultsDeep({}, _slots, defaultThemeSlots);
219
+ const slots = defaultsDeep({}, slotsParam, defaultThemeSlots);
214
220
  return [
215
221
  EditorView.darkTheme.of(themeMode === 'dark'),
216
222
  EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
217
- syntaxHighlightingProps && syntaxHighlighting(themeMode === 'dark' ? oneDarkHighlightStyle : defaultHighlightStyle),
223
+ monospace && editorMonospace,
224
+ syntaxHighlightingProp &&
225
+ syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
218
226
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
219
227
  slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
220
228
  slots.scroll?.className &&
@@ -13,7 +13,7 @@ import {
13
13
  WidgetType,
14
14
  } from '@codemirror/view';
15
15
 
16
- import { getHashColor, mx } from '@dxos/react-ui-theme';
16
+ import { getHashStyles, mx } from '@dxos/react-ui-theme';
17
17
 
18
18
  class TagWidget extends WidgetType {
19
19
  constructor(private _text: string) {
@@ -22,7 +22,7 @@ class TagWidget extends WidgetType {
22
22
 
23
23
  toDOM(): HTMLSpanElement {
24
24
  const span = document.createElement('span');
25
- span.className = mx('cm-tag', getHashColor(this._text).tag);
25
+ span.className = mx('cm-tag', getHashStyles(this._text).surface);
26
26
  span.textContent = this._text;
27
27
  return span;
28
28
  }
@@ -8,13 +8,10 @@ export * from './autoscroll';
8
8
  export * from './automerge';
9
9
  export * from './awareness';
10
10
  export * from './blast';
11
- export * from './command-dialog';
12
- export * from './command-menu';
13
11
  export * from './comments';
14
12
  export * from './debug';
15
13
  export * from './dnd';
16
14
  export * from './factories';
17
- export * from './floating-menu';
18
15
  export * from './focus';
19
16
  export * from './folding';
20
17
  export * from './hashtag';
@@ -24,7 +21,9 @@ export * from './markdown';
24
21
  export * from './mention';
25
22
  export * from './modes';
26
23
  export * from './outliner';
24
+ export * from './popover';
27
25
  export * from './preview';
28
26
  export * from './selection';
27
+ export * from './state';
29
28
  export * from './tags';
30
29
  export * from './typewriter';
@@ -7,7 +7,7 @@ import { type LintSource, linter } from '@codemirror/lint';
7
7
  import { type Extension } from '@codemirror/state';
8
8
  import Ajv, { type ValidateFunction } from 'ajv';
9
9
 
10
- import { type JsonSchemaType } from '@dxos/echo-schema';
10
+ import { type JsonSchemaType } from '@dxos/echo/internal';
11
11
 
12
12
  export type JsonExtensionsOptions = {
13
13
  schema?: JsonSchemaType;
@@ -5,34 +5,28 @@
5
5
  import { type Extension } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
7
 
8
+ import { isNonNullable } from '@dxos/util';
9
+
8
10
  import { documentId } from './selection';
9
11
 
10
12
  export type ListenerOptions = {
11
- onFocus?: (focusing: boolean) => void;
12
- onChange?: (text: string, id: string) => void;
13
+ onFocus?: (event: { id: string; focusing: boolean }) => void;
14
+ onChange?: (event: { id: string; text: string }) => void;
13
15
  };
14
16
 
15
- /**
16
- * Event listener.
17
- * @deprecated Use EditorView.updateListener and listen for specific update events.
18
- */
19
17
  export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
20
- const extensions: Extension[] = [];
21
-
22
- onFocus &&
23
- extensions.push(
24
- EditorView.focusChangeEffect.of((_, focusing) => {
25
- onFocus(focusing);
18
+ return [
19
+ onFocus &&
20
+ EditorView.focusChangeEffect.of((state, focusing) => {
21
+ onFocus({ id: state.facet(documentId), focusing });
26
22
  return null;
27
23
  }),
28
- );
29
24
 
30
- onChange &&
31
- extensions.push(
32
- EditorView.updateListener.of((update) => {
33
- onChange(update.state.doc.toString(), update.state.facet(documentId));
25
+ onChange &&
26
+ EditorView.updateListener.of(({ state, docChanged }) => {
27
+ if (docChanged) {
28
+ onChange({ id: state.facet(documentId), text: state.doc.toString() });
29
+ }
34
30
  }),
35
- );
36
-
37
- return extensions;
31
+ ].filter(isNonNullable);
38
32
  };
@@ -4,8 +4,10 @@
4
4
 
5
5
  import { completionKeymap } from '@codemirror/autocomplete';
6
6
  import { defaultKeymap, indentWithTab } from '@codemirror/commands';
7
+ import { jsonLanguage } from '@codemirror/lang-json';
7
8
  import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
8
- import { syntaxHighlighting } from '@codemirror/language';
9
+ import { xml } from '@codemirror/lang-xml';
10
+ import { LanguageDescription, syntaxHighlighting } from '@codemirror/language';
9
11
  import { languages } from '@codemirror/language-data';
10
12
  import { type Extension } from '@codemirror/state';
11
13
  import { keymap } from '@codemirror/view';
@@ -43,6 +45,7 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
43
45
  base: markdownLanguage,
44
46
 
45
47
  // Languages for syntax highlighting fenced code blocks.
48
+ defaultCodeLanguage: jsonLanguage,
46
49
  codeLanguages: languages,
47
50
 
48
51
  // Don't complete HTML tags.
@@ -66,12 +69,21 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
66
69
 
67
70
  // https://codemirror.net/docs/ref/#commands.defaultKeymap
68
71
  ...defaultKeymap,
72
+
73
+ // TODO(burdon): Remove?
69
74
  ...completionKeymap,
70
75
  ].filter(isTruthy),
71
76
  ),
72
77
  ];
73
78
  };
74
79
 
80
+ const xmlLanguageDesc = LanguageDescription.of({
81
+ name: 'xml',
82
+ alias: ['html', 'xhtml'],
83
+ extensions: ['xml', 'xhtml'],
84
+ load: async () => xml(),
85
+ });
86
+
75
87
  /**
76
88
  * Default customizations.
77
89
  * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
@@ -89,5 +101,5 @@ const noSetExtHeading: MarkdownConfig = {
89
101
  * Remove HTML and XML parsing.
90
102
  */
91
103
  const noHtml: MarkdownConfig = {
92
- remove: ['HTMLBlock', 'HTMLTag'],
104
+ // remove: ['HTMLBlock', 'HTMLTag'],
93
105
  };
@@ -15,10 +15,8 @@ import {
15
15
  } from '@codemirror/state';
16
16
  import { EditorView, type ViewUpdate, keymap } from '@codemirror/view';
17
17
  import { type SyntaxNode, type SyntaxNodeRef } from '@lezer/common';
18
- import { useCallback, useMemo } from 'react';
19
18
 
20
19
  import { debounceAndThrottle } from '@dxos/async';
21
- import { type Live } from '@dxos/live-object';
22
20
 
23
21
  import { type EditorToolbarState } from '../../components';
24
22
 
@@ -1251,17 +1249,19 @@ export const getFormatting = (state: EditorState): Formatting => {
1251
1249
  /**
1252
1250
  * Hook provides an extension to compute the current formatting state.
1253
1251
  */
1254
- export const useFormattingState = (state: Live<EditorToolbarState>): Extension => {
1255
- const handleUpdate = useCallback(
1252
+ export const formattingListener = (stateProvider: () => EditorToolbarState | undefined, delay = 100): Extension => {
1253
+ return EditorView.updateListener.of(
1256
1254
  debounceAndThrottle((update: ViewUpdate) => {
1257
1255
  if (update.docChanged || update.selectionSet) {
1256
+ const state = stateProvider();
1257
+ if (!state) {
1258
+ return;
1259
+ }
1260
+
1258
1261
  Object.entries(getFormatting(update.state)).forEach(([key, active]) => {
1259
1262
  state[key as keyof Formatting] = active as any;
1260
1263
  });
1261
1264
  }
1262
- }, 100),
1263
- [state],
1265
+ }, delay),
1264
1266
  );
1265
-
1266
- return useMemo(() => EditorView.updateListener.of(handleUpdate), [handleUpdate]);
1267
1267
  };
@@ -11,7 +11,7 @@ import { singleValueFacet } from '../util';
11
11
 
12
12
  export type EditorInputConfig = {
13
13
  type?: string;
14
- noTabster?: boolean;
14
+ ignoreEscape?: boolean;
15
15
  };
16
16
 
17
17
  export const editorInputMode = singleValueFacet<EditorInputConfig>({});
@@ -26,7 +26,7 @@ export const InputModeExtensions: { [mode: string]: Extension } = {
26
26
  vim: [
27
27
  // https://github.com/replit/codemirror-vim
28
28
  vim(),
29
- editorInputMode.of({ type: 'vim', noTabster: true }),
29
+ editorInputMode.of({ type: 'vim', ignoreEscape: true }),
30
30
  keymap.of([
31
31
  {
32
32
  key: 'Alt-Escape',