edytor 3.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 (231) 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 +23 -12
  6. package/dist/cloudflare/DocumentRoom.js +10 -6
  7. package/dist/cloudflare/index.d.ts +1 -1
  8. package/dist/cloudflare/room/access.d.ts +8 -3
  9. package/dist/cloudflare/room/access.js +44 -20
  10. package/dist/cloudflare/room/admission.js +28 -5
  11. package/dist/cloudflare/room/comments.js +28 -1
  12. package/dist/cloudflare/room/context.d.ts +139 -129
  13. package/dist/cloudflare/room/context.js +11 -4
  14. package/dist/cloudflare/room/forged.d.ts +14 -0
  15. package/dist/cloudflare/room/forged.js +30 -2
  16. package/dist/cloudflare/room/frames.d.ts +4 -0
  17. package/dist/cloudflare/room/frames.js +5 -1
  18. package/dist/cloudflare/room/purge.d.ts +6 -3
  19. package/dist/cloudflare/room/purge.js +45 -19
  20. package/dist/cloudflare/room/replicas.d.ts +4 -2
  21. package/dist/cloudflare/room/replicas.js +8 -3
  22. package/dist/cloudflare/room/suggest.d.ts +7 -0
  23. package/dist/cloudflare/room/suggest.js +143 -0
  24. package/dist/cloudflare/room/updates.d.ts +3 -4
  25. package/dist/cloudflare/room/updates.js +6 -8
  26. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  27. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  28. package/dist/components/Content.svelte +9 -1
  29. package/dist/crdt/anchors.js +3 -2
  30. package/dist/crdt/attribution/attribution.d.ts +34 -12
  31. package/dist/crdt/attribution/attribution.js +59 -21
  32. package/dist/crdt/attribution/block.d.ts +3 -3
  33. package/dist/crdt/attribution/block.js +7 -8
  34. package/dist/crdt/attribution/index.d.ts +1 -1
  35. package/dist/crdt/attribution/index.js +1 -1
  36. package/dist/crdt/data.d.ts +62 -31
  37. package/dist/crdt/data.js +211 -103
  38. package/dist/crdt/doc/capability.js +9 -13
  39. package/dist/crdt/doc/delete.d.ts +1 -1
  40. package/dist/crdt/doc/delete.js +13 -30
  41. package/dist/crdt/doc/funnel.d.ts +1 -1
  42. package/dist/crdt/doc/funnel.js +34 -26
  43. package/dist/crdt/doc/gate.d.ts +3 -3
  44. package/dist/crdt/doc/history.d.ts +31 -9
  45. package/dist/crdt/doc/history.js +204 -73
  46. package/dist/crdt/doc/layout.js +16 -27
  47. package/dist/crdt/doc/meta.js +17 -23
  48. package/dist/crdt/doc/moves.d.ts +1 -1
  49. package/dist/crdt/doc/moves.js +52 -32
  50. package/dist/crdt/doc/reads.d.ts +2 -2
  51. package/dist/crdt/doc/reads.js +20 -30
  52. package/dist/crdt/doc/seed.js +2 -2
  53. package/dist/crdt/doc/split-merge.js +12 -15
  54. package/dist/crdt/doc/steps.d.ts +25 -13
  55. package/dist/crdt/doc/steps.js +55 -125
  56. package/dist/crdt/doc/suggest.d.ts +167 -0
  57. package/dist/crdt/doc/suggest.js +579 -0
  58. package/dist/crdt/doc/types.d.ts +21 -9
  59. package/dist/crdt/document.d.ts +26 -6
  60. package/dist/crdt/document.js +40 -15
  61. package/dist/crdt/edytor-doc.d.ts +410 -390
  62. package/dist/crdt/edytor-doc.js +143 -95
  63. package/dist/crdt/engine-api.d.ts +39 -1
  64. package/dist/crdt/engine.d.ts +2 -2
  65. package/dist/crdt/engine.js +4 -5
  66. package/dist/crdt/flow.d.ts +2 -2
  67. package/dist/crdt/flow.js +29 -37
  68. package/dist/crdt/index.d.ts +134 -127
  69. package/dist/crdt/index.js +4 -1
  70. package/dist/crdt/operations.d.ts +12 -3
  71. package/dist/crdt/parts.d.ts +43 -0
  72. package/dist/crdt/parts.js +44 -0
  73. package/dist/crdt/placement/display.d.ts +6 -31
  74. package/dist/crdt/placement/display.js +41 -146
  75. package/dist/crdt/placement/keys.d.ts +63 -0
  76. package/dist/crdt/placement/keys.js +95 -0
  77. package/dist/crdt/placement/model.d.ts +119 -118
  78. package/dist/crdt/placement/model.js +242 -142
  79. package/dist/crdt/placement/resolve.d.ts +123 -35
  80. package/dist/crdt/placement/resolve.js +188 -153
  81. package/dist/crdt/placement/slots.d.ts +134 -0
  82. package/dist/crdt/placement/slots.js +152 -0
  83. package/dist/crdt/protocol.d.ts +1 -1
  84. package/dist/crdt/protocol.js +1 -1
  85. package/dist/crdt/protocols/auth.d.ts +11 -1
  86. package/dist/crdt/protocols/auth.js +17 -1
  87. package/dist/crdt/protocols/comments.d.ts +16 -1
  88. package/dist/crdt/protocols/comments.js +9 -1
  89. package/dist/crdt/providers/index.d.ts +6 -4
  90. package/dist/crdt/providers/index.js +3 -1
  91. package/dist/crdt/providers/room.d.ts +16 -2
  92. package/dist/crdt/providers/room.js +64 -17
  93. package/dist/crdt/providers/websocket.d.ts +6 -4
  94. package/dist/crdt/providers/websocket.js +14 -7
  95. package/dist/crdt/purge.d.ts +24 -9
  96. package/dist/crdt/purge.js +266 -126
  97. package/dist/crdt/restore.d.ts +4 -3
  98. package/dist/crdt/restore.js +27 -34
  99. package/dist/crdt/schema.d.ts +30 -21
  100. package/dist/crdt/schema.js +39 -23
  101. package/dist/crdt/storage.d.ts +1 -1
  102. package/dist/crdt/storage.js +1 -1
  103. package/dist/crdt/structs.d.ts +17 -15
  104. package/dist/crdt/structs.js +10 -22
  105. package/dist/crdt/text/index/cache.js +4 -0
  106. package/dist/crdt/text/index/claims.js +13 -23
  107. package/dist/crdt/text/index/fold.d.ts +2 -3
  108. package/dist/crdt/text/index/fold.js +194 -44
  109. package/dist/crdt/text/index/layout.d.ts +2 -4
  110. package/dist/crdt/text/index/layout.js +14 -30
  111. package/dist/crdt/text/index/placement.d.ts +26 -18
  112. package/dist/crdt/text/index/placement.js +420 -157
  113. package/dist/crdt/text/index/records.d.ts +3 -3
  114. package/dist/crdt/text/index/records.js +24 -31
  115. package/dist/crdt/text/index/shared.d.ts +11 -3
  116. package/dist/crdt/text/index/shared.js +29 -17
  117. package/dist/crdt/text/index/state.d.ts +21 -17
  118. package/dist/crdt/text/index/state.js +15 -17
  119. package/dist/crdt/text/index/streams.d.ts +10 -5
  120. package/dist/crdt/text/index/streams.js +78 -27
  121. package/dist/crdt/text/items.d.ts +10 -3
  122. package/dist/crdt/text/items.js +7 -0
  123. package/dist/crdt/text/model.d.ts +87 -62
  124. package/dist/crdt/text/model.js +91 -169
  125. package/dist/crdt/text/ranges.d.ts +1 -1
  126. package/dist/crdt/text/ranges.js +3 -3
  127. package/dist/crdt/text/rows.d.ts +13 -7
  128. package/dist/crdt/text/rows.js +24 -7
  129. package/dist/crdt/text/runs.d.ts +51 -42
  130. package/dist/crdt/text/runs.js +24 -7
  131. package/dist/crdt/text/streams.d.ts +26 -13
  132. package/dist/crdt/text/streams.js +41 -14
  133. package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
  134. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  135. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  136. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  137. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
  138. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  139. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  140. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  141. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  142. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  143. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  144. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  145. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
  146. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  147. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  148. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  149. package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
  150. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  151. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  152. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  153. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  154. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  155. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  156. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  157. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  158. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  159. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  160. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  161. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  162. package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
  163. package/dist/edytor.svelte.d.ts +1 -1
  164. package/dist/edytor.svelte.js +103 -75
  165. package/dist/events/beforeInputCommands.d.ts +2 -2
  166. package/dist/events/beforeInputCommands.js +2 -2
  167. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  168. package/dist/events/nativeInteractiveControl.js +41 -1
  169. package/dist/events/onBeforeInput.js +3 -3
  170. package/dist/events/onFocus.js +9 -11
  171. package/dist/events/onPageHide.d.ts +3 -0
  172. package/dist/events/onPageHide.js +16 -0
  173. package/dist/export/forms.d.ts +50 -50
  174. package/dist/index.d.ts +4 -2
  175. package/dist/index.js +1 -1
  176. package/dist/labels.d.ts +8 -0
  177. package/dist/labels.js +6 -1
  178. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  179. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  180. package/dist/plugins/find/search.js +2 -2
  181. package/dist/plugins/index.d.ts +1 -0
  182. package/dist/plugins/index.js +1 -0
  183. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  184. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  185. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  186. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  187. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  188. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  189. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  190. package/dist/selection/domSelection.d.ts +2 -0
  191. package/dist/selection/domSelection.js +6 -2
  192. package/dist/selection/pointer.svelte.js +75 -48
  193. package/dist/selection/replaceSelection.js +2 -6
  194. package/dist/selection/selection.svelte.d.ts +3 -3
  195. package/dist/selection/selection.svelte.js +39 -32
  196. package/dist/selection/selection.utils.d.ts +14 -0
  197. package/dist/selection/selection.utils.js +28 -3
  198. package/dist/session/commands.js +21 -11
  199. package/dist/session/composition.svelte.d.ts +5 -3
  200. package/dist/session/composition.svelte.js +21 -17
  201. package/dist/session/handles.d.ts +4 -17
  202. package/dist/session/handles.js +11 -37
  203. package/dist/session/navigation.js +6 -9
  204. package/dist/session/selection.d.ts +11 -5
  205. package/dist/session/selection.js +14 -48
  206. package/dist/session/suggestions.svelte.d.ts +38 -9
  207. package/dist/session/suggestions.svelte.js +37 -18
  208. package/dist/session/virtual.d.ts +1 -1
  209. package/dist/surface/attributes.d.ts +5 -0
  210. package/dist/surface/attributes.js +2 -2
  211. package/dist/surface/cells.d.ts +2 -26
  212. package/dist/surface/cells.js +2 -33
  213. package/dist/surface/observer.svelte.d.ts +6 -1
  214. package/dist/surface/observer.svelte.js +130 -83
  215. package/dist/surface/pin.svelte.d.ts +9 -9
  216. package/dist/surface/pin.svelte.js +15 -22
  217. package/dist/surface/projector.svelte.d.ts +15 -3
  218. package/dist/surface/projector.svelte.js +67 -22
  219. package/dist/text/text.svelte.js +14 -19
  220. package/dist/themes/notion.css +29 -0
  221. package/package.json +4 -2
  222. package/dist/crdt/placement/rank.d.ts +0 -87
  223. package/dist/crdt/placement/rank.js +0 -332
  224. package/dist/crdt/placement/source.d.ts +0 -43
  225. package/dist/crdt/placement/source.js +0 -66
  226. package/dist/crdt/text/delete-index.d.ts +0 -129
  227. package/dist/crdt/text/delete-index.js +0 -365
  228. package/dist/crdt/text/deletes.d.ts +0 -76
  229. package/dist/crdt/text/deletes.js +0 -362
  230. package/dist/crdt/text/index/anchored.d.ts +0 -17
  231. package/dist/crdt/text/index/anchored.js +0 -197
@@ -39,9 +39,9 @@ export class Composition {
39
39
  */
40
40
  targetless = false;
41
41
  /**
42
- * The start target is a range across texts: across blocks, or across an inline
43
- * atom inside one block (`block`). Its first write merges the rest into
44
- * the host text.
42
+ * The start target is a range across texts inside one block (across an
43
+ * inline atom): its first write merges the rest into the host text. A range
44
+ * across blocks is replaced at the start (`#rangeless`).
45
45
  */
46
46
  #across = false;
47
47
  /** That rest, shown in the host once the IME wrote its first preview. */
@@ -108,18 +108,18 @@ export class Composition {
108
108
  this.#item = null;
109
109
  this.marks = insertionMarks(this.edytor, intentSnapshot(this.edytor, 'insertCompositionText'));
110
110
  const shown = selection.value.kind;
111
- this.#across =
112
- shown === 'text' && startText !== endText
113
- ? startText?.parent === endText?.parent
114
- ? 'block'
115
- : 'blocks'
116
- : false;
111
+ // A range across blocks is replaced now, as a block selection is: the
112
+ // IME then writes at a parked caret in one text, never its own delete
113
+ // across blocks (Firefox's joins the blocks' elements and removes the
114
+ // render's anchors between them before the change renders).
115
+ const blocks = shown === 'text' && !!startText && !!endText && startText.parent !== endText.parent;
116
+ this.#across = shown === 'text' && startText !== endText && !blocks;
117
117
  // With no target, the caret the browser parked for the IME.
118
118
  const parked = targetless && dom?.anchorNode;
119
119
  const point = parked ? this.edytor.ports.surface.pointAt(parked, dom.anchorOffset) : null;
120
120
  const at = targetless
121
121
  ? point && { text: point.text, from: point.offset, to: point.offset }
122
- : shown === 'blocks' || shown === 'atom'
122
+ : shown === 'blocks' || shown === 'atom' || blocks
123
123
  ? this.#rangeless()
124
124
  : startText && {
125
125
  text: startText,
@@ -217,7 +217,7 @@ export class Composition {
217
217
  const [merged, host] = [this.#merged, this.host];
218
218
  const cell = merged && host && this.edytor.cells?.get(host.parent.id);
219
219
  if (cell)
220
- this.edytor.pin.merge(cell, merged, this.#across === 'block');
220
+ this.edytor.pin.merge(cell, merged);
221
221
  this.#merged = null;
222
222
  return 'live';
223
223
  }
@@ -286,13 +286,17 @@ export class Composition {
286
286
  }
287
287
  /**
288
288
  * A block or atom selection shows no DOM range, so the IME has no caret
289
- * of its own and would write at the editable's start. Its start
290
- * target is replaced now (blocks: `flow.slot`, one empty block in their
291
- * place; an atom: removed), the change rendered and the DOM caret parked
292
- * where the preview goes before the IME writes (`projector.park`, the
293
- * DOM-selection writer). When the replacement is
289
+ * of its own and would write at the editable's start; over a text range
290
+ * across blocks the IME's own replacement would join the blocks' elements
291
+ * and remove the render's anchors between them before the change renders
292
+ * (Firefox, `ime.range.blocks`). Its start target is replaced now (blocks:
293
+ * `flow.slot`, one empty block in their place; an atom: removed; a range:
294
+ * its blocks merged around the caret), the change rendered and the DOM
295
+ * caret parked where the preview goes before the IME writes
296
+ * (`projector.park`, the DOM-selection writer). When the replacement is
294
297
  * refused, the IME writes where Escape leaves for (`selectedBlocksExit`;
295
- * an atom: before it) and the tail restores it: nothing is written.
298
+ * an atom or a range: before it) and the tail restores it: nothing is
299
+ * written.
296
300
  */
297
301
  #rangeless() {
298
302
  const { edytor } = this;
@@ -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,