@dxos/ui-editor 0.0.0 → 0.8.4-main.05e74ebcff

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 (263) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/index.mjs +8657 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/lib/browser/types/index.mjs +33 -0
  7. package/dist/lib/browser/types/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +8659 -0
  9. package/dist/lib/node-esm/index.mjs.map +7 -0
  10. package/dist/lib/node-esm/meta.json +1 -0
  11. package/dist/lib/node-esm/types/index.mjs +35 -0
  12. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  13. package/dist/types/src/defaults.d.ts +6 -0
  14. package/dist/types/src/defaults.d.ts.map +1 -0
  15. package/dist/types/src/extensions/annotations.d.ts +9 -0
  16. package/dist/types/src/extensions/annotations.d.ts.map +1 -0
  17. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts +17 -0
  18. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  19. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  20. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  21. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  22. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  23. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +23 -0
  24. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  25. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  26. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/automerge.d.ts +4 -0
  28. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  30. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  31. package/dist/types/src/extensions/automerge/cursor.d.ts +4 -0
  32. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  33. package/dist/types/src/extensions/automerge/defs.d.ts +17 -0
  34. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  35. package/dist/types/src/extensions/automerge/index.d.ts +2 -0
  36. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  37. package/dist/types/src/extensions/automerge/sync.d.ts +17 -0
  38. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -0
  39. package/dist/types/src/extensions/automerge/update-automerge.d.ts +6 -0
  40. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  41. package/dist/types/src/extensions/automerge/update-codemirror.d.ts +5 -0
  42. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  43. package/dist/types/src/extensions/awareness/awareness-provider.d.ts +31 -0
  44. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
  45. package/dist/types/src/extensions/awareness/awareness.d.ts +46 -0
  46. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
  47. package/dist/types/src/extensions/awareness/index.d.ts +3 -0
  48. package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
  49. package/dist/types/src/extensions/blast.d.ts +25 -0
  50. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  51. package/dist/types/src/extensions/blocks.d.ts +2 -0
  52. package/dist/types/src/extensions/blocks.d.ts.map +1 -0
  53. package/dist/types/src/extensions/bookmarks.d.ts +12 -0
  54. package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
  55. package/dist/types/src/extensions/comments.d.ts +90 -0
  56. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  57. package/dist/types/src/extensions/debug.d.ts +3 -0
  58. package/dist/types/src/extensions/debug.d.ts.map +1 -0
  59. package/dist/types/src/extensions/dnd.d.ts +9 -0
  60. package/dist/types/src/extensions/dnd.d.ts.map +1 -0
  61. package/dist/types/src/extensions/factories.d.ts +88 -0
  62. package/dist/types/src/extensions/factories.d.ts.map +1 -0
  63. package/dist/types/src/extensions/factories.test.d.ts +2 -0
  64. package/dist/types/src/extensions/factories.test.d.ts.map +1 -0
  65. package/dist/types/src/extensions/focus.d.ts +7 -0
  66. package/dist/types/src/extensions/focus.d.ts.map +1 -0
  67. package/dist/types/src/extensions/folding.d.ts +6 -0
  68. package/dist/types/src/extensions/folding.d.ts.map +1 -0
  69. package/dist/types/src/extensions/hashtag.d.ts +3 -0
  70. package/dist/types/src/extensions/hashtag.d.ts.map +1 -0
  71. package/dist/types/src/extensions/index.d.ts +30 -0
  72. package/dist/types/src/extensions/index.d.ts.map +1 -0
  73. package/dist/types/src/extensions/json.d.ts +7 -0
  74. package/dist/types/src/extensions/json.d.ts.map +1 -0
  75. package/dist/types/src/extensions/listener.d.ts +13 -0
  76. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  77. package/dist/types/src/extensions/markdown/action.d.ts +12 -0
  78. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  79. package/dist/types/src/extensions/markdown/bundle.d.ts +25 -0
  80. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  81. package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
  82. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
  83. package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
  84. package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
  85. package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
  86. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
  87. package/dist/types/src/extensions/markdown/decorate.d.ts +25 -0
  88. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
  89. package/dist/types/src/extensions/markdown/formatting.d.ts +63 -0
  90. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  91. package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
  92. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
  93. package/dist/types/src/extensions/markdown/highlight.d.ts +37 -0
  94. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  95. package/dist/types/src/extensions/markdown/image.d.ts +7 -0
  96. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  97. package/dist/types/src/extensions/markdown/index.d.ts +10 -0
  98. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  99. package/dist/types/src/extensions/markdown/link.d.ts +7 -0
  100. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  101. package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
  102. package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
  103. package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
  104. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
  105. package/dist/types/src/extensions/markdown/table.d.ts +8 -0
  106. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  107. package/dist/types/src/extensions/mention.d.ts +7 -0
  108. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  109. package/dist/types/src/extensions/modal.d.ts +7 -0
  110. package/dist/types/src/extensions/modal.d.ts.map +1 -0
  111. package/dist/types/src/extensions/modes.d.ts +10 -0
  112. package/dist/types/src/extensions/modes.d.ts.map +1 -0
  113. package/dist/types/src/extensions/outliner/commands.d.ts +10 -0
  114. package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
  115. package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
  116. package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
  117. package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
  118. package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
  119. package/dist/types/src/extensions/outliner/index.d.ts +4 -0
  120. package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
  121. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  122. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  123. package/dist/types/src/extensions/outliner/outliner.d.ts +11 -0
  124. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
  125. package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
  126. package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
  127. package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
  128. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
  129. package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
  130. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
  131. package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
  132. package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
  133. package/dist/types/src/extensions/preview/index.d.ts +2 -0
  134. package/dist/types/src/extensions/preview/index.d.ts.map +1 -0
  135. package/dist/types/src/extensions/preview/preview.d.ts +34 -0
  136. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -0
  137. package/dist/types/src/extensions/replacer.d.ts +21 -0
  138. package/dist/types/src/extensions/replacer.d.ts.map +1 -0
  139. package/dist/types/src/extensions/replacer.test.d.ts +2 -0
  140. package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
  141. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts +18 -0
  142. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
  143. package/dist/types/src/extensions/scrolling/crawler.d.ts +75 -0
  144. package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
  145. package/dist/types/src/extensions/scrolling/index.d.ts +5 -0
  146. package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
  147. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts +3 -0
  148. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
  149. package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
  150. package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
  151. package/dist/types/src/extensions/selection.d.ts +24 -0
  152. package/dist/types/src/extensions/selection.d.ts.map +1 -0
  153. package/dist/types/src/extensions/snippets.d.ts +10 -0
  154. package/dist/types/src/extensions/snippets.d.ts.map +1 -0
  155. package/dist/types/src/extensions/state.d.ts +2 -0
  156. package/dist/types/src/extensions/state.d.ts.map +1 -0
  157. package/dist/types/src/extensions/submit.d.ts +10 -0
  158. package/dist/types/src/extensions/submit.d.ts.map +1 -0
  159. package/dist/types/src/extensions/tags/extended-markdown.d.ts +10 -0
  160. package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -0
  161. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts +2 -0
  162. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +1 -0
  163. package/dist/types/src/extensions/tags/fader.d.ts +12 -0
  164. package/dist/types/src/extensions/tags/fader.d.ts.map +1 -0
  165. package/dist/types/src/extensions/tags/index.d.ts +7 -0
  166. package/dist/types/src/extensions/tags/index.d.ts.map +1 -0
  167. package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
  168. package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
  169. package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
  170. package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
  171. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
  172. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
  173. package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
  174. package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
  175. package/dist/types/src/extensions/tags/xml-tags.d.ts +117 -0
  176. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -0
  177. package/dist/types/src/extensions/tags/xml-util.d.ts +10 -0
  178. package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -0
  179. package/dist/types/src/extensions/tags/xml-util.test.d.ts +2 -0
  180. package/dist/types/src/extensions/tags/xml-util.test.d.ts.map +1 -0
  181. package/dist/types/src/index.d.ts +8 -0
  182. package/dist/types/src/index.d.ts.map +1 -0
  183. package/dist/types/src/styles/index.d.ts +2 -0
  184. package/dist/types/src/styles/index.d.ts.map +1 -0
  185. package/dist/types/src/styles/theme.d.ts +58 -0
  186. package/dist/types/src/styles/theme.d.ts.map +1 -0
  187. package/dist/types/src/types/index.d.ts +2 -0
  188. package/dist/types/src/types/index.d.ts.map +1 -0
  189. package/dist/types/src/types/types.d.ts +21 -0
  190. package/dist/types/src/types/types.d.ts.map +1 -0
  191. package/dist/types/src/util/cursor.d.ts +31 -0
  192. package/dist/types/src/util/cursor.d.ts.map +1 -0
  193. package/dist/types/src/util/debug.d.ts +17 -0
  194. package/dist/types/src/util/debug.d.ts.map +1 -0
  195. package/dist/types/src/util/decorations.d.ts +4 -0
  196. package/dist/types/src/util/decorations.d.ts.map +1 -0
  197. package/dist/types/src/util/dom.d.ts +10 -0
  198. package/dist/types/src/util/dom.d.ts.map +1 -0
  199. package/dist/types/src/util/facet.d.ts +3 -0
  200. package/dist/types/src/util/facet.d.ts.map +1 -0
  201. package/dist/types/src/util/index.d.ts +7 -0
  202. package/dist/types/src/util/index.d.ts.map +1 -0
  203. package/dist/types/src/util/util.d.ts +8 -0
  204. package/dist/types/src/util/util.d.ts.map +1 -0
  205. package/dist/types/tsconfig.tsbuildinfo +1 -0
  206. package/package.json +43 -44
  207. package/src/defaults.ts +33 -20
  208. package/src/extensions/annotations.ts +1 -1
  209. package/src/extensions/autocomplete/placeholder.ts +37 -18
  210. package/src/extensions/automerge/automerge.test.tsx +37 -11
  211. package/src/extensions/automerge/automerge.ts +5 -7
  212. package/src/extensions/blocks.ts +5 -5
  213. package/src/extensions/comments.ts +5 -6
  214. package/src/extensions/dnd.ts +2 -2
  215. package/src/extensions/factories.test.ts +88 -0
  216. package/src/extensions/factories.ts +32 -15
  217. package/src/extensions/folding.ts +5 -22
  218. package/src/extensions/index.ts +2 -3
  219. package/src/extensions/markdown/action.ts +0 -1
  220. package/src/extensions/markdown/bundle.ts +23 -9
  221. package/src/extensions/markdown/decorate.ts +15 -12
  222. package/src/extensions/markdown/formatting.ts +5 -10
  223. package/src/extensions/markdown/highlight.ts +15 -7
  224. package/src/extensions/markdown/link.ts +27 -33
  225. package/src/extensions/markdown/parser.test.ts +0 -1
  226. package/src/extensions/markdown/styles.ts +42 -9
  227. package/src/extensions/markdown/table.ts +24 -2
  228. package/src/extensions/outliner/outliner.test.ts +0 -1
  229. package/src/extensions/outliner/outliner.ts +3 -4
  230. package/src/extensions/outliner/tree.test.ts +0 -1
  231. package/src/extensions/preview/preview.ts +62 -15
  232. package/src/extensions/scrolling/auto-scroll.ts +244 -0
  233. package/src/extensions/scrolling/crawler.ts +263 -0
  234. package/src/extensions/scrolling/index.ts +8 -0
  235. package/src/extensions/scrolling/scroll-past-end.ts +32 -0
  236. package/src/extensions/scrolling/scroller.ts +27 -0
  237. package/src/extensions/selection.ts +1 -1
  238. package/src/extensions/snippets.ts +67 -0
  239. package/src/extensions/tags/extended-markdown.test.ts +120 -2
  240. package/src/extensions/tags/extended-markdown.ts +80 -1
  241. package/src/extensions/tags/fader.ts +195 -0
  242. package/src/extensions/tags/index.ts +4 -1
  243. package/src/extensions/tags/testing/text.md +36 -0
  244. package/src/extensions/tags/testing/text.txt +35 -0
  245. package/src/extensions/tags/typewriter.test.ts +65 -0
  246. package/src/extensions/tags/typewriter.ts +594 -0
  247. package/src/extensions/tags/xml-block-decoration.ts +123 -0
  248. package/src/extensions/tags/xml-formatting.ts +125 -0
  249. package/src/extensions/tags/xml-tags.ts +186 -35
  250. package/src/extensions/tags/xml-util.test.ts +199 -24
  251. package/src/extensions/tags/xml-util.ts +62 -5
  252. package/src/index.ts +0 -1
  253. package/src/styles/index.ts +0 -2
  254. package/src/styles/theme.ts +125 -33
  255. package/src/types/types.ts +10 -2
  256. package/src/typings.d.ts +8 -0
  257. package/src/util/cursor.ts +1 -2
  258. package/src/extensions/autoscroll.ts +0 -165
  259. package/src/extensions/scrolling.ts +0 -189
  260. package/src/extensions/tags/streamer.ts +0 -243
  261. package/src/extensions/typewriter.ts +0 -68
  262. package/src/styles/markdown.ts +0 -26
  263. package/src/styles/tokens.ts +0 -17
@@ -8,7 +8,6 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
8
8
  import { mx } from '@dxos/ui-theme';
9
9
 
10
10
  import { decorateMarkdown } from '../markdown';
11
-
12
11
  import { commands } from './commands';
13
12
  import { editor } from './editor';
14
13
  import { menu } from './menu';
@@ -61,7 +60,7 @@ export const outliner = (_options: OutlinerProps = {}): Extension => [
61
60
  decorateMarkdown({ listPaddingLeft: 8 }),
62
61
 
63
62
  // Researve space for menu.
64
- EditorView.contentAttributes.of({ class: 'is-full !mr-[3rem]' }),
63
+ EditorView.contentAttributes.of({ class: 'w-full !mr-[3rem]' }),
65
64
  ];
66
65
 
67
66
  /**
@@ -157,10 +156,10 @@ const decorations = () => [
157
156
  },
158
157
 
159
158
  '.cm-list-item-focused': {
160
- borderColor: 'var(--dx-neutralFocusIndicator)',
159
+ borderColor: 'var(--color-focus-ring-subtle)',
161
160
  },
162
161
  '&:focus-within .cm-list-item-selected': {
163
- borderColor: 'var(--dx-separator)',
162
+ borderColor: 'var(--color-separator)',
164
163
  },
165
164
  }),
166
165
  ),
@@ -8,7 +8,6 @@ import { beforeEach, describe, test } from 'vitest';
8
8
 
9
9
  import { type Range } from '../../types';
10
10
  import { join } from '../../util';
11
-
12
11
  import { type Item, listItemToString, outlinerTree, treeFacet } from './tree';
13
12
 
14
13
  const lines = [
@@ -3,10 +3,12 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type Extension, RangeSetBuilder, StateField } from '@codemirror/state';
7
- import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
6
+ import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
8
8
  import { type SyntaxNode } from '@lezer/common';
9
9
 
10
+ import { type Database, DXN, Entity } from '@dxos/echo';
11
+
10
12
  export type PreviewBlock = {
11
13
  link: PreviewLinkRef;
12
14
  el: HTMLElement;
@@ -16,7 +18,7 @@ export type PreviewLinkRef = {
16
18
  suggest?: boolean;
17
19
  block?: boolean;
18
20
  label: string;
19
- ref: string;
21
+ dxn: string;
20
22
  };
21
23
 
22
24
  export type PreviewLinkTarget = {
@@ -26,22 +28,28 @@ export type PreviewLinkTarget = {
26
28
  };
27
29
 
28
30
  export type PreviewOptions = {
31
+ db?: Database.Database;
29
32
  addBlockContainer?: (block: PreviewBlock) => void;
30
33
  removeBlockContainer?: (block: PreviewBlock) => void;
31
34
  };
32
35
 
36
+ const labelResolvedEffect = StateEffect.define<void>();
37
+
33
38
  /**
34
39
  * Create preview decorations.
35
40
  */
36
41
  export const preview = (options: PreviewOptions = {}): Extension => {
42
+ // Mutable ref so the StateField's onLoad callback can dispatch into the view.
43
+ const viewRef: { current: EditorView | undefined } = { current: undefined };
44
+
37
45
  return [
38
46
  // NOTE: Atomic block decorations must be created from a state field, now a widget, otherwise it results in the following error:
39
47
  // "Block decorations may not be specified via plugins".
40
48
  StateField.define<DecorationSet>({
41
- create: (state) => buildDecorations(state, options),
49
+ create: (state) => buildDecorations(state, options, viewRef),
42
50
  update: (decorations, tr) => {
43
- if (tr.docChanged) {
44
- return buildDecorations(tr.state, options);
51
+ if (tr.docChanged || tr.effects.some((effect) => effect.is(labelResolvedEffect))) {
52
+ return buildDecorations(tr.state, options, viewRef);
45
53
  }
46
54
 
47
55
  return decorations.map(tr.changes);
@@ -51,14 +59,51 @@ export const preview = (options: PreviewOptions = {}): Extension => {
51
59
  EditorView.atomicRanges.of((view) => view.state.field(field)),
52
60
  ],
53
61
  }),
62
+ ViewPlugin.define((view) => {
63
+ viewRef.current = view;
64
+ return {
65
+ destroy() {
66
+ viewRef.current = undefined;
67
+ },
68
+ };
69
+ }),
54
70
  ];
55
71
  };
56
72
 
73
+ /**
74
+ * Resolve a DXN to a display label using the database.
75
+ */
76
+ const resolveLabel = (
77
+ db: Database.Database,
78
+ dxnStr: string,
79
+ viewRef: { current: EditorView | undefined },
80
+ ): string | undefined => {
81
+ const dxn = DXN.tryParse(dxnStr);
82
+ if (!dxn) {
83
+ return;
84
+ }
85
+
86
+ const ref = db.makeRef(dxn);
87
+ const target = ref.target;
88
+ if (target) {
89
+ return Entity.getLabel(target);
90
+ }
91
+
92
+ // Object not loaded yet — schedule a decoration rebuild when it resolves.
93
+ void ref.tryLoad().then(() => {
94
+ viewRef.current?.dispatch({ effects: labelResolvedEffect.of(undefined) });
95
+ });
96
+ };
97
+
57
98
  /**
58
99
  * Echo references are represented as markdown reference links.
59
100
  * https://www.markdownguide.org/basic-syntax/#reference-style-links
60
101
  */
61
- const buildDecorations = (state: EditorState, options: PreviewOptions): DecorationSet => {
102
+ const buildDecorations = (
103
+ state: EditorState,
104
+ options: PreviewOptions,
105
+ viewRef: { current: EditorView | undefined },
106
+ ): DecorationSet => {
62
107
  const builder = new RangeSetBuilder<Decoration>();
63
108
 
64
109
  syntaxTree(state).iterate({
@@ -71,11 +116,13 @@ const buildDecorations = (state: EditorState, options: PreviewOptions): Decorati
71
116
  case 'Link': {
72
117
  const link = getLinkRef(state, node.node);
73
118
  if (link) {
119
+ const resolved = options.db ? resolveLabel(options.db, link.dxn, viewRef) : undefined;
120
+ const displayLink = resolved ? { ...link, label: resolved } : link;
74
121
  builder.add(
75
122
  node.from,
76
123
  node.to,
77
124
  Decoration.replace({
78
- widget: new PreviewInlineWidget(options, link),
125
+ widget: new PreviewInlineWidget(options, displayLink),
79
126
  side: 1,
80
127
  }),
81
128
  );
@@ -119,13 +166,13 @@ export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef
119
166
  const mark = node.getChildren('LinkMark');
120
167
  const urlNode = node.getChild('URL');
121
168
  if (mark && urlNode) {
122
- const url = state.sliceDoc(urlNode.from, urlNode.to);
123
- if (url.startsWith('dxn:')) {
169
+ const dxn = state.sliceDoc(urlNode.from, urlNode.to);
170
+ if (dxn.startsWith('dxn:')) {
124
171
  const label = state.sliceDoc(mark[0].to, mark[1].from);
125
172
  return {
126
173
  block: state.sliceDoc(mark[0].from, mark[0].from + 1) === '!',
127
174
  label,
128
- ref: url,
175
+ dxn,
129
176
  };
130
177
  }
131
178
  }
@@ -148,14 +195,14 @@ class PreviewInlineWidget extends WidgetType {
148
195
  // }
149
196
 
150
197
  override eq(other: this) {
151
- return this._link.ref === other._link.ref && this._link.label === other._link.label;
198
+ return this._link.dxn === other._link.dxn && this._link.label === other._link.label;
152
199
  }
153
200
 
154
201
  override toDOM(_view: EditorView) {
155
202
  const root = document.createElement('dx-anchor');
156
203
  root.classList.add('dx-tag--anchor');
157
204
  root.textContent = this._link.label;
158
- root.setAttribute('refId', this._link.ref);
205
+ root.setAttribute('dxn', this._link.dxn);
159
206
  return root;
160
207
  }
161
208
  }
@@ -177,12 +224,12 @@ class PreviewBlockWidget extends WidgetType {
177
224
  // }
178
225
 
179
226
  override eq(other: this) {
180
- return this._link.ref === other._link.ref;
227
+ return this._link.dxn === other._link.dxn;
181
228
  }
182
229
 
183
230
  override toDOM(_view: EditorView) {
184
231
  const root = document.createElement('div');
185
- root.classList.add('cm-preview-block', 'density-fine');
232
+ root.classList.add('cm-preview-block', 'dx-density-fine');
186
233
  this._options.addBlockContainer?.({ link: this._link, el: root });
187
234
  return root;
188
235
  }
@@ -0,0 +1,244 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { StateEffect } from '@codemirror/state';
6
+ import { EditorView, ViewPlugin } from '@codemirror/view';
7
+
8
+ import { addEventListener, combine, throttle } from '@dxos/async';
9
+ import { Domino } from '@dxos/ui';
10
+ import { getSize } from '@dxos/ui-theme';
11
+
12
+ import { crawlerActiveEffect, crawlerLineEffect } from './crawler';
13
+
14
+ /** Enable or disable autoscroll. */
15
+ export const autoScrollEffect = StateEffect.define<boolean>();
16
+
17
+ export type AutoScrollProps = {
18
+ /**
19
+ * If true, immediately jump to the bottom and re-pin whenever the size of the
20
+ * document view (the scroll container) changes — e.g. when a sidebar toggles
21
+ * or the window is resized. This avoids the visible "stuck near bottom" gap
22
+ * that otherwise appears because the previous `scrollTop` no longer reaches
23
+ * the new content height.
24
+ * @default true
25
+ */
26
+ scrollOnResize?: boolean;
27
+ };
28
+
29
+ /**
30
+ * Extension that supports pinning the scroll position and automatically scrolls to the bottom when content is added.
31
+ */
32
+ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
33
+ let buttonContainer: HTMLDivElement | undefined;
34
+ let isPinned = true;
35
+ let jumpPending = false;
36
+ let enabled = true;
37
+ let firstUpdate = true;
38
+
39
+ const setPinned = (pinned: boolean) => {
40
+ buttonContainer?.classList.toggle('opacity-0', pinned);
41
+ isPinned = pinned;
42
+ };
43
+
44
+ return [
45
+ // Update listener for scrolling when content changes.
46
+ EditorView.updateListener.of((update) => {
47
+ const { view, heightChanged, state, startState } = update;
48
+
49
+ // Handle enable/disable effect.
50
+ for (const tr of update.transactions) {
51
+ for (const effect of tr.effects) {
52
+ if (effect.is(autoScrollEffect)) {
53
+ enabled = effect.value;
54
+ if (enabled) {
55
+ setPinned(true);
56
+ view.dispatch({
57
+ effects: crawlerActiveEffect.of(true),
58
+ });
59
+ } else {
60
+ view.dispatch({
61
+ effects: crawlerActiveEffect.of(false),
62
+ });
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ if (!enabled) {
69
+ return;
70
+ }
71
+
72
+ // Jump to bottom instantly when content first appears (either inserted into
73
+ // an empty doc, or present as initialValue when the editor is created).
74
+ if (isPinned && (firstUpdate || startState.doc.length === 0) && state.doc.length > 0) {
75
+ firstUpdate = false;
76
+ jumpPending = true;
77
+ requestAnimationFrame(() => {
78
+ view.scrollDOM.scrollTop = view.scrollDOM.scrollHeight;
79
+ jumpPending = false;
80
+ });
81
+ return;
82
+ }
83
+ firstUpdate = false;
84
+
85
+ // Suppress crawl while the initial jump is pending.
86
+ if (jumpPending) {
87
+ return;
88
+ }
89
+
90
+ // Maybe scroll if doc changed and pinned.
91
+ // NOTE: Geometry changed is triggered when widgets change height (e.g., toggle tool block).
92
+ if (heightChanged) {
93
+ if (isPinned) {
94
+ // NOTE: Use scroll geometry instead of coordsAtPos to avoid forced layout/scroll side-effects.
95
+ const { scrollTop, scrollHeight, clientHeight } = view.scrollDOM;
96
+ const delta = scrollHeight - scrollTop - clientHeight;
97
+ if (delta > 0) {
98
+ setPinned(true);
99
+ view.dispatch({ effects: crawlerActiveEffect.of(true) });
100
+ } else if (delta < -1) {
101
+ setPinned(false);
102
+ }
103
+ } else {
104
+ // TODO(burdon): Should re-pin if content shrinks.
105
+ if (state.doc.length === 0) {
106
+ setPinned(true);
107
+ }
108
+ }
109
+ }
110
+ }),
111
+
112
+ // Re-pin and jump to bottom when the scroll container itself resizes (e.g. sidebar toggle,
113
+ // window resize). Doc-driven height changes are handled by the updateListener above; this
114
+ // observer covers the case where the viewport changes while the doc length is unchanged.
115
+ scrollOnResize
116
+ ? ViewPlugin.fromClass(
117
+ class {
118
+ private readonly observer: ResizeObserver;
119
+ private firstObservation = true;
120
+ private destroyed = false;
121
+ constructor(view: EditorView) {
122
+ // Throttle so a continuous drag-resize (or a flurry of layout changes) coalesces
123
+ // into a single re-pin per ~50ms instead of dispatching every frame.
124
+ const onResize = throttle(() => {
125
+ if (this.destroyed || !enabled) {
126
+ return;
127
+ }
128
+
129
+ setPinned(true);
130
+ requestAnimationFrame(() => {
131
+ if (this.destroyed) {
132
+ return;
133
+ }
134
+
135
+ view.scrollDOM.scrollTo({ top: view.scrollDOM.scrollHeight, behavior: 'instant' });
136
+ view.dispatch({ effects: crawlerActiveEffect.of(false) });
137
+ });
138
+ }, 50);
139
+
140
+ this.observer = new ResizeObserver(() => {
141
+ // Skip the initial fire that ResizeObserver emits on `observe()`.
142
+ if (this.firstObservation) {
143
+ this.firstObservation = false;
144
+ return;
145
+ }
146
+ onResize();
147
+ });
148
+
149
+ this.observer.observe(view.scrollDOM);
150
+ }
151
+ destroy() {
152
+ this.destroyed = true;
153
+ this.observer.disconnect();
154
+ }
155
+ },
156
+ )
157
+ : [],
158
+
159
+ // Detect user scroll and unpin (or re-pin if scrolled to the bottom).
160
+ ViewPlugin.fromClass(
161
+ class {
162
+ private readonly cleanup: () => void;
163
+ constructor(view: EditorView) {
164
+ // Re-pin check is throttled so the listener doesn't thrash while scrolling, but
165
+ // unpinning must be immediate — otherwise content arriving during the throttle
166
+ // window re-applies the crawl effect and yanks the viewport back to the bottom.
167
+ const onUserScroll = throttle(() => {
168
+ requestAnimationFrame(() => {
169
+ const { scrollTop, scrollHeight, clientHeight } = view.scrollDOM;
170
+ const delta = scrollHeight - scrollTop - clientHeight;
171
+ // Sub-pixel tolerance: fractional scroll positions can leave delta at e.g. 0.5
172
+ // even when the user is visually at the bottom.
173
+ const pinned = Math.abs(delta) <= 1;
174
+ setPinned(pinned);
175
+ if (!pinned) {
176
+ view.dispatch({ effects: crawlerActiveEffect.of(false) });
177
+ }
178
+ });
179
+ }, 500);
180
+
181
+ this.cleanup = createUserScrollDetector(view.scrollDOM, () => {
182
+ if (isPinned) {
183
+ setPinned(false);
184
+ view.dispatch({ effects: crawlerActiveEffect.of(false) });
185
+ }
186
+ onUserScroll();
187
+ });
188
+ }
189
+ destroy() {
190
+ this.cleanup();
191
+ }
192
+ },
193
+ ),
194
+
195
+ // Scroll button.
196
+ ViewPlugin.fromClass(
197
+ class {
198
+ constructor(view: EditorView) {
199
+ const icon = Domino.of('dx-icon' as any)
200
+ .classNames(getSize(4))
201
+ .attributes({ icon: 'ph--arrow-down--regular' });
202
+ const button = Domino.of('button')
203
+ .classNames('dx-button bg-accent-surface')
204
+ .attributes({ 'data-density': 'fine' })
205
+ .append(icon)
206
+ .on('click', () => {
207
+ setPinned(true);
208
+ view.dispatch({
209
+ effects: crawlerLineEffect.of({ line: -1, position: 'end', behavior: 'smooth' }),
210
+ });
211
+ });
212
+
213
+ buttonContainer = Domino.of('div')
214
+ .classNames('cm-scroll-button transition-opacity duration-300 opacity-0')
215
+ .append(button).root as HTMLDivElement;
216
+
217
+ view.scrollDOM.parentElement!.appendChild(buttonContainer);
218
+ }
219
+ },
220
+ ),
221
+ ];
222
+ };
223
+
224
+ /**
225
+ * Attaches listeners to detect genuine user-initiated scrolling on an element.
226
+ * Two sources are covered:
227
+ * - `wheel`: fires only from physical mouse wheel / trackpad gestures.
228
+ * - `pointerdown` on the scrollbar gutter: detected by comparing clientX to
229
+ * the element's clientWidth (the content area, excluding the scrollbar).
230
+ * Returns a cleanup function that removes the listeners.
231
+ */
232
+ // TODO(burdon): Still jumps when widgets are rendered.
233
+ // - Track position of specific element/line in document and scroll relative to that.
234
+ function createUserScrollDetector(element: HTMLElement, onUserScroll: () => void): () => void {
235
+ return combine(
236
+ addEventListener(element, 'wheel', () => onUserScroll(), { passive: true }),
237
+ addEventListener(element, 'pointerdown', (event) => {
238
+ // If the pointer lands beyond the content width it hit the scrollbar gutter.
239
+ if (event.clientX > element.getBoundingClientRect().right - (element.offsetWidth - element.clientWidth)) {
240
+ onUserScroll();
241
+ }
242
+ }),
243
+ );
244
+ }