@dxos/ui-editor 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (76) hide show
  1. package/dist/lib/browser/index.mjs +711 -521
  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 +711 -521
  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/defaults.d.ts +3 -10
  8. package/dist/types/src/defaults.d.ts.map +1 -1
  9. package/dist/types/src/extensions/auto-scroll.d.ts +6 -0
  10. package/dist/types/src/extensions/auto-scroll.d.ts.map +1 -0
  11. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  12. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  13. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  14. package/dist/types/src/extensions/index.d.ts +3 -2
  15. package/dist/types/src/extensions/index.d.ts.map +1 -1
  16. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -0
  17. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  19. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  21. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  22. package/dist/types/src/extensions/preview/preview.d.ts +3 -1
  23. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  24. package/dist/types/src/extensions/scroll-past-end.d.ts +3 -0
  25. package/dist/types/src/extensions/scroll-past-end.d.ts.map +1 -0
  26. package/dist/types/src/extensions/scroller.d.ts +66 -0
  27. package/dist/types/src/extensions/scroller.d.ts.map +1 -0
  28. package/dist/types/src/extensions/tags/streamer.d.ts +1 -1
  29. package/dist/types/src/styles/index.d.ts +0 -2
  30. package/dist/types/src/styles/index.d.ts.map +1 -1
  31. package/dist/types/src/styles/theme.d.ts +15 -0
  32. package/dist/types/src/styles/theme.d.ts.map +1 -1
  33. package/dist/types/src/util/cursor.d.ts +1 -1
  34. package/dist/types/src/util/cursor.d.ts.map +1 -1
  35. package/dist/types/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +32 -32
  37. package/src/defaults.ts +19 -21
  38. package/src/extensions/annotations.ts +1 -1
  39. package/src/extensions/auto-scroll.ts +129 -0
  40. package/src/extensions/automerge/automerge.test.tsx +2 -2
  41. package/src/extensions/automerge/automerge.ts +6 -5
  42. package/src/extensions/blocks.ts +5 -5
  43. package/src/extensions/comments.ts +5 -5
  44. package/src/extensions/dnd.ts +2 -2
  45. package/src/extensions/factories.ts +7 -8
  46. package/src/extensions/folding.ts +3 -20
  47. package/src/extensions/index.ts +3 -2
  48. package/src/extensions/markdown/bundle.ts +23 -9
  49. package/src/extensions/markdown/decorate.ts +15 -11
  50. package/src/extensions/markdown/highlight.ts +15 -7
  51. package/src/extensions/markdown/link.ts +27 -33
  52. package/src/extensions/markdown/parser.test.ts +0 -1
  53. package/src/extensions/markdown/styles.ts +36 -9
  54. package/src/extensions/markdown/table.ts +24 -2
  55. package/src/extensions/outliner/outliner.ts +3 -3
  56. package/src/extensions/preview/preview.ts +62 -15
  57. package/src/extensions/scroll-past-end.ts +32 -0
  58. package/src/extensions/scroller.ts +233 -0
  59. package/src/extensions/selection.ts +1 -1
  60. package/src/extensions/tags/streamer.ts +2 -2
  61. package/src/extensions/tags/xml-tags.ts +7 -4
  62. package/src/styles/index.ts +0 -2
  63. package/src/styles/theme.ts +116 -34
  64. package/src/util/cursor.ts +1 -1
  65. package/dist/types/src/extensions/autoscroll.d.ts +0 -20
  66. package/dist/types/src/extensions/autoscroll.d.ts.map +0 -1
  67. package/dist/types/src/extensions/scrolling.d.ts +0 -78
  68. package/dist/types/src/extensions/scrolling.d.ts.map +0 -1
  69. package/dist/types/src/styles/markdown.d.ts +0 -8
  70. package/dist/types/src/styles/markdown.d.ts.map +0 -1
  71. package/dist/types/src/styles/tokens.d.ts +0 -3
  72. package/dist/types/src/styles/tokens.d.ts.map +0 -1
  73. package/src/extensions/autoscroll.ts +0 -165
  74. package/src/extensions/scrolling.ts +0 -189
  75. package/src/styles/markdown.ts +0 -26
  76. package/src/styles/tokens.ts +0 -17
@@ -8,7 +8,6 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
8
8
  import { type SyntaxNodeRef } from '@lezer/common';
9
9
 
10
10
  import { invariant } from '@dxos/invariant';
11
- import { mx } from '@dxos/ui-theme';
12
11
 
13
12
  import { type HeadingLevel, markdownTheme } from '../../styles';
14
13
  import { type RenderCallback } from '../../types';
@@ -56,7 +55,6 @@ class LinkButton extends WidgetType {
56
55
  return this.url === other.url;
57
56
  }
58
57
 
59
- // TODO(burdon): Create icon and link directly without react?
60
58
  override toDOM(view: EditorView) {
61
59
  const el = document.createElement('span');
62
60
  this.render(el, { url: this.url }, view);
@@ -132,8 +130,8 @@ class TextWidget extends WidgetType {
132
130
  const hide = Decoration.replace({});
133
131
  const blockQuote = Decoration.line({ class: 'cm-blockquote' });
134
132
  const fencedCodeLine = Decoration.line({ class: 'cm-code cm-codeblock-line' });
135
- const fencedCodeLineFirst = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-start') });
136
- const fencedCodeLineLast = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-end') });
133
+ const fencedCodeLineFirst = Decoration.line({ class: 'cm-code cm-codeblock-line cm-codeblock-start' });
134
+ const fencedCodeLineLast = Decoration.line({ class: 'cm-code cm-codeblock-line cm-codeblock-end' });
137
135
  const commentBlockLine = fencedCodeLine;
138
136
  const commentBlockLineFirst = fencedCodeLineFirst;
139
137
  const commentBlockLineLast = fencedCodeLineLast;
@@ -244,14 +242,14 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
244
242
  headers
245
243
  .slice(from - 1)
246
244
  .map((level) => level?.number ?? 0)
247
- .join('.') + ' ';
245
+ .join('.') + '). ';
248
246
 
249
247
  if (num.length) {
250
248
  atomicDecoRanges.push({
251
249
  from: mark.from,
252
250
  to: mark.from + len,
253
251
  deco: Decoration.replace({
254
- widget: new TextWidget(num, markdownTheme.heading(level)),
252
+ widget: new TextWidget(num, markdownTheme.heading(level).className),
255
253
  }),
256
254
  });
257
255
  }
@@ -440,11 +438,11 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
440
438
 
441
439
  decoRanges.push({
442
440
  from: marks[0].to,
443
- to: marks[1].from,
441
+ to: !editing && options.renderLinkButton ? node.to : marks[1].from,
444
442
  deco: Decoration.mark({
445
443
  tagName: 'a',
446
444
  attributes: {
447
- class: 'cm-link',
445
+ class: options.renderLinkButton ? 'cm-link cm-link-with-button' : 'cm-link',
448
446
  href: url,
449
447
  rel: 'noreferrer',
450
448
  target: '_blank',
@@ -457,7 +455,9 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
457
455
  from: marks[1].from,
458
456
  to: node.to,
459
457
  deco: options.renderLinkButton
460
- ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
458
+ ? Decoration.replace({
459
+ widget: new LinkButton(url, options.renderLinkButton),
460
+ })
461
461
  : hide,
462
462
  });
463
463
  }
@@ -527,8 +527,12 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
527
527
  }
528
528
 
529
529
  const atomicDeco = new RangeSetBuilder<Decoration>();
530
- for (const { from, to, deco: d } of atomicDecoRanges) {
531
- atomicDeco.add(from, to, d);
530
+ for (const { from, to, deco } of atomicDecoRanges) {
531
+ // Skip replace decorations that span line breaks (not allowed by CodeMirror plugins).
532
+ if (from < to && state.doc.lineAt(from).number !== state.doc.lineAt(to).number) {
533
+ continue;
534
+ }
535
+ atomicDeco.add(from, to, deco);
532
536
  }
533
537
 
534
538
  return {
@@ -145,13 +145,21 @@ export const markdownHighlightStyle = (_options: HighlightOptions = {}) => {
145
145
  class: 'font-mono',
146
146
  },
147
147
 
148
- // Headings.
149
- { tag: tags.heading1, class: markdownTheme.heading(1) },
150
- { tag: tags.heading2, class: markdownTheme.heading(2) },
151
- { tag: tags.heading3, class: markdownTheme.heading(3) },
152
- { tag: tags.heading4, class: markdownTheme.heading(4) },
153
- { tag: tags.heading5, class: markdownTheme.heading(5) },
154
- { tag: tags.heading6, class: markdownTheme.heading(6) },
148
+ // Headings — use CSS properties only (no class:) so CodeMirror generates scoped CSS via
149
+ // StyleModule that overrides vscodeDarkStyle's t.heading rule. When class: is present,
150
+ // HighlightStyle silently ignores all other CSS properties (they're mutually exclusive).
151
+ // Font sizes use Tailwind v4 CSS variables so nothing is hardcoded.
152
+ {
153
+ tag: tags.heading,
154
+ color: 'var(--color-cm-heading) !important',
155
+ fontWeight: '300',
156
+ },
157
+ { tag: tags.heading1, ...markdownTheme.heading(1) },
158
+ { tag: tags.heading2, ...markdownTheme.heading(2) },
159
+ { tag: tags.heading3, ...markdownTheme.heading(3) },
160
+ { tag: tags.heading4, ...markdownTheme.heading(4) },
161
+ { tag: tags.heading5, ...markdownTheme.heading(5) },
162
+ { tag: tags.heading6, ...markdownTheme.heading(6) },
155
163
 
156
164
  // Emphasis.
157
165
  { tag: tags.emphasis, class: 'italic' },
@@ -12,39 +12,33 @@ import { tooltipContent } from '@dxos/ui-theme';
12
12
  import { type RenderCallback } from '../../types';
13
13
 
14
14
  export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
15
- return hoverTooltip(
16
- (view, pos, side) => {
17
- const syntax = syntaxTree(view.state).resolveInner(pos, side);
18
- let link = null;
19
- for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
20
- link = node.name === 'Link' ? node : null;
21
- }
15
+ return hoverTooltip((view, pos, side) => {
16
+ const syntax = syntaxTree(view.state).resolveInner(pos, side);
17
+ let link = null;
18
+ for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
19
+ link = node.name === 'Link' ? node : null;
20
+ }
22
21
 
23
- const url = link && link.getChild('URL');
24
- if (!url || !link) {
25
- return null;
26
- }
22
+ const url = link && link.getChild('URL');
23
+ if (!url || !link) {
24
+ return null;
25
+ }
27
26
 
28
- const urlText = view.state.sliceDoc(url.from, url.to);
29
- if (urlText.startsWith('dxn')) {
30
- return null;
31
- }
32
- return {
33
- pos: link.from,
34
- end: link.to,
35
- // NOTE: Forcing above causes the tooltip to flicker.
36
- // above: true,
37
- create: () => {
38
- const el = document.createElement('div');
39
- el.className = tooltipContent({});
40
- renderTooltip(el, { url: urlText }, view);
41
- return { dom: el, offset: { x: 0, y: 4 } };
42
- },
43
- };
44
- },
45
- {
46
- // NOTE: 0 = default of 300ms.
47
- hoverTime: 1,
48
- },
49
- );
27
+ const urlText = view.state.sliceDoc(url.from, url.to);
28
+ if (urlText.startsWith('dxn')) {
29
+ return null;
30
+ }
31
+
32
+ return {
33
+ pos: link.from,
34
+ end: link.to,
35
+ above: true,
36
+ create: () => {
37
+ const el = document.createElement('div');
38
+ el.className = tooltipContent({});
39
+ renderTooltip(el, { url: urlText }, view);
40
+ return { dom: el, offset: { x: 0, y: 4 } };
41
+ },
42
+ };
43
+ });
50
44
  };
@@ -2,7 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- // @ts-ignore
6
5
  import { testTree } from '@lezer/generator/test';
7
6
  import { parser } from '@lezer/markdown';
8
7
  import { describe, test } from 'vitest';
@@ -18,7 +18,7 @@ export const formattingStyles = EditorView.theme({
18
18
  width: '100%',
19
19
  height: '0',
20
20
  verticalAlign: 'middle',
21
- borderTop: '1px solid var(--dx-cmSeparator)',
21
+ borderTop: '1px solid var(--color-cm-separator)',
22
22
  opacity: 0.5,
23
23
  },
24
24
 
@@ -43,20 +43,45 @@ export const formattingStyles = EditorView.theme({
43
43
  * Blockquote.
44
44
  */
45
45
  '& .cm-blockquote': {
46
- background: 'var(--dx-cmCodeblock)',
47
- borderLeft: '2px solid var(--dx-cmSeparator)',
46
+ background: 'var(--color-cm-codeblock)',
47
+ borderLeft: '2px solid var(--color-cm-separator)',
48
48
  paddingLeft: '1rem',
49
- margin: '0',
49
+ margin: 0,
50
50
  },
51
51
 
52
52
  /**
53
53
  * Code and codeblocks.
54
54
  */
55
+ '& code': {
56
+ fontFamily: fontMono,
57
+ whiteSpace: 'nowrap',
58
+ color: 'var(--color-cm-code)',
59
+ },
55
60
  '& .cm-code': {
56
61
  fontFamily: fontMono,
62
+ whiteSpace: 'nowrap',
63
+ color: 'var(--color-cm-code)',
64
+ },
65
+ // Inline code spans (triggered by backticks) use `cm-code-inline` + `font-mono`.
66
+ // Different monospace font metrics can slightly overflow the fixed CodeMirror line box,
67
+ // so constrain them to the target 24px height.
68
+ '& .cm-code-inline': {
69
+ fontFamily: fontMono,
70
+ height: '24px',
71
+ display: 'inline-flex',
72
+ alignItems: 'center',
73
+ overflow: 'hidden',
74
+ color: 'var(--color-cm-code)',
75
+ },
76
+ '& .cm-code-mark': {
77
+ fontFamily: fontMono,
78
+ height: '24px',
79
+ display: 'inline-flex',
80
+ alignItems: 'center',
81
+ overflow: 'hidden',
57
82
  },
58
83
  '& .cm-codeblock-line': {
59
- background: 'var(--dx-cmCodeblock)',
84
+ background: 'var(--color-cm-codeblock)',
60
85
  paddingInline: '1rem !important',
61
86
  },
62
87
  '& .cm-codeblock-start': {
@@ -87,16 +112,18 @@ export const formattingStyles = EditorView.theme({
87
112
  */
88
113
  '.cm-table *': {
89
114
  fontFamily: fontMono,
115
+ lineHeight: 1.5,
90
116
  textDecoration: 'none !important',
91
117
  },
92
118
  '.cm-table-head': {
93
119
  padding: '2px 16px 2px 0px',
94
120
  textAlign: 'left',
95
- borderBottom: '1px solid var(--dx-cmSeparator)',
96
- color: 'var(--dx-subdued)',
121
+ borderBottom: '1px solid var(--color-cm-separator)',
122
+ color: 'var(--color-subdued)',
97
123
  },
98
124
  '.cm-table-cell': {
99
125
  padding: '2px 16px 2px 0px',
126
+ verticalAlign: 'top',
100
127
  },
101
128
 
102
129
  /**
@@ -113,12 +140,12 @@ export const formattingStyles = EditorView.theme({
113
140
  },
114
141
  '.cm-image-with-loader': {
115
142
  display: 'block',
116
- opacity: '0',
143
+ opacity: 0,
117
144
  transitionDuration: '350ms',
118
145
  transitionProperty: 'opacity',
119
146
  },
120
147
  '.cm-image-with-loader.cm-loaded-image': {
121
- opacity: '1',
148
+ opacity: 1,
122
149
  },
123
150
  '.cm-image-wrapper': {
124
151
  'grid-template-columns': '1fr',
@@ -107,6 +107,28 @@ const update = (state: EditorState, _options: TableOptions) => {
107
107
  return builder.finish();
108
108
  };
109
109
 
110
+ /** Renders cell text into el, processing inline markdown (bold, italic, code). */
111
+ const renderCellContent = (el: HTMLElement, text: string): void => {
112
+ const parts = text.split(/(`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_)/);
113
+ for (const part of parts) {
114
+ if (part.length > 2 && part.startsWith('`') && part.endsWith('`')) {
115
+ const code = document.createElement('code');
116
+ code.textContent = part.slice(1, -1);
117
+ el.appendChild(code);
118
+ } else if ((part.startsWith('**') && part.endsWith('**')) || (part.startsWith('__') && part.endsWith('__'))) {
119
+ const strong = document.createElement('strong');
120
+ strong.textContent = part.slice(2, -2);
121
+ el.appendChild(strong);
122
+ } else if ((part.startsWith('*') && part.endsWith('*')) || (part.startsWith('_') && part.endsWith('_'))) {
123
+ const em = document.createElement('em');
124
+ em.textContent = part.slice(1, -1);
125
+ el.appendChild(em);
126
+ } else {
127
+ el.appendChild(document.createTextNode(part));
128
+ }
129
+ }
130
+ };
131
+
110
132
  class TableWidget extends WidgetType {
111
133
  constructor(readonly _table: Table) {
112
134
  super();
@@ -132,7 +154,7 @@ class TableWidget extends WidgetType {
132
154
  this._table.header?.forEach((cell) => {
133
155
  const th = document.createElement('th');
134
156
  th.setAttribute('class', 'cm-table-head');
135
- tr.appendChild(th).textContent = cell;
157
+ renderCellContent(tr.appendChild(th), cell);
136
158
  });
137
159
 
138
160
  const body = table.appendChild(document.createElement('tbody'));
@@ -141,7 +163,7 @@ class TableWidget extends WidgetType {
141
163
  row.forEach((cell) => {
142
164
  const td = document.createElement('td');
143
165
  td.setAttribute('class', 'cm-table-cell');
144
- tr.appendChild(td).textContent = cell;
166
+ renderCellContent(tr.appendChild(td), cell);
145
167
  });
146
168
  });
147
169
 
@@ -61,7 +61,7 @@ export const outliner = (_options: OutlinerProps = {}): Extension => [
61
61
  decorateMarkdown({ listPaddingLeft: 8 }),
62
62
 
63
63
  // Researve space for menu.
64
- EditorView.contentAttributes.of({ class: 'is-full !mr-[3rem]' }),
64
+ EditorView.contentAttributes.of({ class: 'w-full !mr-[3rem]' }),
65
65
  ];
66
66
 
67
67
  /**
@@ -157,10 +157,10 @@ const decorations = () => [
157
157
  },
158
158
 
159
159
  '.cm-list-item-focused': {
160
- borderColor: 'var(--dx-neutralFocusIndicator)',
160
+ borderColor: 'var(--color-neutral-focus-indicator)',
161
161
  },
162
162
  '&:focus-within .cm-list-item-selected': {
163
- borderColor: 'var(--dx-separator)',
163
+ borderColor: 'var(--color-separator)',
164
164
  },
165
165
  }),
166
166
  ),
@@ -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,32 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { EditorView, ViewPlugin } from '@codemirror/view';
7
+
8
+ /**
9
+ * Custom scroll-past-end extension that accounts for the actual height of the last line.
10
+ * The built-in CodeMirror `scrollPastEnd` uses `defaultLineHeight` which doesn't account
11
+ * for taller elements like headings, block widgets, etc.
12
+ */
13
+ const scrollPastEndPlugin = ViewPlugin.fromClass(
14
+ class {
15
+ height = 1000;
16
+ attrs: { style: string } | null = { style: 'padding-bottom: 1000px' };
17
+
18
+ update({ view }: { view: EditorView }) {
19
+ const lastLineBlock = view.lineBlockAt(view.state.doc.length);
20
+ const height = view.dom.clientHeight - lastLineBlock.height - view.documentPadding.top - 0.5;
21
+ if (height >= 0 && height !== this.height) {
22
+ this.height = height;
23
+ this.attrs = { style: `padding-bottom: ${height}px` };
24
+ }
25
+ }
26
+ },
27
+ );
28
+
29
+ export const scrollPastEnd = (): Extension => [
30
+ scrollPastEndPlugin,
31
+ EditorView.contentAttributes.of((view) => view.plugin(scrollPastEndPlugin)?.attrs ?? null),
32
+ ];