edytor 4.0.0 → 5.0.0

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 (147) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +10 -3
  6. package/dist/cloudflare/index.d.ts +1 -1
  7. package/dist/cloudflare/room/access.d.ts +8 -3
  8. package/dist/cloudflare/room/access.js +44 -20
  9. package/dist/cloudflare/room/admission.js +28 -5
  10. package/dist/cloudflare/room/comments.js +28 -1
  11. package/dist/cloudflare/room/context.d.ts +115 -106
  12. package/dist/cloudflare/room/context.js +10 -3
  13. package/dist/cloudflare/room/forged.d.ts +14 -0
  14. package/dist/cloudflare/room/forged.js +30 -2
  15. package/dist/cloudflare/room/frames.d.ts +4 -0
  16. package/dist/cloudflare/room/frames.js +5 -1
  17. package/dist/cloudflare/room/purge.js +2 -1
  18. package/dist/cloudflare/room/replicas.d.ts +4 -2
  19. package/dist/cloudflare/room/replicas.js +8 -3
  20. package/dist/cloudflare/room/suggest.d.ts +7 -0
  21. package/dist/cloudflare/room/suggest.js +143 -0
  22. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  23. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  24. package/dist/components/Content.svelte +9 -1
  25. package/dist/crdt/anchors.js +3 -2
  26. package/dist/crdt/attribution/attribution.d.ts +3 -0
  27. package/dist/crdt/attribution/attribution.js +3 -3
  28. package/dist/crdt/doc/capability.js +9 -13
  29. package/dist/crdt/doc/funnel.d.ts +1 -1
  30. package/dist/crdt/doc/funnel.js +5 -2
  31. package/dist/crdt/doc/gate.d.ts +1 -1
  32. package/dist/crdt/doc/history.d.ts +6 -0
  33. package/dist/crdt/doc/history.js +77 -56
  34. package/dist/crdt/doc/layout.js +5 -5
  35. package/dist/crdt/doc/meta.js +6 -7
  36. package/dist/crdt/doc/reads.d.ts +2 -2
  37. package/dist/crdt/doc/reads.js +18 -28
  38. package/dist/crdt/doc/steps.js +4 -4
  39. package/dist/crdt/doc/suggest.d.ts +167 -0
  40. package/dist/crdt/doc/suggest.js +579 -0
  41. package/dist/crdt/document.d.ts +10 -1
  42. package/dist/crdt/document.js +19 -11
  43. package/dist/crdt/edytor-doc.d.ts +355 -333
  44. package/dist/crdt/edytor-doc.js +125 -82
  45. package/dist/crdt/engine-api.d.ts +8 -2
  46. package/dist/crdt/engine.d.ts +2 -1
  47. package/dist/crdt/engine.js +3 -1
  48. package/dist/crdt/flow.d.ts +1 -1
  49. package/dist/crdt/flow.js +4 -10
  50. package/dist/crdt/index.d.ts +111 -105
  51. package/dist/crdt/index.js +3 -0
  52. package/dist/crdt/operations.d.ts +12 -3
  53. package/dist/crdt/parts.d.ts +43 -0
  54. package/dist/crdt/parts.js +44 -0
  55. package/dist/crdt/placement/display.d.ts +1 -1
  56. package/dist/crdt/placement/display.js +3 -3
  57. package/dist/crdt/placement/keys.d.ts +5 -0
  58. package/dist/crdt/placement/keys.js +5 -0
  59. package/dist/crdt/placement/resolve.js +2 -1
  60. package/dist/crdt/protocol.d.ts +1 -1
  61. package/dist/crdt/protocol.js +1 -1
  62. package/dist/crdt/protocols/auth.d.ts +11 -1
  63. package/dist/crdt/protocols/auth.js +17 -1
  64. package/dist/crdt/protocols/comments.d.ts +16 -1
  65. package/dist/crdt/protocols/comments.js +9 -1
  66. package/dist/crdt/providers/index.d.ts +6 -4
  67. package/dist/crdt/providers/index.js +3 -1
  68. package/dist/crdt/providers/websocket.d.ts +6 -4
  69. package/dist/crdt/providers/websocket.js +9 -4
  70. package/dist/crdt/purge.d.ts +7 -1
  71. package/dist/crdt/purge.js +32 -2
  72. package/dist/crdt/restore.d.ts +2 -1
  73. package/dist/crdt/restore.js +9 -0
  74. package/dist/crdt/schema.d.ts +9 -1
  75. package/dist/crdt/schema.js +13 -3
  76. package/dist/crdt/structs.d.ts +2 -0
  77. package/dist/crdt/structs.js +2 -0
  78. package/dist/crdt/text/index/claims.js +1 -23
  79. package/dist/crdt/text/index/layout.d.ts +2 -4
  80. package/dist/crdt/text/index/layout.js +9 -22
  81. package/dist/crdt/text/index/placement.js +21 -24
  82. package/dist/crdt/text/index/shared.js +17 -14
  83. package/dist/crdt/text/model.d.ts +10 -0
  84. package/dist/crdt/text/model.js +8 -1
  85. package/dist/crdt/text/runs.d.ts +34 -30
  86. package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
  87. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  88. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
  89. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
  90. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  91. package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
  92. package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
  93. package/dist/edytor.svelte.d.ts +1 -1
  94. package/dist/edytor.svelte.js +100 -75
  95. package/dist/events/beforeInputCommands.d.ts +2 -2
  96. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  97. package/dist/events/nativeInteractiveControl.js +41 -1
  98. package/dist/events/onBeforeInput.js +3 -3
  99. package/dist/events/onFocus.js +9 -11
  100. package/dist/export/forms.d.ts +25 -25
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +1 -1
  103. package/dist/labels.d.ts +8 -0
  104. package/dist/labels.js +6 -1
  105. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  106. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  107. package/dist/plugins/find/search.js +2 -2
  108. package/dist/plugins/index.d.ts +1 -0
  109. package/dist/plugins/index.js +1 -0
  110. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  111. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  112. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  113. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  114. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  115. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  116. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  117. package/dist/selection/domSelection.d.ts +2 -0
  118. package/dist/selection/domSelection.js +6 -2
  119. package/dist/selection/pointer.svelte.js +75 -48
  120. package/dist/selection/replaceSelection.js +2 -6
  121. package/dist/selection/selection.svelte.d.ts +3 -3
  122. package/dist/selection/selection.svelte.js +39 -32
  123. package/dist/selection/selection.utils.d.ts +14 -0
  124. package/dist/selection/selection.utils.js +28 -3
  125. package/dist/session/commands.js +21 -11
  126. package/dist/session/composition.svelte.d.ts +5 -3
  127. package/dist/session/composition.svelte.js +21 -17
  128. package/dist/session/handles.d.ts +4 -17
  129. package/dist/session/handles.js +11 -37
  130. package/dist/session/navigation.js +6 -9
  131. package/dist/session/selection.d.ts +11 -5
  132. package/dist/session/selection.js +14 -48
  133. package/dist/session/suggestions.svelte.d.ts +38 -9
  134. package/dist/session/suggestions.svelte.js +37 -18
  135. package/dist/session/virtual.d.ts +1 -1
  136. package/dist/surface/attributes.d.ts +5 -0
  137. package/dist/surface/attributes.js +2 -2
  138. package/dist/surface/cells.d.ts +2 -26
  139. package/dist/surface/cells.js +2 -33
  140. package/dist/surface/observer.svelte.d.ts +6 -1
  141. package/dist/surface/observer.svelte.js +128 -81
  142. package/dist/surface/pin.svelte.d.ts +9 -9
  143. package/dist/surface/pin.svelte.js +15 -22
  144. package/dist/surface/projector.svelte.js +8 -14
  145. package/dist/text/text.svelte.js +14 -19
  146. package/dist/themes/notion.css +29 -0
  147. package/package.json +3 -1
@@ -15,28 +15,15 @@
15
15
  import { Block } from '../block/block.svelte.js';
16
16
  import { InlineBlock } from '../block/inlineBlock.svelte.js';
17
17
  import { Text } from '../text/text.svelte.js';
18
- import type { ContentItem, Destination, DocChange } from '../crdt/index.js';
18
+ import type { Destination, DocChange } from '../crdt/index.js';
19
+ import { type Part } from '../crdt/parts.js';
19
20
  import type { Edytor } from '../edytor.svelte.js';
20
21
  import type { JSONText } from '../utils/json.js';
21
22
  export type TextRunItem = {
22
23
  text: string;
23
24
  marks?: Record<string, unknown>;
24
25
  };
25
- /** A block's content as the index shows it: text segments between atoms, each at its display start. */
26
- export type ContentPart = {
27
- kind: 'text';
28
- start: number;
29
- items: TextRunItem[];
30
- length: number;
31
- } | {
32
- kind: 'inline';
33
- start: number;
34
- item: ContentItem & {
35
- kind: 'inline';
36
- };
37
- };
38
- /** Segments and atoms, alternating, starting and ending with a segment. */
39
- export declare const contentParts: (items: readonly ContentItem[]) => ContentPart[];
26
+ export type { Part as ContentPart } from '../crdt/parts.js';
40
27
  /**
41
28
  * A text segment's items as `JSONText` runs (`text.value`): empty items
42
29
  * dropped, adjacent runs with the same marks joined, each run's marks a
@@ -65,7 +52,7 @@ export declare class Handles {
65
52
  * Block `id`'s content parts — from its runs, which the index keeps per
66
53
  * block and reads-your-writes (a write elsewhere re-derives nothing here).
67
54
  */
68
- parts: (id: string) => ContentPart[];
55
+ parts: (id: string) => readonly Part[];
69
56
  /** Forget the handles of the blocks a commit removed. */
70
57
  prune: (change: DocChange) => void;
71
58
  clear: () => void;
@@ -15,29 +15,7 @@
15
15
  import { Block } from '../block/block.svelte.js';
16
16
  import { InlineBlock } from '../block/inlineBlock.svelte.js';
17
17
  import { Text } from '../text/text.svelte.js';
18
- /** Segments and atoms, alternating, starting and ending with a segment. */
19
- export const contentParts = (items) => {
20
- const parts = [];
21
- let segment = [];
22
- let start = 0;
23
- let at = 0;
24
- const close = () => {
25
- parts.push({ kind: 'text', start, items: segment, length: at - start });
26
- segment = [];
27
- };
28
- for (const item of items) {
29
- if (item.kind === 'text') {
30
- segment.push({ text: item.text, ...(item.marks ? { marks: item.marks } : {}) });
31
- at += item.text.length;
32
- continue;
33
- }
34
- close();
35
- parts.push({ kind: 'inline', start: at, item });
36
- start = ++at;
37
- }
38
- close();
39
- return parts;
40
- };
18
+ import { partsOf } from '../crdt/parts.js';
41
19
  /**
42
20
  * A text segment's items as `JSONText` runs (`text.value`): empty items
43
21
  * dropped, adjacent runs with the same marks joined, each run's marks a
@@ -58,7 +36,7 @@ export const segmentJSON = (items) => {
58
36
  }
59
37
  return out;
60
38
  };
61
- const ROOT = contentParts([]);
39
+ const ROOT = partsOf([]);
62
40
  /** The destination right after `block` among its siblings (`after: false`, right before it). */
63
41
  export const siblingSlot = (block, after = true) => {
64
42
  const parent = block.parent;
@@ -73,7 +51,7 @@ export class Handles {
73
51
  #blocks = new Map();
74
52
  #texts = new Map();
75
53
  #atoms = new Map();
76
- /** Each block's parts at the index version they were read (the last read of a dead block). */
54
+ /** Each block's parts as last read (a dead block's: what it last showed). */
77
55
  #parts = new Map();
78
56
  constructor(edytor) {
79
57
  this.edytor = edytor;
@@ -119,24 +97,20 @@ export class Handles {
119
97
  parts = (id) => {
120
98
  if (id === 'root')
121
99
  return ROOT;
122
- const { facade } = this.edytor;
123
- const memo = this.#parts.get(id);
124
- const runs = facade.runs(id);
125
- if (memo?.runs === runs)
126
- return memo.parts;
100
+ const parts = partsOf(this.edytor.facade.runs(id));
101
+ const last = this.#parts.get(id);
127
102
  // A dead block keeps what it last showed (a write's intent is minted from it).
128
- if (memo && !facade.isVisibleBlock(id))
129
- return memo.parts;
130
- const parts = contentParts(runs);
131
- this.#parts.set(id, { runs, parts });
103
+ if (last && last !== parts && !this.edytor.facade.isVisibleBlock(id))
104
+ return last;
105
+ this.#parts.set(id, parts);
132
106
  return parts;
133
107
  };
134
108
  /** Forget the handles of the blocks a commit removed. */
135
109
  prune = (change) => {
136
110
  for (const id of change.removed) {
137
- for (const part of this.#parts.get(id)?.parts ?? [])
138
- if (part.kind === 'inline' && this.#atoms.get(part.item.id)?.blockId === id)
139
- this.#atoms.delete(part.item.id);
111
+ for (const part of this.#parts.get(id) ?? [])
112
+ if (part.kind === 'inline' && this.#atoms.get(part.id)?.blockId === id)
113
+ this.#atoms.delete(part.id);
140
114
  this.#blocks.delete(id);
141
115
  this.#texts.delete(id);
142
116
  this.#parts.delete(id);
@@ -1,8 +1,9 @@
1
1
  import { getNextGraphemeEnd, getNextWordEndOffset, getPreviousGraphemeStart, getPreviousWordStartOffset } from '../text/wordBoundary.js';
2
2
  import { getSelectedBlocksInDocumentOrder, shownText } from '../selection/replaceSelection.js';
3
- import { atomSelection, segmentsOf } from './selection.js';
3
+ import { atomSelection, pointOf, segmentsOf } from './selection.js';
4
4
  import { untargeted } from './attempt.js';
5
5
  import { siblingSlot } from './handles.js';
6
+ import { partsOf } from '../crdt/parts.js';
6
7
  /**
7
8
  * With no target (`untargeted`: the value is `none`, no gesture placed the
8
9
  * host's caret), a navigation key does nothing (Notion): it is claimed, so
@@ -16,9 +17,8 @@ export const stayWithoutTarget = (edytor, prevent) => {
16
17
  /** An inline atom in a block's string. */
17
18
  const ATOM = '';
18
19
  /** A block's content as one string, each inline atom one `ATOM`. */
19
- const stringOf = (edytor, block) => edytor.facade
20
- .contentItems(block)
21
- .map((item) => (item.kind === 'text' ? item.text : ATOM))
20
+ const stringOf = (edytor, block) => partsOf(edytor.facade.runs(block))
21
+ .map((part) => (part.kind === 'text' ? part.text : ATOM))
22
22
  .join('');
23
23
  /** A block's stop at its edge in `dir` (its end going forward, its start going backward). */
24
24
  const edge = (edytor, block, dir) => ({
@@ -68,9 +68,7 @@ const select = (edytor, anchor, focus) => {
68
68
  : null;
69
69
  if (atom?.kind === 'inline')
70
70
  return selection.select(atomSelection(start.block, atom.id, order > 0 ? 'after' : 'before'));
71
- const [from, to] = [selection.textAt(start), selection.textAt(end)];
72
- if (from && to)
73
- selection.selectText(from.text, from.offset, to.text, to.offset, order > 0);
71
+ selection.selectAt(start, end, order > 0);
74
72
  };
75
73
  /**
76
74
  * One apply step for `unit` in `dir` (`left`/`right`: visual). Answers whether
@@ -94,8 +92,7 @@ export const move = (edytor, unit, key, extend) => {
94
92
  const first = blocks.map((block) => shownText(block, 'first')).find(Boolean);
95
93
  const last = blocks.map((block) => shownText(block, 'last')).findLast(Boolean);
96
94
  if (first && last) {
97
- start = { block: first.blockId, offset: first.segStart };
98
- end = { block: last.blockId, offset: last.segStart + last.length };
95
+ [start, end] = [pointOf(first, 0), pointOf(last, last.length)];
99
96
  }
100
97
  // Voids alone show no line: the blocks stay selected, except for the
101
98
  // document keys, whose target is the document's edge (`select` moves
@@ -13,8 +13,9 @@
13
13
  * The view's `commit(value, cause)` (public: `select(value)`) is the only writer; the
14
14
  * presence payload is `serialize(value)`.
15
15
  */
16
- import type { BlockId, ContentItem } from '../crdt/index.js';
16
+ import type { BlockId, ContentRun } from '../crdt/index.js';
17
17
  import type { DocAnchor, EdytorDoc } from '../crdt/edytor-doc.js';
18
+ import type { Text } from '../text/text.svelte.js';
18
19
  export type Marks = Record<string, unknown>;
19
20
  export type SelectionValue = {
20
21
  readonly kind: 'none';
@@ -45,7 +46,9 @@ export type SelectionPoint = {
45
46
  block: BlockId;
46
47
  offset: number;
47
48
  };
48
- type TextRun = Extract<ContentItem, {
49
+ /** A point inside a text segment (its block's `ordinal`-th) as a block-offset point. */
50
+ export declare const pointOf: (text: Text, offset: number) => SelectionPoint;
51
+ type TextRun = Extract<ContentRun, {
49
52
  kind: 'text';
50
53
  }>;
51
54
  /** One content part of a block: a text segment (between atoms, possibly empty) or an inline atom. */
@@ -55,7 +58,7 @@ export type SelectionSegment = {
55
58
  segOrd: number;
56
59
  start: number;
57
60
  end: number;
58
- runs: TextRun[];
61
+ runs: readonly TextRun[];
59
62
  } | {
60
63
  block: BlockId;
61
64
  kind: 'inline';
@@ -88,7 +91,7 @@ export type SelectionProjection = {
88
91
  readonly content: string;
89
92
  };
90
93
  /** The document reads a projection needs (the facade satisfies it). */
91
- export type ProjectionDoc = Pick<EdytorDoc, 'version' | 'resolveAnchor' | 'compare' | 'order' | 'contentItems' | 'ancestorsOf' | 'isIsland' | 'isVoid' | 'isVisibleBlock'>;
94
+ export type ProjectionDoc = Pick<EdytorDoc, 'version' | 'resolveAnchor' | 'compare' | 'order' | 'runs' | 'ancestorsOf' | 'isIsland' | 'isVoid' | 'isVisibleBlock'>;
92
95
  /** A block's content parts; always text first and last, one text segment between two atoms. */
93
96
  export declare const segmentsOf: (doc: ProjectionDoc, block: BlockId) => SelectionSegment[];
94
97
  /** The projection of `value` at the document's current version, memoized per (value, version). */
@@ -97,7 +100,10 @@ export declare const project: (value: SelectionValue, doc: ProjectionDoc) => Sel
97
100
  export declare const sameEnds: (a: SelectionProjection, b: SelectionProjection) => boolean;
98
101
  /** Why a selection was written. */
99
102
  export type SelectCause = 'dom' | 'model' | 'repair' | 'history';
100
- /** Value equality: kind, anchors (item + side), ids, pending marks. */
103
+ /**
104
+ * Value equality: kind, anchors (item + side), ids, pending marks. A value
105
+ * is plain JSON (anchors, ids, marks), so it is the JSON equality.
106
+ */
101
107
  export declare const sameValue: (a: SelectionValue, b: SelectionValue) => boolean;
102
108
  /**
103
109
  * The presence payload: anchors only. A text range publishes its
@@ -1,36 +1,16 @@
1
+ import { partsOf, segmentAt } from '../crdt/parts.js';
1
2
  import { jsonEquals } from '../utils/json.js';
2
3
  export const noSelection = Object.freeze({ kind: 'none' });
3
4
  export const textSelection = (anchor, focus = anchor, pending) => Object.freeze({ kind: 'text', anchor, focus, ...(pending ? { pending } : {}) });
4
5
  export const atomSelection = (blockId, atomId, from = 'before') => Object.freeze({ kind: 'atom', blockId, atomId, from });
5
6
  export const blockSelection = (ids) => Object.freeze({ kind: 'blocks', ids: Object.freeze([...ids]) });
7
+ /** A point inside a text segment (its block's `ordinal`-th) as a block-offset point. */
8
+ export const pointOf = (text, offset) => ({
9
+ block: text.blockId,
10
+ offset: text.segStart + offset
11
+ });
6
12
  /** A block's content parts; always text first and last, one text segment between two atoms. */
7
- export const segmentsOf = (doc, block) => {
8
- const out = [];
9
- let text = { block, kind: 'text', segOrd: 0, start: 0, end: 0, runs: [] };
10
- for (const item of doc.contentItems(block)) {
11
- if (item.kind === 'text') {
12
- text.runs.push(item);
13
- text.end += item.text.length;
14
- continue;
15
- }
16
- out.push(text, { block, kind: 'inline', id: item.id, start: text.end, end: text.end + 1 });
17
- text = {
18
- block,
19
- kind: 'text',
20
- segOrd: text.segOrd + 1,
21
- start: text.end + 1,
22
- end: text.end + 1,
23
- runs: []
24
- };
25
- }
26
- out.push(text);
27
- return out;
28
- };
29
- /** The text segment displaying `offset`: the first one ending at or after it (a seam belongs to the left text). */
30
- const textSegmentAt = (segments, offset) => {
31
- const texts = segments.filter((s) => s.kind === 'text');
32
- return texts.find((s) => offset <= s.end) ?? texts[texts.length - 1];
33
- };
13
+ export const segmentsOf = (doc, block) => partsOf(doc.runs(block)).map((part) => ({ ...part, block }));
34
14
  const EMPTY = {
35
15
  start: null,
36
16
  end: null,
@@ -95,8 +75,8 @@ const projectRange = (doc, kind, anchor, focus, atomic) => {
95
75
  const partsOf = new Map(blocks.map((id) => [id, segmentsOf(doc, id)]));
96
76
  const startSegments = partsOf.get(start.block) ?? segmentsOf(doc, start.block);
97
77
  const endSegments = partsOf.get(end.block) ?? segmentsOf(doc, end.block);
98
- const first = textSegmentAt(startSegments, start.offset);
99
- const last = textSegmentAt(endSegments, end.offset);
78
+ const first = segmentAt(startSegments, start.offset);
79
+ const last = segmentAt(endSegments, end.offset);
100
80
  const all = blocks.flatMap((id) => partsOf.get(id));
101
81
  const from = all.indexOf(first);
102
82
  const to = all.indexOf(last);
@@ -191,25 +171,11 @@ export const sameEnds = (a, b) => [a.start, a.end].every((p, i) => {
191
171
  const q = i ? b.end : b.start;
192
172
  return p?.block === q?.block && p?.offset === q?.offset;
193
173
  });
194
- const sameAnchor = (a, b) => a === b ||
195
- (a.b === b.b &&
196
- a.a.a === b.a.a &&
197
- (a.a.i === b.a.i || (a.a.i?.c === b.a.i?.c && a.a.i?.k === b.a.i?.k)));
198
- const sameMarks = (a, b) => jsonEquals(a ?? null, b ?? null);
199
- /** Value equality: kind, anchors (item + side), ids, pending marks. */
200
- export const sameValue = (a, b) => {
201
- if (a === b)
202
- return true;
203
- if (a.kind === 'text' && b.kind === 'text')
204
- return (sameAnchor(a.anchor, b.anchor) &&
205
- sameAnchor(a.focus, b.focus) &&
206
- sameMarks(a.pending, b.pending));
207
- if (a.kind === 'blocks' && b.kind === 'blocks')
208
- return a.ids.length === b.ids.length && a.ids.every((id, i) => id === b.ids[i]);
209
- if (a.kind === 'atom' && b.kind === 'atom')
210
- return a.blockId === b.blockId && a.atomId === b.atomId && a.from === b.from;
211
- return a.kind === b.kind && a.kind === 'none';
212
- };
174
+ /**
175
+ * Value equality: kind, anchors (item + side), ids, pending marks. A value
176
+ * is plain JSON (anchors, ids, marks), so it is the JSON equality.
177
+ */
178
+ export const sameValue = jsonEquals;
213
179
  /**
214
180
  * A text value's anchors as (start, end). A range an edit collapsed has no
215
181
  * order left; its anchors keep the roles they were minted with (a start
@@ -1,4 +1,5 @@
1
1
  import type { BlockId, DocAnchor, OpResult } from '../crdt/index.js';
2
+ import type { SuggestedChange } from '../crdt/doc/suggest.js';
2
3
  import type { Edytor } from '../edytor.svelte.js';
3
4
  import { type JSONBlock } from '../utils/json.js';
4
5
  /** A text range, by its anchors (a text selection's). */
@@ -48,12 +49,15 @@ export type SuggestionsAt = {
48
49
  };
49
50
  /** The block's `data-edytor-suggestion-replaced` (`empty`: folded, the preview stands in its place). */
50
51
  export declare const replacedMark: ({ replaced, inPlace }: SuggestionsAt) => "" | "empty" | null;
52
+ /** A view-local suggestion: shown in this view only, until it is accepted. */
51
53
  export declare class Suggestion {
52
54
  #private;
53
55
  readonly id: string;
54
56
  /** The position, resolved: `replace: 'selection'` is the selection's blocks or text range. */
55
57
  readonly at: ResolvedAt;
56
58
  readonly label: string | undefined;
59
+ /** View-local: a {@link SharedSuggestion} is in the document. */
60
+ readonly shared: false;
57
61
  constructor(layer: Suggestions, at: ResolvedAt, kind: string, options: SuggestionOptions, status: SuggestionStatus);
58
62
  /** The proposed blocks (reactive). */
59
63
  get content(): readonly JSONBlock[];
@@ -71,25 +75,50 @@ export declare class Suggestion {
71
75
  append: (text: string) => void;
72
76
  /** The stream ended: `status` is `ready`. */
73
77
  done: () => void;
74
- /** Place the content in the document: one command, one undo step (see {@link Suggestions}). */
78
+ /** Accept it: one command, one undo step, placing it in the document (see {@link Suggestions}). */
75
79
  accept: () => OpResult;
76
- /** Drop it; writes nothing. */
80
+ /** Drop it (writes nothing). */
77
81
  discard: () => void;
78
82
  /** Empty it, stream again, and ask the app for another answer (`onRetry`). */
79
83
  retry: () => void;
80
84
  }
85
+ /**
86
+ * A shared suggestion (`createSharedSuggestionsPlugin`): in the document,
87
+ * so every collaborator sees it with its author and reviews it. Its
88
+ * `accept()` and `reject()` are one command each (`acceptSuggestion`,
89
+ * `rejectSuggestion`) of a view that may decide; `discard()` rejects it
90
+ * (its author's withdrawal too).
91
+ */
92
+ export interface SharedSuggestion extends Omit<Suggestion, 'shared' | 'at'> {
93
+ readonly shared: true;
94
+ /** Its position as this view added it; `null` for one another view made (its `changes` say where it is). */
95
+ readonly at: ResolvedAt | null;
96
+ /** Its author (an actor id). */
97
+ readonly by: string | undefined;
98
+ /** Who wrote it for its author (`'ai'`): a reviewer sees "Ada (AI)". */
99
+ readonly via: string | undefined;
100
+ /** Its pending changes, in document order (reactive). */
101
+ readonly changes: readonly SuggestedChange[];
102
+ /** This view may accept or reject it. */
103
+ readonly canDecide: boolean;
104
+ /** Its author is this view's actor (it may reject it). */
105
+ readonly mine: boolean;
106
+ /** Reject its changes: one command (`rejectSuggestion`). */
107
+ reject(): OpResult;
108
+ }
109
+ /** A suggestion of the list: view-local, or shared (with the shared suggestions plugin). */
110
+ export type AnySuggestion = Suggestion | SharedSuggestion;
81
111
  /** The view's suggestions (`edytor.suggestions`). */
82
112
  export declare class Suggestions {
83
113
  #private;
84
- private edytor;
85
- /** Bumped by every change of a suggestion: the surface's passes re-run. */
114
+ readonly edytor: Edytor;
115
+ /** Bumped by every change of a suggestion (and of the document's): the surface's passes re-run. */
86
116
  revision: number;
87
- constructor(edytor: Edytor);
88
- /** Every live suggestion, oldest first (reactive). */
89
- get list(): readonly Suggestion[];
90
- /** The newest one: what the keys act on. */
117
+ /** Every suggestion, view-local and shared, oldest first (reactive). */
118
+ get list(): readonly AnySuggestion[];
119
+ /** The newest view-local one: what the keys act on. */
91
120
  get latest(): Suggestion | undefined;
92
- get: (id: string) => Suggestion | undefined;
121
+ get: (id: string) => AnySuggestion | undefined;
93
122
  /**
94
123
  * Propose `content` at `at`. Throws when the position names no shown
95
124
  * block, a block that holds no children (`inside` a void or a code line),
@@ -1,13 +1,22 @@
1
1
  /**
2
- * Suggestions (site `editor/suggestions`): content one view proposes at a
3
- * position — AI output, a completion — shown only in that view until it is
4
- * accepted. Session state: nothing reaches the document, its history,
5
- * presence, `onChange` or another view before `accept()`, which is one
6
- * command (`acceptSuggestion`: admission, hooks, one transaction, one undo
7
- * step) placing the content as a paste places it (`flow.place`, `flow.slot`,
8
- * `flow.inline`). A suggestion is anchored by block ids (a text range by its
9
- * anchors): it survives selection changes and edits elsewhere, and is dropped
10
- * when its block dies. The layer owns the list; the surface renders it.
2
+ * Suggestions (site `editor/suggestions`): content proposed at a position —
3
+ * AI output, a completion, a collaborator's edit to review — in one list,
4
+ * each one view-local or shared (`shared`).
5
+ *
6
+ * - **View-local** (the default): shown only in this view until accepted.
7
+ * Session state: nothing reaches the document, its history, presence,
8
+ * `onChange` or another view before `accept()`, one command
9
+ * (`acceptSuggestion`: admission, hooks, one transaction, one undo step)
10
+ * placing the content as a paste places it (`flow.place`, `flow.slot`,
11
+ * `flow.inline`). Anchored by block ids (a text range by its anchors): it
12
+ * survives selection changes and edits elsewhere, and is dropped when its
13
+ * block dies.
14
+ * - **Shared** (with the shared suggestions plugin,
15
+ * `plugins/suggestions/shared.svelte.ts`, `sugg.*`): in the document, so
16
+ * every replica sees it and its author is known. The plugin adds them and
17
+ * lists them here (`shared`, the port it sets).
18
+ *
19
+ * The layer owns the list; the surface renders it.
11
20
  */
12
21
  import { DEV } from 'esm-env';
13
22
  import { REFUSED } from '../crdt/doc/plan.js';
@@ -34,11 +43,14 @@ const sideOf = (at) => {
34
43
  const side = SIDES.find((side) => side in at);
35
44
  return side && [side, at[side]];
36
45
  };
46
+ /** A view-local suggestion: shown in this view only, until it is accepted. */
37
47
  export class Suggestion {
38
48
  id;
39
49
  /** The position, resolved: `replace: 'selection'` is the selection's blocks or text range. */
40
50
  at;
41
51
  label;
52
+ /** View-local: a {@link SharedSuggestion} is in the document. */
53
+ shared = false;
42
54
  #content = $state.raw([]);
43
55
  #status = $state('ready');
44
56
  #layer;
@@ -109,9 +121,9 @@ export class Suggestion {
109
121
  if (this.live)
110
122
  this.#status = 'ready';
111
123
  };
112
- /** Place the content in the document: one command, one undo step (see {@link Suggestions}). */
124
+ /** Accept it: one command, one undo step, placing it in the document (see {@link Suggestions}). */
113
125
  accept = () => this.#layer.accept(this);
114
- /** Drop it; writes nothing. */
126
+ /** Drop it (writes nothing). */
115
127
  discard = () => this.#layer.drop(this);
116
128
  /** Empty it, stream again, and ask the app for another answer (`onRetry`). */
117
129
  retry = () => {
@@ -127,27 +139,33 @@ export class Suggestion {
127
139
  export class Suggestions {
128
140
  edytor;
129
141
  #list = $state.raw([]);
130
- /** Bumped by every change of a suggestion: the surface's passes re-run. */
142
+ /** Bumped by every change of a suggestion (and of the document's): the surface's passes re-run. */
131
143
  revision = $state(0);
144
+ /**
145
+ * @internal The shared suggestions plugin's part (`createSharedSuggestionsPlugin`):
146
+ * the list with the shared ones after this view's own.
147
+ */
148
+ shared;
149
+ /** @internal */
132
150
  constructor(edytor) {
133
151
  this.edytor = edytor;
134
152
  }
135
- /** Every live suggestion, oldest first (reactive). */
153
+ /** Every suggestion, view-local and shared, oldest first (reactive). */
136
154
  get list() {
137
- return this.#list;
155
+ return this.shared?.(this.#list) ?? this.#list;
138
156
  }
139
- /** The newest one: what the keys act on. */
157
+ /** The newest view-local one: what the keys act on. */
140
158
  get latest() {
141
159
  return this.#list.at(-1);
142
160
  }
143
- get = (id) => this.#list.find((s) => s.id === id);
161
+ get = (id) => this.list.find((s) => s.id === id);
144
162
  /**
145
163
  * Propose `content` at `at`. Throws when the position names no shown
146
164
  * block, a block that holds no children (`inside` a void or a code line),
147
165
  * or no selection (`replace: 'selection'` with none).
148
166
  */
149
167
  add = (at, content, options = {}) => {
150
- const resolved = this.#resolve(at);
168
+ const resolved = this.resolve(at);
151
169
  const { facade, document } = this.edytor;
152
170
  const anchor = this.#blocks(resolved)[0];
153
171
  const parent = 'inside' in resolved ? anchor : (facade.parentOf(anchor) ?? null);
@@ -327,7 +345,8 @@ export class Suggestions {
327
345
  const blocks = this.#blocks(at);
328
346
  return blocks.length > 0 && blocks.every((id) => this.edytor.facade.isVisibleBlock(id));
329
347
  };
330
- #resolve = (at) => {
348
+ /** @internal `at` resolved: a selection read, a position checked (throws when it names nothing shown). */
349
+ resolve = (at) => {
331
350
  const { edytor } = this;
332
351
  const fail = (why) => {
333
352
  throw new RangeError(`[edytor] suggestions.add: ${why}`);
@@ -28,6 +28,6 @@ ready: () => boolean,
28
28
  type: () => string,
29
29
  /** A kind's roles, for where a paste's caret ends (`placedEnd`). */
30
30
  roleOf: (kind: string) => {
31
- void: boolean;
31
+ childless: boolean;
32
32
  rendersContent: boolean;
33
33
  }) => ViewDoc;
@@ -42,6 +42,11 @@ export declare const ownedOf: (edytor: Edytor, element: Element, entry: {
42
42
  block: string;
43
43
  name?: string;
44
44
  owned?: () => Owned;
45
+ handle?: {
46
+ id: string;
47
+ type?: string;
48
+ isEmpty?: boolean;
49
+ };
45
50
  } | null) => Owned | null;
46
51
  /** Heal `element` against `table`; answers whether anything was written. */
47
52
  export declare const heal: (element: HTMLElement, table: Owned) => boolean;
@@ -63,7 +63,7 @@ export const ownedOf = (edytor, element, entry) => {
63
63
  style: { display: 'none' }
64
64
  };
65
65
  if (entry.kind === 'atom') {
66
- const atom = edytor.nodeToInlineBlock.get(element);
66
+ const atom = entry.handle;
67
67
  return {
68
68
  strict: true,
69
69
  owned: {
@@ -74,7 +74,7 @@ export const ownedOf = (edytor, element, entry) => {
74
74
  style: {}
75
75
  };
76
76
  }
77
- const text = edytor.nodeToText.get(element);
77
+ const text = entry.handle;
78
78
  const voided = insideVoid(edytor, block);
79
79
  return {
80
80
  strict: true,
@@ -39,37 +39,14 @@ export type Cells = {
39
39
  * reports (`onPatch` is told which cells each report replaced).
40
40
  */
41
41
  export declare const createCells: (source: CellSource, onPatch?: (patched: Patched, before: Replaced) => void) => Cells;
42
- export declare const START = "start";
43
- type TextRun = Extract<ContentRun, {
44
- kind: 'text';
45
- }>;
46
- /** The text between two inline atoms (or a content edge), keyed causally. */
47
- export type Segment = {
48
- readonly kind: 'text';
49
- /** The id of the atom before this segment, or {@link START}. */
50
- readonly key: string;
51
- readonly text: string;
52
- readonly runs: readonly TextRun[];
53
- };
42
+ export { START, partsOf, type Segment, type AtomPart, type Part } from '../crdt/parts.js';
43
+ import { type Segment } from '../crdt/parts.js';
54
44
  /** A suggestion's block as a cell (`components/Suggestion`): rendered like one, never registered. */
55
45
  export type PreviewCell = Omit<Cell, 'childIds'> & {
56
46
  readonly children: readonly PreviewCell[];
57
47
  };
58
48
  /** `blocks` as preview cells, keyed by position under `key` (stable while a stream grows). */
59
49
  export declare const previewCells: (blocks: readonly JSONBlock[], key: string) => PreviewCell[];
60
- export type AtomPart = {
61
- readonly kind: 'inline';
62
- readonly key: string;
63
- readonly id: string;
64
- readonly type: string;
65
- readonly data: Data;
66
- };
67
- export type Part = Segment | AtomPart;
68
- /**
69
- * A cell's content as segments and atoms, alternating, starting and ending
70
- * with a segment (an empty content is one empty `start` segment).
71
- */
72
- export declare const partsOf: (runs: readonly ContentRun[]) => readonly Part[];
73
50
  /** Whether a view renders mark `name` (its record has a tag or a snippet): an unknown mark renders its text only. */
74
51
  export declare const rendersMark: (marks: ReadonlyMap<string, {
75
52
  tag?: unknown;
@@ -105,4 +82,3 @@ export declare const segmentDeltas: (cell: Cell, segment: Segment, transform?: T
105
82
  * composition is in it.
106
83
  */
107
84
  export declare const placeholderOf: (cell: Cell, composing: boolean) => boolean;
108
- export {};
@@ -125,7 +125,8 @@ export const createCells = (source, onPatch) => {
125
125
  };
126
126
  };
127
127
  // ── segments and render deltas ──────────────────────────────────────────
128
- export const START = 'start';
128
+ export { START, partsOf } from '../crdt/parts.js';
129
+ import { partsOf } from '../crdt/parts.js';
129
130
  /** `blocks` as preview cells, keyed by position under `key` (stable while a stream grows). */
130
131
  export const previewCells = (blocks, key) => blocks.map((block, index) => {
131
132
  const id = `${key}.${index}`;
@@ -139,38 +140,6 @@ export const previewCells = (blocks, key) => blocks.map((block, index) => {
139
140
  children: Object.freeze(previewCells(block.children ?? [], id))
140
141
  });
141
142
  });
142
- const partsMemo = new WeakMap();
143
- /**
144
- * A cell's content as segments and atoms, alternating, starting and ending
145
- * with a segment (an empty content is one empty `start` segment).
146
- */
147
- export const partsOf = (runs) => {
148
- const memo = partsMemo.get(runs);
149
- if (memo)
150
- return memo;
151
- const parts = [];
152
- let key = START;
153
- let segment = [];
154
- const close = () => parts.push(Object.freeze({
155
- kind: 'text',
156
- key,
157
- text: segment.map((run) => run.text).join(''),
158
- runs: Object.freeze(segment)
159
- }));
160
- for (const run of runs) {
161
- if (run.kind === 'text') {
162
- segment.push(run);
163
- continue;
164
- }
165
- close();
166
- parts.push(Object.freeze({ kind: 'inline', key: run.id, id: run.id, type: run.type, data: run.data }));
167
- key = run.id;
168
- segment = [];
169
- }
170
- close();
171
- partsMemo.set(runs, Object.freeze(parts));
172
- return parts;
173
- };
174
143
  /** Whether a view renders mark `name` (its record has a tag or a snippet): an unknown mark renders its text only. */
175
144
  export const rendersMark = (marks, name) => {
176
145
  const mark = marks.get(name);