@dxos/react-ui-editor 0.8.4-main.406dc2a → 0.8.4-main.548089c

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 (108) hide show
  1. package/dist/lib/browser/index.mjs +1379 -1139
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +1379 -1139
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Editor/Editor.stories.d.ts +0 -3
  8. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +17 -2
  10. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  11. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  12. package/dist/types/src/components/EditorToolbar/util.d.ts +5 -19
  13. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  15. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  17. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/sync.d.ts +1 -1
  19. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  21. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  22. package/dist/types/src/extensions/autoscroll.d.ts +14 -4
  23. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  24. package/dist/types/src/extensions/awareness/awareness-provider.d.ts +1 -1
  25. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  26. package/dist/types/src/extensions/blocks.d.ts +2 -0
  27. package/dist/types/src/extensions/blocks.d.ts.map +1 -0
  28. package/dist/types/src/extensions/bookmarks.d.ts +12 -0
  29. package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
  30. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  31. package/dist/types/src/extensions/factories.d.ts +4 -4
  32. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  33. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  34. package/dist/types/src/extensions/index.d.ts +4 -0
  35. package/dist/types/src/extensions/index.d.ts.map +1 -1
  36. package/dist/types/src/extensions/listener.d.ts +8 -6
  37. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  39. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -2
  40. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  41. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +1 -1
  42. package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -1
  43. package/dist/types/src/extensions/popover/popover.d.ts.map +1 -1
  44. package/dist/types/src/extensions/preview/preview.d.ts +6 -2
  45. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  46. package/dist/types/src/extensions/replacer.d.ts +21 -0
  47. package/dist/types/src/extensions/replacer.d.ts.map +1 -0
  48. package/dist/types/src/extensions/replacer.test.d.ts +2 -0
  49. package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
  50. package/dist/types/src/extensions/scrolling.d.ts +78 -0
  51. package/dist/types/src/extensions/scrolling.d.ts.map +1 -0
  52. package/dist/types/src/extensions/tags/xml-tags.d.ts +41 -16
  53. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  54. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
  55. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  56. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  57. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  58. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  59. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  60. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  61. package/dist/types/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +41 -38
  63. package/src/components/Editor/Editor.stories.tsx +4 -7
  64. package/src/components/EditorToolbar/EditorToolbar.tsx +90 -90
  65. package/src/components/EditorToolbar/headings.ts +6 -4
  66. package/src/components/EditorToolbar/util.ts +4 -20
  67. package/src/extensions/autocomplete/autocomplete.ts +5 -5
  68. package/src/extensions/automerge/automerge.stories.tsx +1 -1
  69. package/src/extensions/automerge/automerge.ts +1 -1
  70. package/src/extensions/automerge/cursor.ts +1 -1
  71. package/src/extensions/automerge/sync.ts +1 -1
  72. package/src/extensions/automerge/update-automerge.ts +1 -1
  73. package/src/extensions/autoscroll.ts +74 -68
  74. package/src/extensions/awareness/awareness-provider.ts +2 -2
  75. package/src/extensions/blocks.ts +131 -0
  76. package/src/extensions/bookmarks.ts +75 -0
  77. package/src/extensions/comments.ts +2 -1
  78. package/src/extensions/factories.ts +6 -4
  79. package/src/extensions/folding.tsx +1 -2
  80. package/src/extensions/index.ts +4 -0
  81. package/src/extensions/listener.ts +14 -20
  82. package/src/extensions/markdown/bundle.ts +12 -2
  83. package/src/extensions/markdown/decorate.ts +8 -8
  84. package/src/extensions/markdown/formatting.ts +8 -8
  85. package/src/extensions/markdown/highlight.ts +1 -1
  86. package/src/extensions/markdown/image.ts +2 -2
  87. package/src/extensions/markdown/table.ts +6 -6
  88. package/src/extensions/popover/PopoverMenuProvider.tsx +2 -3
  89. package/src/extensions/popover/popover.ts +0 -4
  90. package/src/extensions/preview/preview.ts +14 -9
  91. package/src/extensions/replacer.test.ts +75 -0
  92. package/src/extensions/replacer.ts +93 -0
  93. package/src/extensions/scrolling.ts +189 -0
  94. package/src/extensions/selection.ts +1 -1
  95. package/src/extensions/tags/extended-markdown.test.ts +2 -1
  96. package/src/extensions/tags/xml-tags.ts +310 -203
  97. package/src/extensions/typewriter.ts +1 -1
  98. package/src/stories/CommandDialog.stories.tsx +9 -4
  99. package/src/stories/Comments.stories.tsx +1 -1
  100. package/src/stories/EditorToolbar.stories.tsx +4 -5
  101. package/src/stories/Popover.stories.tsx +4 -6
  102. package/src/stories/Preview.stories.tsx +15 -8
  103. package/src/stories/Tags.stories.tsx +19 -5
  104. package/src/stories/TextEditor.stories.tsx +2 -2
  105. package/src/stories/components/EditorStory.tsx +3 -3
  106. package/src/stories/components/util.tsx +39 -6
  107. package/src/styles/markdown.ts +1 -1
  108. package/src/styles/theme.ts +1 -1
@@ -5,101 +5,117 @@
5
5
  import { StateEffect } from '@codemirror/state';
6
6
  import { EditorView, ViewPlugin } from '@codemirror/view';
7
7
 
8
+ import { debounce } from '@dxos/async';
8
9
  import { Domino } from '@dxos/react-ui';
9
10
 
10
- const lineHeight = 24;
11
+ import { scrollToLineEffect } from './scrolling';
11
12
 
12
- export const scrollToBottomEffect = StateEffect.define<any>();
13
+ // TODO(burdon): Reconcile with scrollToLineEffect (scrolling).
14
+ export const scrollToBottomEffect = StateEffect.define<ScrollBehavior | undefined>();
13
15
 
14
16
  export type AutoScrollOptions = {
15
- overscroll?: number;
16
- throttle?: number;
17
+ /** Auto-scroll when reaches the bottom. */
18
+ autoScroll?: boolean;
19
+ /** Threshold in px to trigger scroll from bottom. */
20
+ threshold?: number;
21
+ /** Throttle time in ms. */
22
+ throttleDelay?: number;
23
+ /** Callback when auto-scrolling. */
24
+ onAutoScroll?: (props: { view: EditorView; distanceFromBottom: number }) => boolean | void;
17
25
  };
18
26
 
19
27
  /**
20
28
  * Extension that supports pinning the scroll position and automatically scrolls to the bottom when content is added.
21
29
  */
22
30
  // TODO(burdon): Reconcile with transcript-extension.
23
- export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Partial<AutoScrollOptions> = {}) => {
24
- let isThrottled = false;
25
- let isPinned = true;
26
- let timeout: NodeJS.Timeout | undefined;
31
+ export const autoScroll = ({
32
+ autoScroll = true,
33
+ threshold = 100,
34
+ throttleDelay = 1_000,
35
+ onAutoScroll,
36
+ }: Partial<AutoScrollOptions> = {}) => {
27
37
  let buttonContainer: HTMLDivElement | undefined;
38
+ let hideTimeout: NodeJS.Timeout | undefined;
28
39
  let lastScrollTop = 0;
29
- let scrollCounter = 0;
40
+ let isPinned = true;
41
+
42
+ const setPinned = (pin: boolean) => {
43
+ isPinned = pin;
44
+ buttonContainer?.classList.toggle('opacity-0', pin);
45
+ };
30
46
 
47
+ // Temporarily hide the scrollbar while auto-scrolling.
31
48
  const hideScrollbar = (view: EditorView) => {
32
49
  view.scrollDOM.classList.add('cm-hide-scrollbar');
33
- clearTimeout(timeout);
34
- timeout = setTimeout(() => {
50
+ clearTimeout(hideTimeout);
51
+ hideTimeout = setTimeout(() => {
35
52
  view.scrollDOM.classList.remove('cm-hide-scrollbar');
36
53
  }, 1_000);
37
54
  };
38
55
 
39
- const scrollToBottom = (view: EditorView) => {
40
- isPinned = true;
41
- scrollCounter = 0;
42
- buttonContainer?.classList.add('opacity-0');
43
- requestAnimationFrame(() => {
44
- hideScrollbar(view);
45
- view.scrollDOM.scrollTo({
46
- top: view.scrollDOM.scrollHeight,
47
- behavior: 'smooth',
48
- });
56
+ // Throttled scroll to bottom.
57
+ const scrollToBottom = (view: EditorView, behavior?: ScrollBehavior) => {
58
+ setPinned(true);
59
+ hideScrollbar(view);
60
+ const line = view.state.doc.lineAt(view.state.doc.length);
61
+ view.dispatch({
62
+ selection: { anchor: line.to, head: line.to },
63
+ effects: scrollToLineEffect.of({ line: line.number, options: { position: 'end', offset: threshold, behavior } }),
49
64
  });
50
65
  };
51
66
 
67
+ // Throttled check for distance from bottom (for downward scrolls only).
68
+ const checkDistance = debounce((view: EditorView) => {
69
+ const scrollerRect = view.scrollDOM.getBoundingClientRect();
70
+ const coords = view.coordsAtPos(view.state.doc.length);
71
+ const distanceFromBottom = coords ? coords.bottom - scrollerRect.bottom : 0;
72
+ setPinned(distanceFromBottom < 0);
73
+ }, 1_000);
74
+
75
+ // Debounce scroll updates so rapid edits don't cause clunky scrolling.
76
+ const triggerUpdate = debounce((view: EditorView) => scrollToBottom(view), throttleDelay);
77
+
52
78
  return [
53
79
  // Update listener for logging when scrolling is needed.
54
- EditorView.updateListener.of((update) => {
55
- // Listen for effects.
56
- update.transactions.forEach((transaction) => {
80
+ EditorView.updateListener.of(({ view, transactions, heightChanged }) => {
81
+ // TODO(burdon): Remove and use scrollToLineEffect instead.
82
+ transactions.forEach((transaction) => {
57
83
  for (const effect of transaction.effects) {
58
84
  if (effect.is(scrollToBottomEffect)) {
59
- scrollToBottom(update.view);
85
+ scrollToBottom(view, effect.value);
60
86
  }
61
87
  }
62
88
  });
63
89
 
64
90
  // Maybe scroll if doc changed and pinned.
65
- if (update.docChanged && isPinned && !isThrottled) {
66
- const distanceFromBottom = calcDistance(update.view.scrollDOM);
67
- if (distanceFromBottom >= overscroll) {
68
- isThrottled = true;
69
- requestAnimationFrame(() => {
70
- scrollToBottom(update.view);
71
- });
72
-
73
- // Reset throttle.
74
- setTimeout(() => {
75
- isThrottled = false;
76
- scrollToBottom(update.view);
77
- }, throttle);
91
+ // NOTE: Geometry changed is triggered when widgets change height (e.g., toggle tool block).
92
+ if (heightChanged && isPinned) {
93
+ const coords = view.coordsAtPos(view.state.doc.length);
94
+ const scrollerRect = view.scrollDOM.getBoundingClientRect();
95
+ const distanceFromBottom = coords ? scrollerRect.bottom - coords.bottom : 0;
96
+ if (autoScroll && distanceFromBottom < threshold) {
97
+ const shouldScroll = onAutoScroll?.({ view, distanceFromBottom }) ?? true;
98
+ if (shouldScroll) {
99
+ triggerUpdate(view);
100
+ }
101
+ } else if (distanceFromBottom < 0) {
102
+ setPinned(false);
78
103
  }
79
104
  }
80
105
  }),
81
106
 
82
107
  // Detect user scroll.
83
- // NOTE: Multiple scroll events are triggered during programmatic smooth scrolling.
84
108
  EditorView.domEventHandlers({
85
109
  scroll: (event, view) => {
86
- const scroller = view.scrollDOM;
87
- // Suspect delta goes positive when rendering widgets, so count positive deltas.
88
- // TODO(burdon): Detect user scroll directly (wheel, touch, keys, etc.)
89
- if (lastScrollTop > scroller.scrollTop) {
90
- scrollCounter++;
91
- }
92
- lastScrollTop = scroller.scrollTop;
93
- const distanceFromBottom = calcDistance(scroller);
94
- if (distanceFromBottom === 0) {
95
- // Pin to bottom.
96
- isPinned = true;
97
- buttonContainer?.classList.add('opacity-0');
98
- scrollCounter = 0;
99
- } else if (scrollCounter > 3) {
100
- // Break pin if user scrolls up.
101
- isPinned = false;
102
- buttonContainer?.classList.remove('opacity-0');
110
+ const currentScrollTop = view.scrollDOM.scrollTop;
111
+ const scrollingUp = currentScrollTop < lastScrollTop;
112
+ lastScrollTop = currentScrollTop;
113
+
114
+ // If user scrolls up, immediately unpin auto-scroll.
115
+ if (scrollingUp) {
116
+ setPinned(false);
117
+ } else {
118
+ checkDistance(view);
103
119
  }
104
120
  },
105
121
  }),
@@ -108,7 +124,6 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
108
124
  ViewPlugin.fromClass(
109
125
  class {
110
126
  constructor(view: EditorView) {
111
- const scroller = view.scrollDOM.parentElement;
112
127
  buttonContainer = Domino.of('div')
113
128
  .classNames(true && 'cm-scroll-button transition-opacity duration-300 opacity-0')
114
129
  .children(
@@ -121,7 +136,8 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
121
136
  }),
122
137
  )
123
138
  .build();
124
- scroller?.appendChild(buttonContainer);
139
+
140
+ view.scrollDOM.parentElement!.appendChild(buttonContainer);
125
141
  }
126
142
  },
127
143
  ),
@@ -129,7 +145,6 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
129
145
  // Styles.
130
146
  EditorView.theme({
131
147
  '.cm-scroller': {
132
- paddingBottom: `${overscroll}px`,
133
148
  scrollbarWidth: 'thin',
134
149
  },
135
150
  '.cm-scroller.cm-hide-scrollbar': {
@@ -138,7 +153,6 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
138
153
  '.cm-scroller.cm-hide-scrollbar::-webkit-scrollbar': {
139
154
  display: 'none',
140
155
  },
141
-
142
156
  '.cm-scroll-button': {
143
157
  position: 'absolute',
144
158
  bottom: '0.5rem',
@@ -147,11 +161,3 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
147
161
  }),
148
162
  ];
149
163
  };
150
-
151
- const calcDistance = (scroller: HTMLElement) => {
152
- const scrollTop = scroller.scrollTop;
153
- const scrollHeight = scroller.scrollHeight;
154
- const clientHeight = scroller.clientHeight;
155
- const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
156
- return distanceFromBottom;
157
- };
@@ -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
 
@@ -0,0 +1,131 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { RangeSetBuilder } from '@codemirror/state';
6
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
7
+
8
+ import { mx } from '@dxos/react-ui-theme';
9
+
10
+ const paragraphBlockPlugin = ViewPlugin.fromClass(
11
+ class {
12
+ decorations: DecorationSet;
13
+
14
+ constructor(view: EditorView) {
15
+ this.decorations = this.build(view);
16
+ }
17
+
18
+ update(update: ViewUpdate) {
19
+ if (update.docChanged || update.viewportChanged) {
20
+ this.decorations = this.build(update.view);
21
+ }
22
+ }
23
+
24
+ build({ state }: EditorView) {
25
+ const builder = new RangeSetBuilder<Decoration>();
26
+
27
+ // Helper: commit a block from blockStart to endLine (inclusive).
28
+ const pushBlock = (fromLine: number, toLine: number) => {
29
+ // Add line decorations for each line in the block.
30
+ for (let lineNum = fromLine; lineNum <= toLine; lineNum++) {
31
+ const line = state.doc.line(lineNum);
32
+ builder.add(
33
+ line.from,
34
+ line.from,
35
+ Decoration.line({
36
+ class: mx(
37
+ 'block-line',
38
+ fromLine === toLine && 'block-single',
39
+ lineNum === fromLine && 'block-first',
40
+ lineNum > fromLine && lineNum < toLine && 'block-middle',
41
+ lineNum === toLine && 'block-last',
42
+ ),
43
+ }),
44
+ );
45
+ }
46
+ };
47
+
48
+ let blockStart: number | null = null;
49
+ let consecutiveBlankLines = 0;
50
+ const totalLines = state.doc.lines;
51
+ for (let i = 1; i <= totalLines; i++) {
52
+ const line = state.doc.line(i);
53
+ const isBlank = /^\s*$/.test(line.text);
54
+
55
+ if (!isBlank) {
56
+ // Reset blank line counter.
57
+ consecutiveBlankLines = 0;
58
+ // Start a new block if we're not already in one.
59
+ if (blockStart === null) {
60
+ blockStart = i;
61
+ }
62
+ } else {
63
+ // Increment blank line counter.
64
+ consecutiveBlankLines++;
65
+
66
+ // End the current block if we have 2+ consecutive blank lines.
67
+ if (consecutiveBlankLines >= 2 && blockStart !== null) {
68
+ pushBlock(blockStart, i - consecutiveBlankLines);
69
+ blockStart = null;
70
+ }
71
+ }
72
+ }
73
+
74
+ // Handle any remaining block at the end of the document.
75
+ if (blockStart !== null) {
76
+ // Find the last non-blank line for the block end.
77
+ let lastNonBlankLine = totalLines;
78
+ while (lastNonBlankLine >= blockStart) {
79
+ const line = state.doc.line(lastNonBlankLine);
80
+ if (!/^\s*$/.test(line.text)) {
81
+ break;
82
+ }
83
+ lastNonBlankLine--;
84
+ }
85
+ if (lastNonBlankLine >= blockStart) {
86
+ pushBlock(blockStart, lastNonBlankLine);
87
+ }
88
+ }
89
+
90
+ return builder.finish();
91
+ }
92
+ },
93
+ {
94
+ decorations: (v) => v.decorations,
95
+ },
96
+ );
97
+
98
+ export const blocks = () => [
99
+ paragraphBlockPlugin,
100
+ EditorView.baseTheme({
101
+ '.cm-line.block-line': {
102
+ paddingLeft: '0.75rem',
103
+ paddingRight: '0.75rem',
104
+ borderLeft: '1px solid var(--dx-subduedSeparator)',
105
+ borderRight: '1px solid var(--dx-subduedSeparator)',
106
+ },
107
+ '.cm-line.block-single': {
108
+ border: '1px solid var(--dx-subduedSeparator)',
109
+ borderRadius: '6px',
110
+ paddingTop: '0.5rem',
111
+ paddingBottom: '0.5rem',
112
+ marginTop: '0.5rem',
113
+ marginBottom: '0.5rem',
114
+ },
115
+ '.cm-line.block-first': {
116
+ borderTop: '1px solid var(--dx-subduedSeparator)',
117
+ borderTopLeftRadius: '6px',
118
+ borderTopRightRadius: '6px',
119
+ paddingTop: '0.5rem',
120
+ marginTop: '0.5rem',
121
+ },
122
+ '.cm-line.block-middle': {},
123
+ '.cm-line.block-last': {
124
+ borderBottom: '1px solid var(--dx-subduedSeparator)',
125
+ borderBottomLeftRadius: '6px',
126
+ borderBottomRightRadius: '6px',
127
+ paddingBottom: '0.5rem',
128
+ marginBottom: '0.5rem',
129
+ },
130
+ }),
131
+ ];
@@ -0,0 +1,75 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Extension, Prec, StateEffect, StateField } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+
8
+ type Bookmark = {
9
+ id: string;
10
+ pos: number;
11
+ name: string;
12
+ };
13
+
14
+ export const addBookmark = StateEffect.define<Bookmark>();
15
+ export const removeBookmark = StateEffect.define<string>();
16
+ export const clearBookmarks = StateEffect.define<void>();
17
+
18
+ export const bookmarks = (): Extension => {
19
+ return [
20
+ bookmarksField,
21
+ Prec.highest(
22
+ keymap.of([
23
+ {
24
+ key: 'Mod-ArrowUp',
25
+ run: (view) => {
26
+ const bookmarks = view.state.field(bookmarksField);
27
+ console.log('up', bookmarks);
28
+ return true;
29
+ },
30
+ },
31
+ {
32
+ key: 'Mod-ArrowDown',
33
+ run: (view) => {
34
+ const bookmarks = view.state.field(bookmarksField);
35
+ console.log('down', bookmarks);
36
+ return true;
37
+ },
38
+ },
39
+ ]),
40
+ ),
41
+ ];
42
+ };
43
+
44
+ type BookmarkFieldState = {
45
+ bookmarks: Bookmark[];
46
+ };
47
+
48
+ const bookmarksField = StateField.define<BookmarkFieldState>({
49
+ create: (): BookmarkFieldState => ({
50
+ bookmarks: [],
51
+ }),
52
+
53
+ update: (value: BookmarkFieldState, tr): BookmarkFieldState => {
54
+ // Map bookmark positions through document changes.
55
+ let bookmarks = value.bookmarks.map((bookmark) => ({
56
+ ...bookmark,
57
+ pos: tr.changes.mapPos(bookmark.pos),
58
+ }));
59
+
60
+ // Process effects.
61
+ for (const effect of tr.effects) {
62
+ if (effect.is(addBookmark)) {
63
+ bookmarks = [...bookmarks, effect.value];
64
+ } else if (effect.is(removeBookmark)) {
65
+ bookmarks = bookmarks.filter((b) => b.id !== effect.value);
66
+ } else if (effect.is(clearBookmarks)) {
67
+ bookmarks = [];
68
+ }
69
+ }
70
+
71
+ // Sort bookmarks by position.
72
+ bookmarks.sort(({ pos: a }, { pos: b }) => a - b);
73
+ return { bookmarks };
74
+ },
75
+ });
@@ -104,13 +104,14 @@ export const commentsState = StateField.define<CommentsState>({
104
104
  const styles = EditorView.theme({
105
105
  '.cm-comment, .cm-comment-current': {
106
106
  padding: '3px 0',
107
+ color: 'var(--dx-cmCommentText)',
107
108
  backgroundColor: 'var(--dx-cmCommentSurface)',
108
109
  },
109
110
  '.cm-comment > span, .cm-comment-current > span': {
110
111
  boxDecorationBreak: 'clone',
111
112
  boxShadow: '0 0 1px 3px var(--dx-cmCommentSurface)',
112
113
  backgroundColor: 'var(--dx-cmCommentSurface)',
113
- color: 'var(--dx-cmComment)',
114
+ color: 'var(--dx-cmCommentText)',
114
115
  cursor: 'pointer',
115
116
  },
116
117
  });
@@ -23,10 +23,10 @@ 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,7 +87,6 @@ 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;
@@ -136,7 +135,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
136
135
  props.history && history(),
137
136
  props.lineNumbers && [lineNumbers(), editorGutter],
138
137
  props.lineWrapping && EditorView.lineWrapping,
139
- props.monospace && editorMonospace,
140
138
  props.placeholder && placeholder(props.placeholder),
141
139
  props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
142
140
  props.scrollPastEnd && scrollPastEnd(),
@@ -173,6 +171,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
173
171
 
174
172
  export type ThemeExtensionsOptions = {
175
173
  themeMode?: ThemeMode;
174
+ monospace?: boolean;
176
175
  styles?: ThemeStyles;
177
176
  syntaxHighlighting?: boolean;
178
177
  slots?: {
@@ -180,6 +179,7 @@ export type ThemeExtensionsOptions = {
180
179
  className?: string;
181
180
  };
182
181
  scroll?: {
182
+ // NOTE: Do not apply vertical padding to scroll container.
183
183
  className?: string;
184
184
  };
185
185
  content?: {
@@ -212,6 +212,7 @@ export const defaultStyles = {
212
212
  */
213
213
  export const createThemeExtensions = ({
214
214
  themeMode,
215
+ monospace,
215
216
  styles,
216
217
  syntaxHighlighting: syntaxHighlightingProp,
217
218
  slots: slotsParam,
@@ -220,6 +221,7 @@ export const createThemeExtensions = ({
220
221
  return [
221
222
  EditorView.darkTheme.of(themeMode === 'dark'),
222
223
  EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
224
+ monospace && editorMonospace,
223
225
  syntaxHighlightingProp &&
224
226
  syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
225
227
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
@@ -25,8 +25,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
25
25
  foldGutter({
26
26
  markerDOM: (open) => {
27
27
  return renderRoot(
28
- Domino.of('div').classNames('flex h-full items-center').build(),
29
- // TODO(burdon): Use sprite directly.
28
+ Domino.of('div').classNames('flex bs-full items-center').build(),
30
29
  <Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
31
30
  );
32
31
  },
@@ -8,6 +8,8 @@ export * from './autoscroll';
8
8
  export * from './automerge';
9
9
  export * from './awareness';
10
10
  export * from './blast';
11
+ export * from './blocks';
12
+ export * from './bookmarks';
11
13
  export * from './comments';
12
14
  export * from './debug';
13
15
  export * from './dnd';
@@ -23,7 +25,9 @@ export * from './modes';
23
25
  export * from './outliner';
24
26
  export * from './popover';
25
27
  export * from './preview';
28
+ export * from './replacer';
26
29
  export * from './selection';
30
+ export * from './scrolling';
27
31
  export * from './state';
28
32
  export * from './tags';
29
33
  export * from './typewriter';
@@ -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.
@@ -74,6 +77,13 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
74
77
  ];
75
78
  };
76
79
 
80
+ const xmlLanguageDesc = LanguageDescription.of({
81
+ name: 'xml',
82
+ alias: ['html', 'xhtml'],
83
+ extensions: ['xml', 'xhtml'],
84
+ load: async () => xml(),
85
+ });
86
+
77
87
  /**
78
88
  * Default customizations.
79
89
  * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
@@ -91,5 +101,5 @@ const noSetExtHeading: MarkdownConfig = {
91
101
  * Remove HTML and XML parsing.
92
102
  */
93
103
  const noHtml: MarkdownConfig = {
94
- remove: ['HTMLBlock', 'HTMLTag'],
104
+ // remove: ['HTMLBlock', 'HTMLTag'],
95
105
  };