@voithos-labs/aragonite 0.10.2 → 0.10.4

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 (300) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +19 -0
  4. package/dist/a11y-strings.js +19 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +37 -24
  9. package/dist/components/BlockHost.svelte +18 -11
  10. package/dist/components/Editor.svelte +434 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +25 -17
  13. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  14. package/dist/components/TailInsert.svelte +107 -0
  15. package/dist/components/TailInsert.svelte.d.ts +17 -0
  16. package/dist/components/block-content-selector.d.ts +6 -2
  17. package/dist/components/block-content-selector.js +6 -2
  18. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  19. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  20. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  21. package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
  22. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  23. package/dist/components/blocks/code/code-bootstrap.js +16 -0
  24. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  25. package/dist/components/blocks/code/code-context-actions.js +24 -0
  26. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  27. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  28. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  29. package/dist/components/blocks/code/code-languages.js +22 -3
  30. package/dist/components/blocks/code/code-renderer.js +11 -0
  31. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  32. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  33. package/dist/components/blocks/editable-leaf.js +248 -30
  34. package/dist/components/blocks/editable-surface.d.ts +9 -0
  35. package/dist/components/blocks/editable-surface.js +22 -3
  36. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  37. package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
  38. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  39. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  40. package/dist/components/blocks/list/task-checkbox.js +11 -2
  41. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  42. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  43. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  44. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  45. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  46. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  47. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  48. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  49. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  50. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  51. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  52. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  53. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  54. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  55. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  56. package/dist/components/blocks/table/table-menu-model.js +36 -17
  57. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  58. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  59. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  60. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  61. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  62. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  63. package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
  64. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  65. package/dist/components/blocks/text/live-selection-edit.js +37 -7
  66. package/dist/components/blocks/text/text-clipboard.js +2 -2
  67. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  68. package/dist/components/blocks/text/text-keydown.js +11 -1
  69. package/dist/components/blocks/text/text-render.d.ts +1 -1
  70. package/dist/components/blocks/text/text-render.js +3 -1
  71. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  72. package/dist/components/blocks/text/widget-interaction.js +181 -37
  73. package/dist/components/drag-handle.d.ts +41 -0
  74. package/dist/components/drag-handle.js +134 -0
  75. package/dist/components/editor-root-focus.d.ts +19 -0
  76. package/dist/components/editor-root-focus.js +67 -0
  77. package/dist/components/editor-root-geometry.d.ts +39 -0
  78. package/dist/components/editor-root-geometry.js +91 -0
  79. package/dist/components/editor-root-keydown.d.ts +1 -1
  80. package/dist/components/editor-root-keydown.js +12 -3
  81. package/dist/components/editor-root-listeners.d.ts +3 -6
  82. package/dist/components/editor-root-listeners.js +3 -24
  83. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  84. package/dist/components/editor-root-mode-flip.js +92 -0
  85. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  86. package/dist/components/image/ImageProperties.svelte +512 -70
  87. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  88. package/dist/components/image/ImageResizeHandles.svelte +67 -34
  89. package/dist/components/image/image-crop.d.ts +39 -0
  90. package/dist/components/image/image-crop.js +74 -0
  91. package/dist/components/image/image-edit-commit.d.ts +1 -0
  92. package/dist/components/image/image-edit-commit.js +21 -5
  93. package/dist/components/image/image-source-bytes.js +10 -3
  94. package/dist/components/image/image-widget-editing.js +1 -0
  95. package/dist/components/image/widget-dom.js +5 -1
  96. package/dist/components/link-card/link-card-commit.js +1 -1
  97. package/dist/components/lrd-map-gate.js +1 -1
  98. package/dist/components/menu/BlockMenu.svelte +315 -0
  99. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  100. package/dist/components/menu/MenuIcon.svelte +153 -0
  101. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  102. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  103. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  104. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  105. package/dist/components/menu/clipboard-actions.js +42 -0
  106. package/dist/components/menu/default-context-actions.d.ts +15 -0
  107. package/dist/components/menu/default-context-actions.js +76 -0
  108. package/dist/components/menu/flyout-placement.d.ts +6 -0
  109. package/dist/components/menu/flyout-placement.js +25 -0
  110. package/dist/core/inline/format-toggle.d.ts +12 -4
  111. package/dist/core/inline/format-toggle.js +94 -40
  112. package/dist/core/inline/image-dimensions.d.ts +3 -0
  113. package/dist/core/inline/image-dimensions.js +46 -10
  114. package/dist/core/inline/inline-widgets.d.ts +19 -0
  115. package/dist/core/inline/inline-widgets.js +5 -0
  116. package/dist/core/inline/scan/brackets.js +1 -0
  117. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  118. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  119. package/dist/core/inline/transparency.js +3 -3
  120. package/dist/core/inline-render.d.ts +6 -0
  121. package/dist/core/inline-render.js +32 -0
  122. package/dist/core/nodes.d.ts +14 -0
  123. package/dist/cursor/edge-affinity.js +2 -1
  124. package/dist/cursor/height-oracle.d.ts +2 -3
  125. package/dist/cursor/height-oracle.js +0 -1
  126. package/dist/cursor/overlay-remeasure.js +8 -0
  127. package/dist/cursor/reveal-source.js +7 -2
  128. package/dist/cursor/scroll-hold.d.ts +10 -0
  129. package/dist/cursor/scroll-hold.js +22 -0
  130. package/dist/cursor/scrollport.d.ts +9 -0
  131. package/dist/cursor/scrollport.js +30 -1
  132. package/dist/cursor/visual-lines.d.ts +5 -4
  133. package/dist/cursor/visual-lines.js +56 -11
  134. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  135. package/dist/cursor/widget-edge-snap.js +38 -0
  136. package/dist/cursor/widget-offset.d.ts +9 -0
  137. package/dist/cursor/widget-offset.js +71 -4
  138. package/dist/debug/interaction-trace.d.ts +4 -0
  139. package/dist/debug/interaction-trace.js +15 -0
  140. package/dist/decorations/decoration-state.svelte.js +1 -1
  141. package/dist/decorations/reserved-attrs.js +1 -0
  142. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  143. package/dist/editor-actions/ancestry-folds.js +1 -1
  144. package/dist/editor-actions/block-edit-scope.js +1 -1
  145. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  146. package/dist/editor-actions/commit/text-batch.js +1 -1
  147. package/dist/editor-actions/commit/undo-controller.js +4 -2
  148. package/dist/editor-actions/container-edit.js +2 -1
  149. package/dist/editor-actions/enter-completion.d.ts +2 -0
  150. package/dist/editor-actions/enter-completion.js +23 -2
  151. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  152. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  153. package/dist/editor-actions/focus/focus-landing.js +20 -6
  154. package/dist/editor-actions/inline-range-commit.js +1 -1
  155. package/dist/editor-actions/plugin/container.d.ts +7 -0
  156. package/dist/editor-actions/plugin/container.js +3 -1
  157. package/dist/editor-actions/reorder-action.js +19 -10
  158. package/dist/editor-actions/reorder-drag.js +29 -1
  159. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  160. package/dist/editor-actions/replacement-focus.js +1 -1
  161. package/dist/editor-actions/search-replace.js +1 -1
  162. package/dist/editor-actions/table-context.d.ts +4 -1
  163. package/dist/editor-actions/table-context.js +57 -1
  164. package/dist/editor-events.d.ts +3 -0
  165. package/dist/editor-keys.d.ts +33 -0
  166. package/dist/editor-props.d.ts +24 -12
  167. package/dist/index.d.ts +1 -1
  168. package/dist/plugin.d.ts +7 -0
  169. package/dist/plugin.js +16 -0
  170. package/dist/plugins/latex/BlockMath.svelte +264 -29
  171. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  172. package/dist/plugins/latex/index.d.ts +2 -1
  173. package/dist/plugins/latex/latex-kind.js +60 -10
  174. package/dist/plugins/latex/math-completion.js +4 -1
  175. package/dist/plugins/latex/math-layout.d.ts +15 -0
  176. package/dist/plugins/latex/math-layout.js +13 -0
  177. package/dist/plugins/latex/math-source.d.ts +19 -0
  178. package/dist/plugins/latex/math-source.js +103 -0
  179. package/dist/plugins/latex/register.d.ts +11 -2
  180. package/dist/plugins/latex/register.js +3 -1
  181. package/dist/plugins/latex/renderer.d.ts +3 -3
  182. package/dist/plugins/latex/renderer.js +13 -6
  183. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  184. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  185. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  186. package/dist/reactivity/list-windowing.svelte.js +75 -30
  187. package/dist/schema/block-completions.d.ts +8 -0
  188. package/dist/schema/block-completions.js +11 -0
  189. package/dist/schema/commands.d.ts +9 -5
  190. package/dist/schema/commands.js +12 -7
  191. package/dist/schema/context-actions.d.ts +31 -0
  192. package/dist/schema/context-actions.js +23 -0
  193. package/dist/schema/fenced-code-raw.js +31 -1
  194. package/dist/schema/operations.d.ts +8 -1
  195. package/dist/schema/reserved-chords.js +37 -4
  196. package/dist/schema/table-cell-raw.d.ts +1 -1
  197. package/dist/schema/table-cell-raw.js +1 -1
  198. package/dist/selection/block-hit-test.js +3 -2
  199. package/dist/selection/char-endpoint-snap.js +1 -1
  200. package/dist/selection/clipboard-text.js +6 -1
  201. package/dist/selection/covered-block.d.ts +10 -0
  202. package/dist/selection/covered-block.js +24 -0
  203. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  204. package/dist/selection/cross-block/dispatch.js +13 -1
  205. package/dist/selection/cross-block/format-range.d.ts +1 -1
  206. package/dist/selection/cross-block/format-range.js +4 -15
  207. package/dist/selection/cross-block/format-toggle.js +1 -1
  208. package/dist/selection/cross-block/keydown.js +3 -29
  209. package/dist/selection/cross-block/ops.js +1 -1
  210. package/dist/selection/cross-block/paste.js +31 -35
  211. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  212. package/dist/selection/cross-block/type-replace.js +52 -13
  213. package/dist/selection/dead-space-caret.d.ts +13 -0
  214. package/dist/selection/dead-space-caret.js +57 -1
  215. package/dist/selection/drag-pointer.d.ts +26 -2
  216. package/dist/selection/drag-pointer.js +103 -2
  217. package/dist/selection/gap-caret.js +1 -1
  218. package/dist/selection/keyboard-extend.d.ts +3 -2
  219. package/dist/selection/keyboard-extend.js +4 -3
  220. package/dist/selection/multi-click.d.ts +42 -0
  221. package/dist/selection/multi-click.js +203 -0
  222. package/dist/selection/native-bridge.d.ts +3 -0
  223. package/dist/selection/native-bridge.js +31 -2
  224. package/dist/selection/path-lookup.d.ts +10 -2
  225. package/dist/selection/path-lookup.js +11 -4
  226. package/dist/selection/pointer-gesture.d.ts +9 -0
  227. package/dist/selection/pointer-gesture.js +11 -0
  228. package/dist/selection/primitives.d.ts +7 -0
  229. package/dist/selection/primitives.js +19 -1
  230. package/dist/selection/range-delete-ceremony.js +4 -2
  231. package/dist/selection/range-delete-chrome.js +2 -1
  232. package/dist/selection/range-delete-table-coverage.js +2 -1
  233. package/dist/selection/range-delete-table.js +3 -2
  234. package/dist/selection/range-delete.d.ts +4 -0
  235. package/dist/selection/range-delete.js +31 -3
  236. package/dist/selection/selection-drop.d.ts +34 -0
  237. package/dist/selection/selection-drop.js +253 -0
  238. package/dist/selection/selection-restore.js +1 -1
  239. package/dist/selection/selection-state.svelte.d.ts +6 -0
  240. package/dist/selection/selection-state.svelte.js +36 -1
  241. package/dist/selection/table-endpoint-snap.js +1 -1
  242. package/dist/selection/table-rect-extend.js +1 -1
  243. package/dist/styles/editor-theme.css +75 -31
  244. package/dist/styles/editor.css +268 -18
  245. package/dist/testing/container-conformance.js +2 -2
  246. package/dist/testing/inline-conformance.js +2 -1
  247. package/dist/tree-operations/blockquote.js +1 -1
  248. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  249. package/dist/tree-operations/chain-rebuild.js +142 -0
  250. package/dist/tree-operations/children.d.ts +1 -1
  251. package/dist/tree-operations/children.js +1 -1
  252. package/dist/tree-operations/cleanup.js +1 -1
  253. package/dist/tree-operations/content-write.d.ts +50 -0
  254. package/dist/tree-operations/content-write.js +263 -0
  255. package/dist/tree-operations/index.d.ts +8 -3
  256. package/dist/tree-operations/index.js +6 -2
  257. package/dist/tree-operations/list/exit-replacement.js +1 -1
  258. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  259. package/dist/tree-operations/node-ops.d.ts +17 -234
  260. package/dist/tree-operations/node-ops.js +47 -1113
  261. package/dist/tree-operations/node-primitives.d.ts +75 -0
  262. package/dist/tree-operations/node-primitives.js +117 -0
  263. package/dist/tree-operations/paste/apply.js +1 -1
  264. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  265. package/dist/tree-operations/paste/body-write.js +2 -2
  266. package/dist/tree-operations/paste/container-match.js +4 -2
  267. package/dist/tree-operations/paste/dispatch.js +2 -1
  268. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  269. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  270. package/dist/tree-operations/paste/list-absorb.js +1 -1
  271. package/dist/tree-operations/paste/list-break-out.js +1 -1
  272. package/dist/tree-operations/paste/parent-scope.js +1 -1
  273. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  274. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  275. package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
  276. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  277. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  278. package/dist/tree-operations/path-mutate.d.ts +1 -1
  279. package/dist/tree-operations/path-mutate.js +2 -1
  280. package/dist/tree-operations/reorder-unit.js +1 -1
  281. package/dist/tree-operations/reorder.d.ts +5 -2
  282. package/dist/tree-operations/reorder.js +55 -2
  283. package/dist/tree-operations/settle.d.ts +105 -0
  284. package/dist/tree-operations/settle.js +660 -0
  285. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  286. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  287. package/dist/tree-operations/unshare.d.ts +15 -77
  288. package/dist/tree-operations/unshare.js +15 -166
  289. package/docs/guide/consumer-guide.md +136 -104
  290. package/docs/guide/plugin-api.md +73 -29
  291. package/docs/guide/plugin-guide.md +55 -23
  292. package/package.json +9 -7
  293. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  294. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  295. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  296. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  297. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  298. package/dist/components/blocks/table/table-drop-target.js +0 -16
  299. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  300. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Scroll-position hold across a block's render swap. A swap that shortens a block clamps the
3
+ * scrollport against the transient layout on the way to its settled height, and the clamp is
4
+ * never given back. A reveal in flight outranks the hold and owns the position; host mode does
5
+ * not, since native anchoring cannot undo a max-scroll clamp. Only a measure-then-fit surface
6
+ * needs this, and today that is one: an editable leaf's source is content-sized at mount.
7
+ */
8
+ import { tick } from 'svelte';
9
+ import { userScrollportFor } from './scroll-ancestors';
10
+ import { createScrollport } from './scrollport';
11
+ /** Capture the position of whatever scrolls `el`; await the result AFTER the state flip that
12
+ * changes the block's height. */
13
+ export function captureScrollPosition(el, isRevealInFlight) {
14
+ const port = el ? createScrollport(userScrollportFor(el)) : null;
15
+ const before = port?.scrollTop() ?? 0;
16
+ return async () => {
17
+ await tick();
18
+ if (!port || isRevealInFlight() || port.scrollTop() === before)
19
+ return;
20
+ port.setScrollTop(before);
21
+ };
22
+ }
@@ -14,7 +14,16 @@ export interface Scrollport {
14
14
  contentWidth(): number;
15
15
  scrollTop(): number;
16
16
  setScrollTop(value: number): void;
17
+ /** Move by `delta`. The only relative write: see {@link withRelativeScroll}. */
18
+ scrollBy(delta: number): void;
17
19
  /** Fires on user and programmatic scrolls alike; returns the unsubscribe. */
18
20
  subscribe(onScroll: () => void): () => void;
19
21
  }
20
22
  export declare function createScrollport(target: UserScrollport): Scrollport;
23
+ /**
24
+ * Adds the relative write every corrector goes through. A scroller snaps a fractional write to
25
+ * a whole device pixel and reports the snapped value back, so a run of relative corrections (a
26
+ * mode flip fires one per re-measured block) would drop that fraction every time and slide the
27
+ * reader's content by the sum. The refused fraction carries into the next call instead.
28
+ */
29
+ export declare function withRelativeScroll(base: Omit<Scrollport, 'scrollBy'>): Scrollport;
@@ -1,5 +1,34 @@
1
1
  export function createScrollport(target) {
2
- return target === window ? pageScrollport() : elementScrollport(target);
2
+ return withRelativeScroll(target === window ? pageScrollport() : elementScrollport(target));
3
+ }
4
+ /**
5
+ * Adds the relative write every corrector goes through. A scroller snaps a fractional write to
6
+ * a whole device pixel and reports the snapped value back, so a run of relative corrections (a
7
+ * mode flip fires one per re-measured block) would drop that fraction every time and slide the
8
+ * reader's content by the sum. The refused fraction carries into the next call instead.
9
+ */
10
+ export function withRelativeScroll(base) {
11
+ let carried = 0;
12
+ let written = null;
13
+ return {
14
+ ...base,
15
+ setScrollTop(value) {
16
+ base.setScrollTop(value);
17
+ carried = 0;
18
+ written = null;
19
+ },
20
+ scrollBy(delta) {
21
+ const from = base.scrollTop();
22
+ // Anything that moved the port since our own write (the reader, a reveal) leaves the
23
+ // carried fraction describing a position nobody holds any more.
24
+ const target = from + (written === from ? carried : 0) + delta;
25
+ base.setScrollTop(target);
26
+ written = base.scrollTop();
27
+ const refused = target - written;
28
+ // Only the snap's own fraction carries; a clamp at either end is a real refusal.
29
+ carried = Math.abs(refused) < 1 ? refused : 0;
30
+ }
31
+ };
3
32
  }
4
33
  // ── Internal ───────────────────────────────────────────────────────────────
5
34
  function elementScrollport(el) {
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Whether the cursor sits on the first or last visual line of a wrapping element. Offsets
3
- * alone can't answer it past 2 wrapped lines, so cursor Y is compared to the first/last
4
- * line's Y. The text-node walk works around collapsed ranges next to non-text children
5
- * (dimmed marker spans) returning null rects — measuring around real text always works.
2
+ * Whether the cursor sits on the first or last visual line of a wrapping element. Offsets alone
3
+ * can't answer it past 2 wrapped lines, so the cursor's line is compared to the edge line's.
4
+ * Collapsed ranges beside non-text children (dimmed markers, atomic islands) measure to nothing,
5
+ * so the edge line is measured around real text, and a rect-less caret reads the box it sits
6
+ * against.
6
7
  */
7
8
  /** The first rect that can position a caret: the leading client rect when it has real
8
9
  * height, else the bounding rect. `widthTolerant` also accepts a zero-height rect with width. */
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Whether the cursor sits on the first or last visual line of a wrapping element. Offsets
3
- * alone can't answer it past 2 wrapped lines, so cursor Y is compared to the first/last
4
- * line's Y. The text-node walk works around collapsed ranges next to non-text children
5
- * (dimmed marker spans) returning null rects — measuring around real text always works.
2
+ * Whether the cursor sits on the first or last visual line of a wrapping element. Offsets alone
3
+ * can't answer it past 2 wrapped lines, so the cursor's line is compared to the edge line's.
4
+ * Collapsed ranges beside non-text children (dimmed markers, atomic islands) measure to nothing,
5
+ * so the edge line is measured around real text, and a rect-less caret reads the box it sits
6
+ * against.
6
7
  */
7
8
  import { domDescendants } from './dom-walk';
8
9
  import { FALLBACK_LINE_HEIGHT } from './typography-estimates';
@@ -61,6 +62,7 @@ export function findLastTextNode(root) {
61
62
  export function isAtFirstVisualLine(el, fallbackOffset, contentStart) {
62
63
  return isAtEdgeVisualLine(el, () => fallbackOffset <= contentStart, {
63
64
  isEmpty: (el.textContent ?? '').length === 0,
65
+ toStart: true,
64
66
  boundaryTop: () => {
65
67
  const firstText = findFirstTextNode(el);
66
68
  const top = firstText ? getCharRangeTop(firstText, 0, false) : null;
@@ -71,6 +73,7 @@ export function isAtFirstVisualLine(el, fallbackOffset, contentStart) {
71
73
  export function isAtLastVisualLine(el, fallbackOffset, contentEnd) {
72
74
  return isAtEdgeVisualLine(el, () => fallbackOffset >= contentEnd, {
73
75
  isEmpty: contentEnd === 0,
76
+ toStart: false,
74
77
  boundaryTop: () => {
75
78
  const lastText = findLastTextNode(el);
76
79
  const top = lastText ? getCharRangeTop(lastText, lastText.textContent.length, true) : null;
@@ -80,7 +83,7 @@ export function isAtLastVisualLine(el, fallbackOffset, contentEnd) {
80
83
  }
81
84
  // ── Internal ────────────────────────────────────────────────────────────────
82
85
  /** The shared skeleton of the two edge predicates: `fallback` answers where geometry cannot — a
83
- * dropped range, an unmeasurable collapsed caret, an unmeasurable boundary line — and
86
+ * dropped range, a caret no box can be found for, an unmeasurable boundary line — and
84
87
  * `boundaryTop` measures the edge line each side's own way. */
85
88
  function isAtEdgeVisualLine(el, fallback, edge) {
86
89
  const sel = window.getSelection();
@@ -89,19 +92,61 @@ function isAtEdgeVisualLine(el, fallback, edge) {
89
92
  if (edge.isEmpty)
90
93
  return true;
91
94
  const cursorRange = sel.getRangeAt(0);
95
+ const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || FALLBACK_LINE_HEIGHT;
96
+ const tolerance = lineHeight * SAME_LINE_TOLERANCE;
92
97
  const cursorTop = getRangeTop(cursorRange);
93
- if (cursorTop === null)
94
- return cursorRange.collapsed ? fallback() : true;
98
+ if (cursorTop === null) {
99
+ if (!cursorRange.collapsed)
100
+ return true;
101
+ // A caret beside an atomic island sits at an element-level position and measures to no rect
102
+ // of its own: it rides the island's box, and is at the edge line when nothing reaches past.
103
+ const band = neighbourBand(cursorRange);
104
+ const contents = bandOfRange(contentsRange(el));
105
+ if (!band || !contents)
106
+ return fallback();
107
+ return edge.toStart
108
+ ? band.top < contents.top + tolerance
109
+ : band.bottom > contents.bottom - tolerance;
110
+ }
95
111
  const edgeTop = edge.boundaryTop();
96
112
  if (edgeTop === null)
97
113
  return fallback();
98
- const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || FALLBACK_LINE_HEIGHT;
99
- return Math.abs(cursorTop - edgeTop) < lineHeight * SAME_LINE_TOLERANCE;
114
+ return Math.abs(cursorTop - edgeTop) < tolerance;
100
115
  }
101
- /** The boundary line's collapsed-contents fallback measurement. */
102
- function collapsedContentsTop(el, toStart) {
116
+ /** The vertical band of the box a rect-less caret sits against: the child it precedes, else the
117
+ * one it follows. Null where the caret is not at an element-level position. */
118
+ function neighbourBand(range) {
119
+ const container = range.startContainer;
120
+ if (container.nodeType !== Node.ELEMENT_NODE)
121
+ return null;
122
+ const children = container.childNodes;
123
+ return (nodeBand(children[range.startOffset], false) ?? nodeBand(children[range.startOffset - 1], true));
124
+ }
125
+ function nodeBand(node, fromEnd) {
126
+ if (!node)
127
+ return null;
128
+ const range = document.createRange();
129
+ range.selectNode(node);
130
+ // A node that wraps has one rect per line, and the caret touches the line on its own side.
131
+ const rects = range.getClientRects();
132
+ if (rects.length === 0)
133
+ return bandOfRange(range);
134
+ return bandOf(rects[fromEnd ? rects.length - 1 : 0]);
135
+ }
136
+ function contentsRange(el) {
103
137
  const range = document.createRange();
104
138
  range.selectNodeContents(el);
139
+ return range;
140
+ }
141
+ function bandOfRange(range) {
142
+ return bandOf(range.getBoundingClientRect());
143
+ }
144
+ function bandOf(rect) {
145
+ return rect.height > 0 ? { top: rect.top, bottom: rect.bottom } : null;
146
+ }
147
+ /** The boundary line's collapsed-contents fallback measurement. */
148
+ function collapsedContentsTop(el, toStart) {
149
+ const range = contentsRange(el);
105
150
  range.collapse(toStart);
106
151
  return getRangeTop(range);
107
152
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Which atomic island's raw edge a point lands on: the NEAREST edge among the islands the caller
3
+ * measures, so a run of flush islands answers the one the point is actually beside. A point inside
4
+ * one reads its kind: a character-like island names the edge on the point's side, while an island
5
+ * that selects whole declines and keeps its own click handling.
6
+ */
7
+ export interface WidgetEdgeCandidate {
8
+ /** The island's own raw source span; the caret seats at one end or the other. */
9
+ start: number;
10
+ end: number;
11
+ rect: Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
12
+ /** The kind reads as one character, so a press ON it names an edge by side rather than
13
+ * selecting the island whole. */
14
+ seatsInside: boolean;
15
+ }
16
+ export interface WidgetEdgeSeat {
17
+ offset: number;
18
+ /** The press landed ON the island: the engine answers such a point with a position in the
19
+ * neighbouring text, so its own caret is no reason to stand this seat down. */
20
+ inside: boolean;
21
+ }
22
+ /**
23
+ * The raw offset a point snaps to, in document order for ties. Null where the point snaps to none:
24
+ * inside an island that selects whole, whose own click handling owns it, or with no island to
25
+ * either side. A null `y` is a point with no line to compare against, so vertical distance drops
26
+ * out and horizontal containment alone reads as inside.
27
+ */
28
+ export declare function nearestWidgetEdgeSeat(candidates: Iterable<WidgetEdgeCandidate>, x: number, y: number | null): WidgetEdgeSeat | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Which atomic island's raw edge a point lands on: the NEAREST edge among the islands the caller
3
+ * measures, so a run of flush islands answers the one the point is actually beside. A point inside
4
+ * one reads its kind: a character-like island names the edge on the point's side, while an island
5
+ * that selects whole declines and keeps its own click handling.
6
+ */
7
+ /**
8
+ * The raw offset a point snaps to, in document order for ties. Null where the point snaps to none:
9
+ * inside an island that selects whole, whose own click handling owns it, or with no island to
10
+ * either side. A null `y` is a point with no line to compare against, so vertical distance drops
11
+ * out and horizontal containment alone reads as inside.
12
+ */
13
+ export function nearestWidgetEdgeSeat(candidates, x, y) {
14
+ let best = null;
15
+ for (const { start, end, rect, seatsInside } of candidates) {
16
+ const withinRow = y === null || (rect.top < y && y < rect.bottom);
17
+ if (rect.left < x && x < rect.right && withinRow) {
18
+ if (!seatsInside)
19
+ return null;
20
+ return { offset: x < (rect.left + rect.right) / 2 ? start : end, inside: true };
21
+ }
22
+ // Rows before columns: a point past the end of one line must not reach an island on
23
+ // another line that happens to sit at the same x.
24
+ const rowGap = y === null ? 0 : Math.max(rect.top - y, y - rect.bottom, 0);
25
+ if (x <= rect.left)
26
+ best = nearer(best, { offset: start, rowGap, gap: rect.left - x });
27
+ if (x >= rect.right)
28
+ best = nearer(best, { offset: end, rowGap, gap: x - rect.right });
29
+ }
30
+ return best === null ? null : { offset: best.offset, inside: false };
31
+ }
32
+ function nearer(held, next) {
33
+ if (!held)
34
+ return next;
35
+ if (next.rowGap !== held.rowGap)
36
+ return next.rowGap < held.rowGap ? next : held;
37
+ return next.gap < held.gap ? next : held;
38
+ }
@@ -42,6 +42,9 @@ export declare function findDomTextOffsetTarget(container: ParentNode, target: D
42
42
  * against the ambient-adjusted argument.
43
43
  */
44
44
  export declare function widgetsIntersectingRange(container: HTMLElement, start: DomTextOffset, end: DomTextOffset): HTMLElement[];
45
+ /** The walk's text with marker chrome and widget bytes blanked to spaces, at the walk's own
46
+ * offsets: what a rule reading words (the multi-click ladder) sees as content. */
47
+ export declare function maskedWalkText(container: ParentNode): string;
45
48
  /** Total walk length of `container` — its one-past-end walk position. */
46
49
  export declare function containerDomTextLength(container: ParentNode): DomTextOffset;
47
50
  /**
@@ -101,6 +104,12 @@ export declare function landableDomTextBounds(container: ParentNode): {
101
104
  * questions: a reference label is chrome that stays hidden, so it is not content standing behind
102
105
  * the stamp, and it is not the paint the stamp promises either.
103
106
  */
107
+ /**
108
+ * The container's bytes outside its marker chrome — what a painted source holds as CONTENT,
109
+ * whatever the mode paints. A fence with an empty body answers whitespace; the leaf reads that
110
+ * as "nothing here a Backspace could mean but the block".
111
+ */
112
+ export declare function chromeFreeText(container: ParentNode): string;
104
113
  export declare function holdsOnlyMarkerChrome(container: ParentNode): boolean;
105
114
  /**
106
115
  * Whether the mode paints NOTHING landable here: every walk segment is a hidden marker run, so no
@@ -85,8 +85,11 @@ export function findDomTextOffsetTarget(container, target) {
85
85
  const after = positionBeside(seg.last, 'after');
86
86
  if (!after)
87
87
  continue;
88
- if (seg.start + seg.len >= target)
88
+ // Chromium canonicalizes the slot after a HIDDEN run upstream across it, so a byte typed
89
+ // there lands before the run; text starting at the target is the position that keeps it.
90
+ if (seg.start + seg.len > target || (seg.start + seg.len === target && !seg.hidden)) {
89
91
  return after;
92
+ }
90
93
  last = after;
91
94
  }
92
95
  return last;
@@ -108,6 +111,16 @@ export function widgetsIntersectingRange(container, start, end) {
108
111
  }
109
112
  return out;
110
113
  }
114
+ /** The walk's text with marker chrome and widget bytes blanked to spaces, at the walk's own
115
+ * offsets: what a rule reading words (the multi-click ladder) sees as content. */
116
+ export function maskedWalkText(container) {
117
+ let out = '';
118
+ for (const seg of walkSegments(container, null)) {
119
+ const content = seg.kind === 'text' && markerRootOf(seg.node, container) === null;
120
+ out += content ? (seg.node.textContent ?? '') : ' '.repeat(seg.len);
121
+ }
122
+ return out;
123
+ }
111
124
  /** Total walk length of `container` — its one-past-end walk position. */
112
125
  export function containerDomTextLength(container) {
113
126
  let count = 0;
@@ -239,7 +252,19 @@ export function revealsNoMarkers(container) {
239
252
  * An unmounted surface reads as source, where nothing hides and no rewrite has a run to move.
240
253
  */
241
254
  export function screenVisibilityOf(container) {
242
- return screenVisibility(container === null ? 'source' : (markerHidingMode(container) ?? 'source'), { chromePaints: container instanceof Element && container.hasAttribute(CONTENT_EMPTY_ATTR) });
255
+ return screenVisibility(container === null ? 'source' : (markerHidingMode(container) ?? 'source'), { chromePaints: container instanceof Element && chromeStampPaints(container) });
256
+ }
257
+ /**
258
+ * The stamp's own condition, as `styles/editor.css` states it: the container carries
259
+ * `data-content-empty` AND holds focus. The seat is for a caret, so a stamped block nobody is in
260
+ * keeps its chrome hidden and reads as the empty construct it is. Focus containment rather than
261
+ * `:focus-within`, so a selector engine without that pseudo-class (jsdom's) reads the same answer.
262
+ */
263
+ function chromeStampPaints(container) {
264
+ if (!container.hasAttribute(CONTENT_EMPTY_ATTR))
265
+ return false;
266
+ const active = container.ownerDocument.activeElement;
267
+ return active !== null && container.contains(active);
243
268
  }
244
269
  /**
245
270
  * The extreme walk offsets a caret can occupy in `container`: a hidden marker run or the leading
@@ -252,6 +277,7 @@ export function landableDomTextBounds(container) {
252
277
  let start = 0;
253
278
  let end = 0;
254
279
  let landed = false;
280
+ let lastText = null;
255
281
  for (const seg of landingSegments(container, markerHidingMode(container))) {
256
282
  if (seg.len === 0)
257
283
  continue;
@@ -265,9 +291,28 @@ export function landableDomTextBounds(container) {
265
291
  }
266
292
  landed = true;
267
293
  end = stop;
294
+ lastText = seg.kind === 'text' ? seg.node : null;
295
+ }
296
+ // The position after the last landable run's final `\n` is the start of a line nothing
297
+ // paints: the engine seats no caret there, so an end gate reading it never fires and the
298
+ // caret is stuck on the empty line. A caret anchor gives that line its paint and keeps it.
299
+ if (lastText?.textContent?.endsWith('\n') && !followedByCaretAnchor(lastText, container)) {
300
+ end -= 1;
268
301
  }
269
302
  return { start: asDomTextOffset(start), end: asDomTextOffset(Math.max(start, end)) };
270
303
  }
304
+ /** Whether the next painted node after `text`, at any depth above it, is a caret anchor. */
305
+ function followedByCaretAnchor(text, root) {
306
+ for (let node = text; node && node !== root; node = node.parentNode) {
307
+ let next = node.nextSibling;
308
+ while (next?.nodeType === Node.TEXT_NODE && (next.textContent?.length ?? 0) === 0) {
309
+ next = next.nextSibling;
310
+ }
311
+ if (next)
312
+ return next instanceof HTMLElement && next.tagName === 'BR' && 'caretAnchor' in next.dataset;
313
+ }
314
+ return false;
315
+ }
271
316
  /**
272
317
  * Whether every byte `container` holds is marker chrome, at least one span of it a family the
273
318
  * empty-construct override paints. The stamp condition the render path writes as
@@ -276,6 +321,22 @@ export function landableDomTextBounds(container) {
276
321
  * questions: a reference label is chrome that stays hidden, so it is not content standing behind
277
322
  * the stamp, and it is not the paint the stamp promises either.
278
323
  */
324
+ /**
325
+ * The container's bytes outside its marker chrome — what a painted source holds as CONTENT,
326
+ * whatever the mode paints. A fence with an empty body answers whitespace; the leaf reads that
327
+ * as "nothing here a Backspace could mean but the block".
328
+ */
329
+ export function chromeFreeText(container) {
330
+ let text = '';
331
+ for (const seg of walkSegments(container, null)) {
332
+ if (seg.kind !== 'text' || seg.len === 0)
333
+ continue;
334
+ if (markerRootOf(seg.node, container) !== null)
335
+ continue;
336
+ text += seg.node.textContent ?? '';
337
+ }
338
+ return text;
339
+ }
279
340
  export function holdsOnlyMarkerChrome(container) {
280
341
  let chrome = false;
281
342
  for (const seg of walkSegments(container, null)) {
@@ -383,8 +444,7 @@ function markerRootOf(node, root) {
383
444
  function chromeStandsAloneUnder(root, mode) {
384
445
  if (mode === null || !(root instanceof Element))
385
446
  return false;
386
- return screenVisibility(mode, { chromePaints: root.hasAttribute(CONTENT_EMPTY_ATTR) })
387
- .chromePaints;
447
+ return screenVisibility(mode, { chromePaints: chromeStampPaints(root) }).chromePaints;
388
448
  }
389
449
  /**
390
450
  * Whether `node` is inside the leading ambient marker island — a container's `- ` / `1. ` prefix,
@@ -442,8 +502,15 @@ function positionBeside(el, side) {
442
502
  return { node: sibling, offset: side === 'before' ? (sibling.textContent?.length ?? 0) : 0 };
443
503
  }
444
504
  const idx = Array.prototype.indexOf.call(parent.childNodes, el);
505
+ // A pending hard break's first anchor (`inline-render.ts`) ends the line the marker sat on;
506
+ // the seat past the marker is the start of the next one, which is after that anchor.
507
+ if (side === 'after' && isBreakAnchor(sibling))
508
+ return { node: parent, offset: idx + 2 };
445
509
  return { node: parent, offset: side === 'before' ? idx : idx + 1 };
446
510
  }
511
+ function isBreakAnchor(node) {
512
+ return (node instanceof HTMLElement && node.tagName === 'BR' && node.dataset.caretAnchor === 'break');
513
+ }
447
514
  /**
448
515
  * The classification every walk-space consumer shares: a text node contributes its textContent
449
516
  * length, an atomic widget its raw source length (never descended), any other element is
@@ -17,6 +17,7 @@ export declare function disableInteractionTrace(): void;
17
17
  export declare function isInteractionTraceEnabled(): boolean;
18
18
  /** Empty the buffer without touching the enabled flag — test isolation. */
19
19
  export declare function resetInteractionTrace(): void;
20
+ export declare function interactionTraceKeydownCount(): number;
20
21
  export declare function interactionTraceSnapshot(): InteractionTraceEntry[];
21
22
  /** `changed` names the differing render-key segments, comma-joined. */
22
23
  export declare function traceRebuild(changed: string, force: boolean): void;
@@ -37,3 +38,6 @@ export declare function traceCompositionEnd(): void;
37
38
  export declare function traceIslandsApplied(count: number): void;
38
39
  export declare function traceStickyCapture(x: number): void;
39
40
  export declare function traceStickyReset(): void;
41
+ /** `handled` is the event's own `defaultPrevented`: whether the editor claimed the press.
42
+ * Recorded once per keydown, after the surface handler's await chain has settled. */
43
+ export declare function traceKeydownVerdict(key: string, handled: boolean): void;
@@ -8,6 +8,9 @@
8
8
  const CAPACITY = 200;
9
9
  let enabled = false;
10
10
  let buf = [];
11
+ // Monotonic, so the harness poll costs one read and the ring buffer's eviction cannot
12
+ // retract a verdict it already counted.
13
+ let keydownVerdicts = 0;
11
14
  // ── Switch and readout ──────────────────────────────────────────────────────
12
15
  export function enableInteractionTrace() {
13
16
  enabled = true;
@@ -21,6 +24,10 @@ export function isInteractionTraceEnabled() {
21
24
  /** Empty the buffer without touching the enabled flag — test isolation. */
22
25
  export function resetInteractionTrace() {
23
26
  buf = [];
27
+ keydownVerdicts = 0;
28
+ }
29
+ export function interactionTraceKeydownCount() {
30
+ return keydownVerdicts;
24
31
  }
25
32
  export function interactionTraceSnapshot() {
26
33
  return buf.slice();
@@ -103,3 +110,11 @@ export function traceStickyReset() {
103
110
  return;
104
111
  record('sticky-column', 'reset');
105
112
  }
113
+ /** `handled` is the event's own `defaultPrevented`: whether the editor claimed the press.
114
+ * Recorded once per keydown, after the surface handler's await chain has settled. */
115
+ export function traceKeydownVerdict(key, handled) {
116
+ if (!enabled)
117
+ return;
118
+ keydownVerdicts++;
119
+ record('keydown', 'verdict', { key, handled });
120
+ }
@@ -5,7 +5,7 @@ import { islandPosition } from './island-dom';
5
5
  import { assertInvariant } from '../assert';
6
6
  import { isCommitInProgress } from '../invariants/commit-scope';
7
7
  import { contentLengthOf, isProseKind } from '../core/inline';
8
- import { isBlockNode, nodeAt } from '../tree-operations/node-ops';
8
+ import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
9
9
  import { devWarn } from '../dev-warn';
10
10
  import { recordDecorationRun } from '../perf/instruments';
11
11
  const EMPTY_MARKS = [];
@@ -18,6 +18,7 @@ export const RESERVED_BLOCK_ATTRS = new Set([
18
18
  'data-image-widget',
19
19
  'data-inline-widget',
20
20
  'data-link-card',
21
+ 'data-pointer-gesture',
21
22
  'data-presentation',
22
23
  'data-table-row-idx'
23
24
  ]);
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Publishing side of the ancestry settle (`tree-operations/unshare.ts`): a fold at a container's
2
+ * Publishing side of the ancestry settle (`tree-operations/chain-rebuild.ts`): a fold at a container's
3
3
  * own slot splices its PARENT's children, a scope the commit's own change descriptor never
4
4
  * covers, so that scope's ids and refs are resynced here instead.
5
5
  */
6
6
  import type { CstNode } from '../core/nodes';
7
- import type { AncestrySeamFold } from '../tree-operations/unshare';
7
+ import type { AncestrySeamFold } from '../tree-operations/chain-rebuild';
8
8
  import { type StructuralChange } from '../tree-operations/structural-change';
9
9
  import type { EditorActionsDeps } from './deps';
10
10
  /** Where the folded container's first byte ended up, as a document path. */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Publishing side of the ancestry settle (`tree-operations/unshare.ts`): a fold at a container's
2
+ * Publishing side of the ancestry settle (`tree-operations/chain-rebuild.ts`): a fold at a container's
3
3
  * own slot splices its PARENT's children, a scope the commit's own change descriptor never
4
4
  * covers, so that scope's ids and refs are resynced here instead.
5
5
  */
@@ -44,7 +44,7 @@ export function createTopLevelScope(deps, controller) {
44
44
  // ── Container adapter ────────────────────────────────────────────────────────
45
45
  export function createContainerScope(state, deps) {
46
46
  return {
47
- // A fold at this container's own slot detaches it (`tree-operations/unshare.ts`), so a
47
+ // A fold at this container's own slot detaches it (`tree-operations/chain-rebuild.ts`), so a
48
48
  // post-commit read can find the scope gone rather than merely empty.
49
49
  children: () => deps.node?.children ?? [],
50
50
  refAt: (i) => state.innerBlockRefs[i],
@@ -6,8 +6,9 @@
6
6
  export interface TextBatchDeps {
7
7
  /** Capture the pre-edit snapshot for the first keystroke of a batch. */
8
8
  pushSnapshot(leafPath: number[], offset: number): void;
9
- /** Emit the batched `input` edit event when a batch flushes. */
10
- emitInput(leafPath: number[], byteLength: number): void;
9
+ /** Emit the batched `input` edit event when a batch flushes. Omitted by a batch with no edit
10
+ * channel of its own: a revealed source's bytes reach it once, on the fold's commit. */
11
+ emitInput?(leafPath: number[], byteLength: number): void;
11
12
  }
12
13
  export interface TextBatch {
13
14
  /**
@@ -17,7 +17,7 @@ export function createTextBatch(deps) {
17
17
  */
18
18
  function flushPendingInput() {
19
19
  if (batchByteLength > 0 && batchPath) {
20
- deps.emitInput(batchPath, batchByteLength);
20
+ deps.emitInput?.(batchPath, batchByteLength);
21
21
  }
22
22
  batchPath = null;
23
23
  batchByteLength = 0;
@@ -12,8 +12,10 @@ import { assertInvariant } from '../../assert';
12
12
  import { beginCommit, endCommit } from '../../invariants/commit-scope';
13
13
  import { assignIds } from '../../block-id';
14
14
  import { replaceRefs } from '../../reactivity/publish-ref.svelte';
15
- import { nodeAt, settleSeparator } from '../../tree-operations/node-ops';
16
- import { attachedChainPrefix, ensureUnsharedPath, rebuildUnsharedChain } from '../../tree-operations/unshare';
15
+ import { nodeAt } from '../../tree-operations/node-primitives';
16
+ import { settleSeparator } from '../../tree-operations/settle';
17
+ import { ensureUnsharedPath } from '../../tree-operations/unshare';
18
+ import { attachedChainPrefix, rebuildUnsharedChain } from '../../tree-operations/chain-rebuild';
17
19
  import { foldLandingFor, publishAncestryFolds } from '../ancestry-folds';
18
20
  import { createTextBatch } from './text-batch';
19
21
  import { toEditEvent } from '../../editor-events';
@@ -2,7 +2,8 @@
2
2
  * ContainerEditActions factory: the debounced checkpoint pusher for raw typing
3
3
  * outside the commit primitive, the doc-root reactivity nudge, and `commitContainer`.
4
4
  */
5
- import { ensureUnsharedPath, rebuildUnsharedChain } from '../tree-operations/unshare';
5
+ import { ensureUnsharedPath } from '../tree-operations/unshare';
6
+ import { rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
6
7
  import { publishAncestryFolds, publishScopeFold } from './ancestry-folds';
7
8
  export function createContainerEditActions(deps, controller) {
8
9
  return {
@@ -23,3 +23,5 @@ export interface EnterCompletion {
23
23
  export declare function withEnterCompletion(blockEdit: BlockEditActions, childAt: (index: number) => NodeView | undefined): BlockEditActions;
24
24
  /** The completion a press at `offset` earns, or null when the block or the caret disqualifies it. */
25
25
  export declare function planEnterCompletion(node: NodeView | undefined, offset: number): EnterCompletion | null;
26
+ /** The completion a keystroke earns, consulting only the completers that answer on type. */
27
+ export declare function planTypedCompletion(node: NodeView | undefined, offset: number): EnterCompletion | null;
@@ -6,7 +6,7 @@
6
6
  import { parse } from '../core/parser';
7
7
  import { displayLength, splitLines, trailingLineEnding } from '../core/lines';
8
8
  import { getBlockKindDescriptor } from '../schema/block-kind-descriptor';
9
- import { completeTypedLine } from '../schema/block-completions';
9
+ import { completeLineOnType, completeTypedLine } from '../schema/block-completions';
10
10
  /**
11
11
  * Wraps a COMPOSED `splitBlock` with the consult, at the two bundle composition sites only. Above
12
12
  * the container overrides rather than inside the split body, so a container that replaces
@@ -26,17 +26,38 @@ export function withEnterCompletion(blockEdit, childAt) {
26
26
  // `snapshotOffset` is where the caret WAS, so one undo restores the typed line with the
27
27
  // caret at its end rather than in front of it.
28
28
  await blockEdit.replaceBlock(index, completion.replacement, { replacementIndex: 0, ...completion.caret }, { snapshotOffset: offset });
29
+ },
30
+ // The typed arm: a keystroke that leaves the block as a line an on-type completer claims
31
+ // (see `BlockCompleter.onType`) forms the structure at once, the way a typed ` ``` ` is a
32
+ // fence the moment the parser sees it. The write lands first, so the typed line is its own
33
+ // undo step and the mint replaces the bytes the CST actually holds.
34
+ async updateBlockContent(index, text, preEditOffset, postEditFocusOffset) {
35
+ await blockEdit.updateBlockContent(index, text, preEditOffset, postEditFocusOffset);
36
+ const offset = postEditFocusOffset ?? preEditOffset;
37
+ if (offset === undefined)
38
+ return;
39
+ const completion = planTypedCompletion(childAt(index), offset);
40
+ if (!completion)
41
+ return;
42
+ await blockEdit.replaceBlock(index, completion.replacement, { replacementIndex: 0, ...completion.caret }, { snapshotOffset: offset });
29
43
  }
30
44
  };
31
45
  }
32
46
  /** The completion a press at `offset` earns, or null when the block or the caret disqualifies it. */
33
47
  export function planEnterCompletion(node, offset) {
48
+ return planCompletion(node, offset, completeTypedLine);
49
+ }
50
+ /** The completion a keystroke earns, consulting only the completers that answer on type. */
51
+ export function planTypedCompletion(node, offset) {
52
+ return planCompletion(node, offset, completeLineOnType);
53
+ }
54
+ function planCompletion(node, offset, consult) {
34
55
  if (!node)
35
56
  return null;
36
57
  const line = wholeTypedLine(node);
37
58
  if (line === null || offset !== displayLength(node.raw))
38
59
  return null;
39
- const claim = completeTypedLine(line);
60
+ const claim = consult(line);
40
61
  if (!claim)
41
62
  return null;
42
63
  // Through the parser rather than a hand-built node, so the mint is exactly what a reload of