@dxos/react-ui-editor 0.6.13 → 0.6.14-main.2b6a0f3

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 (128) hide show
  1. package/dist/lib/browser/chunk-CIQSMP7K.mjs +148 -0
  2. package/dist/lib/browser/chunk-CIQSMP7K.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +248 -355
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/state/index.mjs +17 -0
  7. package/dist/lib/browser/state/index.mjs.map +7 -0
  8. package/dist/lib/node/chunk-GZWIENFM.cjs +169 -0
  9. package/dist/lib/node/chunk-GZWIENFM.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +5491 -0
  11. package/dist/lib/node/index.cjs.map +7 -0
  12. package/dist/lib/node/meta.json +1 -0
  13. package/dist/lib/node/state/index.cjs +39 -0
  14. package/dist/lib/node/state/index.cjs.map +7 -0
  15. package/dist/lib/node-esm/chunk-GP5RCZ3X.mjs +150 -0
  16. package/dist/lib/node-esm/chunk-GP5RCZ3X.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +5482 -0
  18. package/dist/lib/node-esm/index.mjs.map +7 -0
  19. package/dist/lib/node-esm/meta.json +1 -0
  20. package/dist/lib/node-esm/state/index.mjs +18 -0
  21. package/dist/lib/node-esm/state/index.mjs.map +7 -0
  22. package/dist/types/src/InputMode.stories.d.ts +11 -11
  23. package/dist/types/src/InputMode.stories.d.ts.map +1 -1
  24. package/dist/types/src/TextEditor.stories.d.ts +4 -1
  25. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  27. package/dist/types/src/defaults.d.ts.map +1 -1
  28. package/dist/types/src/extensions/autocomplete.d.ts +2 -1
  29. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  30. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -1
  31. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  32. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  33. package/dist/types/src/extensions/awareness/awareness.d.ts +2 -2
  34. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  35. package/dist/types/src/extensions/command/state.d.ts +2 -2
  36. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  37. package/dist/types/src/extensions/comments.d.ts +1 -1
  38. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  39. package/dist/types/src/extensions/debug.d.ts +2 -2
  40. package/dist/types/src/extensions/debug.d.ts.map +1 -1
  41. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  42. package/dist/types/src/extensions/index.d.ts +0 -4
  43. package/dist/types/src/extensions/index.d.ts.map +1 -1
  44. package/dist/types/src/extensions/listener.d.ts +1 -0
  45. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  46. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -1
  48. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  49. package/dist/types/src/extensions/markdown/link.d.ts +1 -1
  50. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  51. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  52. package/dist/types/src/extensions/modes.d.ts +3 -3
  53. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  54. package/dist/types/src/extensions/util/overlap.d.ts +1 -1
  55. package/dist/types/src/extensions/util/overlap.d.ts.map +1 -1
  56. package/dist/types/src/extensions/util/react.d.ts +1 -1
  57. package/dist/types/src/extensions/util/react.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useTextEditor.d.ts +1 -1
  59. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  60. package/dist/types/src/index.d.ts +1 -0
  61. package/dist/types/src/index.d.ts.map +1 -1
  62. package/dist/types/src/{extensions → state}/cursor.d.ts +2 -2
  63. package/dist/types/src/state/cursor.d.ts.map +1 -0
  64. package/dist/types/src/state/doc.d.ts +5 -0
  65. package/dist/types/src/state/doc.d.ts.map +1 -0
  66. package/dist/types/src/state/index.d.ts +6 -0
  67. package/dist/types/src/state/index.d.ts.map +1 -0
  68. package/dist/types/src/{extensions → state}/state.d.ts +2 -2
  69. package/dist/types/src/state/state.d.ts.map +1 -0
  70. package/dist/types/src/state/types.d.ts.map +1 -0
  71. package/dist/types/src/state/util.d.ts +3 -0
  72. package/dist/types/src/state/util.d.ts.map +1 -0
  73. package/dist/types/src/styles/markdown.d.ts +1 -2
  74. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  75. package/dist/types/src/styles/theme.d.ts.map +1 -1
  76. package/dist/types/src/util.d.ts +6 -0
  77. package/dist/types/src/util.d.ts.map +1 -1
  78. package/package.json +61 -41
  79. package/src/InputMode.stories.tsx +8 -8
  80. package/src/TextEditor.stories.tsx +93 -70
  81. package/src/components/Toolbar/Toolbar.tsx +8 -11
  82. package/src/defaults.ts +0 -2
  83. package/src/extensions/annotations.ts +1 -1
  84. package/src/extensions/autocomplete.ts +9 -8
  85. package/src/extensions/automerge/automerge.stories.tsx +2 -2
  86. package/src/extensions/automerge/{automerge.spec.tsx → automerge.test.tsx} +1 -0
  87. package/src/extensions/automerge/automerge.ts +2 -2
  88. package/src/extensions/automerge/cursor.ts +1 -1
  89. package/src/extensions/awareness/awareness.ts +3 -5
  90. package/src/extensions/command/hint.ts +1 -1
  91. package/src/extensions/command/state.ts +3 -4
  92. package/src/extensions/comments.ts +44 -45
  93. package/src/extensions/debug.ts +2 -2
  94. package/src/extensions/folding.tsx +6 -4
  95. package/src/extensions/index.ts +0 -4
  96. package/src/extensions/listener.ts +1 -0
  97. package/src/extensions/markdown/changes.test.ts +1 -3
  98. package/src/extensions/markdown/decorate.ts +49 -6
  99. package/src/extensions/markdown/formatting.test.ts +1 -3
  100. package/src/extensions/markdown/highlight.ts +0 -5
  101. package/src/extensions/markdown/link.ts +3 -2
  102. package/src/extensions/markdown/parser.test.ts +1 -2
  103. package/src/extensions/markdown/styles.ts +10 -0
  104. package/src/extensions/markdown/table.ts +3 -3
  105. package/src/extensions/modes.ts +6 -5
  106. package/src/extensions/util/overlap.ts +1 -1
  107. package/src/extensions/util/react.tsx +6 -1
  108. package/src/hooks/useTextEditor.ts +36 -32
  109. package/src/index.ts +1 -0
  110. package/src/{extensions → state}/cursor.ts +3 -6
  111. package/src/state/doc.ts +10 -0
  112. package/src/state/index.ts +11 -0
  113. package/src/{extensions → state}/state.ts +7 -3
  114. package/src/state/util.ts +13 -0
  115. package/src/styles/markdown.ts +1 -3
  116. package/src/styles/theme.ts +3 -1
  117. package/src/util.ts +10 -0
  118. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +0 -2
  119. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +0 -1
  120. package/dist/types/src/extensions/cursor.d.ts.map +0 -1
  121. package/dist/types/src/extensions/doc.d.ts +0 -6
  122. package/dist/types/src/extensions/doc.d.ts.map +0 -1
  123. package/dist/types/src/extensions/state.d.ts.map +0 -1
  124. package/dist/types/src/extensions/types.d.ts.map +0 -1
  125. package/src/extensions/automerge/automerge.test.ts +0 -13
  126. package/src/extensions/doc.ts +0 -17
  127. /package/dist/types/src/{extensions → state}/types.d.ts +0 -0
  128. /package/src/{extensions → state}/types.ts +0 -0
@@ -5,7 +5,6 @@
5
5
  import { invertedEffects } from '@codemirror/commands';
6
6
  import {
7
7
  type Extension,
8
- Facet,
9
8
  StateEffect,
10
9
  StateField,
11
10
  type Text,
@@ -29,18 +28,15 @@ import { debounce, type UnsubscribeCallback } from '@dxos/async';
29
28
  import { log } from '@dxos/log';
30
29
  import { nonNullable } from '@dxos/util';
31
30
 
32
- import { Cursor } from './cursor';
33
- import { type Comment, type Range } from './types';
34
31
  import { overlap } from './util';
32
+ import { Cursor, documentId, singleValueFacet } from '../state';
33
+ import { type Comment, type Range } from '../state';
35
34
  import { callbackWrapper } from '../util';
36
35
 
37
36
  //
38
37
  // State management.
39
38
  //
40
39
 
41
- // TODO(wittjosiah): Factor out, not comments-specific.
42
- const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
43
-
44
40
  type CommentState = {
45
41
  comment: Comment;
46
42
  range: Range;
@@ -369,7 +365,7 @@ export type CommentsOptions = {
369
365
  onHover?: (el: Element, shortcut: string) => void;
370
366
  };
371
367
 
372
- const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({ combine: (providers) => providers[0] });
368
+ const optionsFacet = singleValueFacet<CommentsOptions>();
373
369
 
374
370
  /**
375
371
  * Comment threads.
@@ -389,7 +385,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
389
385
 
390
386
  return [
391
387
  optionsFacet.of(options),
392
- documentId.of(options.id),
388
+ options.id ? documentId.of(options.id) : undefined,
393
389
  commentsState,
394
390
  commentsDecorations,
395
391
  handleCommentClick,
@@ -398,45 +394,43 @@ export const comments = (options: CommentsOptions = {}): Extension => {
398
394
  //
399
395
  // Keymap.
400
396
  //
401
- options.onCreate
402
- ? keymap.of([
403
- {
404
- key: shortcut,
405
- run: callbackWrapper(createComment),
406
- },
407
- ])
408
- : [],
397
+ options.onCreate &&
398
+ keymap.of([
399
+ {
400
+ key: shortcut,
401
+ run: callbackWrapper(createComment),
402
+ },
403
+ ]),
409
404
 
410
405
  //
411
406
  // Hover tooltip (for key shortcut hints, etc.)
412
407
  // TODO(burdon): Factor out to generic hints extension for current selection/line.
413
408
  //
414
- options.onHover
415
- ? hoverTooltip(
416
- (view, pos) => {
417
- const selection = view.state.selection.main;
418
- if (selection && pos >= selection.from && pos <= selection.to) {
419
- return {
420
- pos: selection.from,
421
- end: selection.to,
422
- above: true,
423
- create: () => {
424
- const el = document.createElement('div');
425
- options.onHover!(el, shortcut);
426
- return { dom: el, offset: { x: 0, y: 8 } };
427
- },
428
- };
429
- }
409
+ options.onHover &&
410
+ hoverTooltip(
411
+ (view, pos) => {
412
+ const selection = view.state.selection.main;
413
+ if (selection && pos >= selection.from && pos <= selection.to) {
414
+ return {
415
+ pos: selection.from,
416
+ end: selection.to,
417
+ above: true,
418
+ create: () => {
419
+ const el = document.createElement('div');
420
+ options.onHover!(el, shortcut);
421
+ return { dom: el, offset: { x: 0, y: 8 } };
422
+ },
423
+ };
424
+ }
430
425
 
431
- return null;
432
- },
433
- {
434
- // TODO(burdon): Hide on change triggered immediately?
435
- // hideOnChange: true,
436
- hoverTime: 1_000,
437
- },
438
- )
439
- : [],
426
+ return null;
427
+ },
428
+ {
429
+ // TODO(burdon): Hide on change triggered immediately?
430
+ // hideOnChange: true,
431
+ hoverTime: 1_000,
432
+ },
433
+ ),
440
434
 
441
435
  //
442
436
  // Track deleted ranges and update ranges for decorations.
@@ -511,8 +505,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
511
505
  }
512
506
  }),
513
507
 
514
- options.onUpdate ? trackPastedComments(options.onUpdate) : [],
515
- ];
508
+ options.onUpdate && trackPastedComments(options.onUpdate),
509
+ ].filter(nonNullable);
516
510
  };
517
511
 
518
512
  //
@@ -553,9 +547,13 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
553
547
  * Query the editor state for the active formatting at the selection.
554
548
  */
555
549
  export const selectionOverlapsComment = (state: EditorState): boolean => {
556
- const { selection } = state;
557
- const commentState = state.field(commentsState);
550
+ // May not be defined if thread plugin not installed.
551
+ const commentState = state.field(commentsState, false);
552
+ if (commentState === undefined) {
553
+ return false;
554
+ }
558
555
 
556
+ const { selection } = state;
559
557
  for (const range of selection.ranges) {
560
558
  if (commentState.comments.some(({ range: commentRange }) => overlap(commentRange, range))) {
561
559
  return true;
@@ -597,6 +595,7 @@ class ExternalCommentSync implements PluginValue {
597
595
  };
598
596
  }
599
597
 
598
+ // TODO(burdon): Needs comment.
600
599
  export const createExternalCommentSync = (
601
600
  id: string,
602
601
  subscribe: (sink: () => void) => UnsubscribeCallback,
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
6
+ import { type EditorState, type Extension, type RangeSet, StateField, type Transaction } from '@codemirror/state';
7
7
 
8
8
  // eslint-disable-next-line no-console
9
- export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
9
+ export const debugNodeLogger = (log: (...args: any[]) => void = console.log): Extension => {
10
10
  const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
11
11
  return StateField.define<any>({
12
12
  create: (state) => logTokens(state),
@@ -8,7 +8,6 @@ import { EditorView } from '@codemirror/view';
8
8
  import React from 'react';
9
9
 
10
10
  import { Icon } from '@dxos/react-ui';
11
- import { getSize } from '@dxos/react-ui-theme';
12
11
 
13
12
  import { createElement, renderRoot } from './util';
14
13
 
@@ -17,7 +16,7 @@ export type FoldingOptions = {};
17
16
  /**
18
17
  * https://codemirror.net/examples/gutter
19
18
  */
20
- // TODO(burdon): Remember folding state.
19
+ // TODO(burdon): Remember folding state (to state).
21
20
  export const folding = (_props: FoldingOptions = {}): Extension => [
22
21
  codeFolding({
23
22
  placeholderDOM: () => {
@@ -26,9 +25,11 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
26
25
  }),
27
26
  foldGutter({
28
27
  markerDOM: (open) => {
28
+ // TODO(burdon): Use sprite directly.
29
+ const el = createElement('div', { className: 'flex h-full items-center' });
29
30
  return renderRoot(
30
- createElement('div', { className: 'flex h-full items-center' }),
31
- <Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'mx-3 cursor-pointer', open && 'rotate-90']} />,
31
+ el,
32
+ <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
32
33
  );
33
34
  },
34
35
  }),
@@ -36,6 +37,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
36
37
  '.cm-foldGutter': {
37
38
  opacity: 0.3,
38
39
  transition: 'opacity 0.3s',
40
+ width: '32px',
39
41
  },
40
42
  '.cm-foldGutter:hover': {
41
43
  opacity: 1,
@@ -9,9 +9,7 @@ export * from './awareness';
9
9
  export * from './blast';
10
10
  export * from './command';
11
11
  export * from './comments';
12
- export * from './cursor';
13
12
  export * from './debug';
14
- export * from './doc';
15
13
  export * from './dnd';
16
14
  export * from './factories';
17
15
  export * from './folding';
@@ -19,6 +17,4 @@ export * from './listener';
19
17
  export * from './markdown';
20
18
  export * from './mention';
21
19
  export * from './modes';
22
- export * from './state';
23
- export * from './types';
24
20
  export * from './typewriter';
@@ -12,6 +12,7 @@ export type ListenerOptions = {
12
12
 
13
13
  /**
14
14
  * Event listener.
15
+ * @deprecated Use EditorView.updateListener and listen for specific update events.
15
16
  */
16
17
  export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
17
18
  const extensions: Extension[] = [];
@@ -2,9 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
6
-
7
- import { describe, test } from '@dxos/test';
5
+ import { describe, expect, test } from 'vitest';
8
6
 
9
7
  import { createLinkLabel } from './changes';
10
8
 
@@ -45,7 +45,7 @@ class HorizontalRuleWidget extends WidgetType {
45
45
  class LinkButton extends WidgetType {
46
46
  constructor(
47
47
  private readonly url: string,
48
- private readonly render: (el: Element, url: string) => void,
48
+ private readonly render: (el: HTMLElement, url: string) => void,
49
49
  ) {
50
50
  super();
51
51
  }
@@ -54,6 +54,7 @@ class LinkButton extends WidgetType {
54
54
  return this.url === other.url;
55
55
  }
56
56
 
57
+ // TODO(burdon): Create icon and link directly without react?
57
58
  override toDOM(view: EditorView) {
58
59
  const el = document.createElement('span');
59
60
  this.render(el, this.url);
@@ -127,6 +128,7 @@ class TextWidget extends WidgetType {
127
128
  }
128
129
 
129
130
  const hide = Decoration.replace({});
131
+ const blockQuote = Decoration.line({ class: mx('cm-blockquote') });
130
132
  const fencedCodeLine = Decoration.line({ class: mx('cm-code cm-codeblock-line') });
131
133
  const fencedCodeLineFirst = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-first') });
132
134
  const fencedCodeLineLast = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-last') });
@@ -199,7 +201,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
199
201
  return listLevels[listLevels.length - 1];
200
202
  };
201
203
 
202
- // const count = 0;
204
+ // let count = 0;
203
205
  const enterNode = (node: SyntaxNodeRef) => {
204
206
  // console.log(`[${count++}]`, { node: node.name, from: node.from, to: node.to });
205
207
  switch (node.name) {
@@ -329,7 +331,36 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
329
331
  break;
330
332
  }
331
333
 
334
+ //
335
+ // Blockquote > QuoteMark > Paragraph
336
+ //
337
+
338
+ case 'Blockquote': {
339
+ const editing = editingRange(state, node, focus);
340
+ const quoteMark = node.node.getChild('QuoteMark');
341
+ const paragraph = node.node.getChild('Paragraph');
342
+ if (!editing && quoteMark && paragraph) {
343
+ atomicDeco.add(quoteMark.from, paragraph.from, hide);
344
+ }
345
+
346
+ for (const block of view.viewportLineBlocks) {
347
+ if (block.to < node.from) {
348
+ continue;
349
+ }
350
+ if (block.from > node.to) {
351
+ break;
352
+ }
353
+
354
+ deco.add(block.from, block.from, blockQuote);
355
+ }
356
+
357
+ break;
358
+ }
359
+
360
+ //
332
361
  // CommentBlock
362
+ //
363
+
333
364
  case 'CommentBlock': {
334
365
  const editing = editingRange(state, node, focus);
335
366
  for (const block of view.viewportLineBlocks) {
@@ -339,21 +370,27 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
339
370
  if (block.from > node.to) {
340
371
  break;
341
372
  }
342
- const first = block.from <= node.from;
343
- const last = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
373
+
374
+ const isFirst = block.from <= node.from;
375
+ const isLast = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
376
+
344
377
  deco.add(
345
378
  block.from,
346
379
  block.from,
347
- first ? commentBlockLineFirst : last ? commentBlockLineLast : commentBlockLine,
380
+ isFirst ? commentBlockLineFirst : isLast ? commentBlockLineLast : commentBlockLine,
348
381
  );
349
- if (!editing && (first || last)) {
382
+
383
+ if (!editing && (isFirst || isLast)) {
350
384
  atomicDeco.add(block.from, block.to, hide);
351
385
  }
352
386
  }
353
387
  break;
354
388
  }
355
389
 
390
+ //
356
391
  // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
392
+ //
393
+
357
394
  case 'FencedCode': {
358
395
  for (const block of view.viewportLineBlocks) {
359
396
  if (block.to < node.from) {
@@ -375,7 +412,10 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
375
412
  return false;
376
413
  }
377
414
 
415
+ //
378
416
  // Link > [LinkMark, URL]
417
+ //
418
+
379
419
  case 'Link': {
380
420
  const marks = node.node.getChildren('LinkMark');
381
421
  const urlNode = node.node.getChild('URL');
@@ -412,7 +452,10 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
412
452
  break;
413
453
  }
414
454
 
455
+ //
415
456
  // HR
457
+ //
458
+
416
459
  case 'HorizontalRule': {
417
460
  if (!editingRange(state, node, focus)) {
418
461
  deco.add(node.from, node.to, horizontalRule);
@@ -4,9 +4,7 @@
4
4
 
5
5
  import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { EditorState, type StateCommand } from '@codemirror/state';
7
- import { expect } from 'chai';
8
-
9
- import { describe, test } from '@dxos/test';
7
+ import { describe, expect, test } from 'vitest';
10
8
 
11
9
  import {
12
10
  addBlockquote,
@@ -168,11 +168,6 @@ export const markdownHighlightStyle = (_options: HighlightOptions = {}) => {
168
168
  class: theme.code,
169
169
  },
170
170
 
171
- {
172
- tag: [markdownTags.QuoteMark],
173
- class: theme.blockquote,
174
- },
175
-
176
171
  {
177
172
  tag: [markdownTags.TableCell],
178
173
  class: 'font-mono',
@@ -9,8 +9,8 @@ import { type SyntaxNode } from '@lezer/common';
9
9
 
10
10
  import { tooltipContent } from '@dxos/react-ui-theme';
11
11
 
12
- export const linkTooltip = (render: (el: Element, url: string) => void) =>
13
- hoverTooltip((view, pos, side) => {
12
+ export const linkTooltip = (render: (el: HTMLElement, url: string) => void) => {
13
+ return hoverTooltip((view, pos, side) => {
14
14
  const syntax = syntaxTree(view.state).resolveInner(pos, side);
15
15
  let link = null;
16
16
  for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
@@ -35,3 +35,4 @@ export const linkTooltip = (render: (el: Element, url: string) => void) =>
35
35
  },
36
36
  };
37
37
  });
38
+ };
@@ -5,8 +5,7 @@
5
5
  // @ts-ignore
6
6
  import { testTree } from '@lezer/generator/test';
7
7
  import { parser } from '@lezer/markdown';
8
-
9
- import { describe, test } from '@dxos/test';
8
+ import { describe, test } from 'vitest';
10
9
 
11
10
  describe('parser', () => {
12
11
  // test.only('list-mark', () => {
@@ -39,6 +39,16 @@ export const formattingStyles = EditorView.theme({
39
39
  width: `${orderedListIndentationWidth}px`,
40
40
  },
41
41
 
42
+ /**
43
+ * Blockquote.
44
+ */
45
+ '& .cm-blockquote': {
46
+ background: 'var(--dx-cmCodeblock)',
47
+ borderLeft: '2px solid var(--dx-cmSeparator)',
48
+ paddingLeft: '1rem',
49
+ margin: '0',
50
+ },
51
+
42
52
  /**
43
53
  * Code and codeblocks.
44
54
  */
@@ -25,9 +25,9 @@ export type TableOptions = {};
25
25
  * https://github.github.com/gfm/#tables-extension
26
26
  */
27
27
  export const table = (options: TableOptions = {}): Extension => {
28
- return StateField.define<RangeSet<any>>({
28
+ return StateField.define<RangeSet<Decoration>>({
29
29
  create: (state) => update(state, options),
30
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
30
+ update: (_: RangeSet<Decoration>, tr: Transaction) => update(tr.state, options),
31
31
  provide: (field) => EditorView.decorations.from(field),
32
32
  });
33
33
  };
@@ -40,7 +40,7 @@ type Table = {
40
40
  };
41
41
 
42
42
  const update = (state: EditorState, _options: TableOptions) => {
43
- const builder = new RangeSetBuilder();
43
+ const builder = new RangeSetBuilder<Decoration>();
44
44
  const cursor = state.selection.main.head;
45
45
 
46
46
  const tables: Table[] = [];
@@ -2,26 +2,27 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension, Facet } from '@codemirror/state';
5
+ import { type Extension } from '@codemirror/state';
6
6
  import { keymap } from '@codemirror/view';
7
7
  import { vim } from '@replit/codemirror-vim';
8
8
  import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
9
9
 
10
+ import { singleValueFacet } from '../state';
11
+
10
12
  export const focusEvent = 'focus.container';
11
13
 
12
14
  export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
13
15
  export type EditorViewMode = (typeof EditorViewModes)[number];
16
+
14
17
  export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
15
18
  export type EditorInputMode = (typeof EditorInputModes)[number];
16
19
 
17
20
  export type EditorInputConfig = {
18
- type: string;
21
+ type?: string;
19
22
  noTabster?: boolean;
20
23
  };
21
24
 
22
- export const editorInputMode = Facet.define<EditorInputConfig, EditorInputConfig>({
23
- combine: (modes) => modes[0] ?? {},
24
- });
25
+ export const editorInputMode = singleValueFacet<EditorInputConfig>({});
25
26
 
26
27
  export const InputModeExtensions: { [mode: string]: Extension } = {
27
28
  default: [],
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Range } from '../types';
5
+ import { type Range } from '../../state';
6
6
 
7
7
  /**
8
8
  * Determines if two ranges overlap.
@@ -8,6 +8,8 @@ import { createRoot } from 'react-dom/client';
8
8
  import { ThemeProvider } from '@dxos/react-ui';
9
9
  import { defaultTx } from '@dxos/react-ui-theme';
10
10
 
11
+ // TODO(burdon): Factor out.
12
+
11
13
  export type ElementOptions = {
12
14
  className?: string;
13
15
  };
@@ -20,10 +22,13 @@ export const createElement = (tag: string, options?: ElementOptions, children?:
20
22
  if (children) {
21
23
  el.append(...(Array.isArray(children) ? children : [children]));
22
24
  }
25
+
23
26
  return el;
24
27
  };
25
28
 
26
- export const renderRoot = (root: HTMLElement, node: ReactNode): HTMLElement => {
29
+ // TODO(burdon): Remove react rendering; use DOM directly.
30
+ // NOTE: CM seems to remove/detach/overwrite portals that are attached to the DOM it control.s
31
+ export const renderRoot = <T extends Element>(root: T, node: ReactNode): T => {
27
32
  createRoot(root).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
28
33
  return root;
29
34
  };
@@ -19,10 +19,12 @@ import {
19
19
  import { log } from '@dxos/log';
20
20
  import { getProviderValue, isNotFalsy, type MaybeProvider } from '@dxos/util';
21
21
 
22
- import { createEditorStateTransaction, documentId, editorInputMode, type EditorSelection } from '../extensions';
23
- import { logChanges } from '../util';
22
+ import { editorInputMode } from '../extensions';
23
+ import { type EditorSelection, documentId, createEditorStateTransaction } from '../state';
24
+ import { debugDispatcher } from '../util';
24
25
 
25
26
  export type UseTextEditor = {
27
+ // TODO(burdon): Rename.
26
28
  parentRef: RefObject<HTMLDivElement>;
27
29
  view?: EditorView;
28
30
  focusAttributes: ReturnType<typeof useFocusableGroup> & {
@@ -65,8 +67,6 @@ export const useTextEditor = (
65
67
 
66
68
  // NOTE: Increments by 2 in strict mode.
67
69
  const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
68
- // Callback once view is created.
69
- const onUpdate = useRef<() => void>();
70
70
  const [view, setView] = useState<EditorView>();
71
71
  const parentRef = useRef<HTMLDivElement>(null);
72
72
 
@@ -87,44 +87,45 @@ export const useTextEditor = (
87
87
  }
88
88
 
89
89
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
90
- // NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
91
90
  const state = EditorState.create({
92
91
  doc: initialValue,
93
- selection: initialSelection,
92
+ // selection: initialSelection,
94
93
  extensions: [
95
94
  id && documentId.of(id),
96
- // NOTE: Doesn't catch errors in keymap functions.
95
+ extensions,
96
+ // NOTE: This doesn't catch errors in keymap functions.
97
97
  EditorView.exceptionSink.of((err) => {
98
98
  log.catch(err);
99
99
  }),
100
- extensions,
101
- EditorView.updateListener.of(() => {
102
- setTimeout(() => {
103
- onUpdate.current?.();
104
- });
105
- }),
100
+ // TODO(burdon): Factor out debug inspector.
101
+ // ViewPlugin.fromClass(
102
+ // class {
103
+ // constructor(_view: EditorView) {
104
+ // log('construct', { id });
105
+ // }
106
+ //
107
+ // destroy() {
108
+ // log('destroy', { id });
109
+ // }
110
+ // },
111
+ // ),
106
112
  ].filter(isNotFalsy),
107
113
  });
108
114
 
109
115
  // https://codemirror.net/docs/ref/#view.EditorViewConfig
110
116
  view = new EditorView({
111
117
  parent: parentRef.current,
112
- selection: initialSelection,
113
118
  state,
114
- // NOTE: Uncomment to debug/monitor all transactions.
115
- // https://codemirror.net/docs/ref/#view.EditorView.dispatch
116
- dispatchTransactions: (trs, view) => {
117
- if (debug) {
118
- logChanges(trs);
119
- }
120
- view.update(trs);
121
- },
119
+ scrollTo: scrollTo ? EditorView.scrollIntoView(scrollTo, { yMargin: 96 }) : undefined, // TODO(burdon): Const.
120
+ dispatchTransactions: debug ? debugDispatcher : undefined,
122
121
  });
123
122
 
124
- // Move to end of line after document loaded.
125
- if (!initialValue && moveToEndOfLine) {
123
+ // Move to end of line after document loaded (unless selection is specified).
124
+ if (moveToEndOfLine && !initialSelection) {
126
125
  const { to } = view.state.doc.lineAt(0);
127
- view.dispatch({ selection: { anchor: to } });
126
+ if (to) {
127
+ view.dispatch({ selection: { anchor: to } });
128
+ }
128
129
  }
129
130
 
130
131
  setView(view);
@@ -138,18 +139,21 @@ export const useTextEditor = (
138
139
 
139
140
  useEffect(() => {
140
141
  if (view) {
141
- // NOTE: Set selection after first update (since content may rerender on focus).
142
- onUpdate.current = () => {
143
- onUpdate.current = undefined;
144
- view.dispatch(createEditorStateTransaction({ scrollTo, selection }));
145
- };
146
-
147
142
  // Remove tabster attribute (rely on custom keymap).
148
143
  if (view.state.facet(editorInputMode).noTabster) {
149
144
  parentRef.current?.removeAttribute('data-tabster');
150
145
  }
146
+
147
+ if (scrollTo || selection) {
148
+ if (selection && selection.anchor > view.state.doc.length) {
149
+ log.warn('invalid selection', { length: view.state.doc.length, scrollTo, selection });
150
+ return;
151
+ }
152
+
153
+ view.dispatch(createEditorStateTransaction(view.state, { scrollTo, selection }));
154
+ }
151
155
  }
152
- }, [view, selection, scrollTo]);
156
+ }, [view, scrollTo, selection]);
153
157
 
154
158
  useEffect(() => {
155
159
  if (view && autoFocus) {
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from './components';
14
14
  export * from './defaults';
15
15
  export * from './extensions';
16
16
  export * from './hooks';
17
+ export * from './state';
17
18
  export * from './util';
18
19
 
19
20
  export { translations };