@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
@@ -1,5 +1,5 @@
1
1
  /** Document-tree path navigation. Pure functions over a Document + path. */
2
- import { nodeAt } from '../tree-operations/node-ops';
2
+ import { nodeAt } from '../tree-operations/node-primitives';
3
3
  /** Block immediately after `path` in doc order (children before siblings), else null. */
4
4
  export function nextPath(doc, path) {
5
5
  const node = nodeAt(doc, path);
@@ -125,8 +125,15 @@ export function findCellPathForElement(el) {
125
125
  return null;
126
126
  return [...tablePath, rowIdx, colIdx];
127
127
  }
128
- /** The path of the editing SURFACE `el` sits in — the enclosing cell where there is one, else
129
- * the enclosing block. The pairing {@link findCellPathForElement} says every producer owes. */
128
+ /** The pairing {@link findCellPathForElement} says every producer owes, in one place. */
129
+ export function findSurfaceForElement(el) {
130
+ const cellPath = findCellPathForElement(el);
131
+ if (cellPath)
132
+ return { path: cellPath, inCell: true };
133
+ const blockPath = findBlockPathForElement(el);
134
+ return blockPath ? { path: blockPath, inCell: false } : null;
135
+ }
136
+ /** {@link findSurfaceForElement} for a caller that needs only the path. */
130
137
  export function findSurfacePathForElement(el) {
131
- return findCellPathForElement(el) ?? findBlockPathForElement(el);
138
+ return findSurfaceForElement(el)?.path ?? null;
132
139
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The door a widget declares over an element whose pointer drags belong to its own gesture (a
3
+ * diagram's pan, a canvas's brush): the editor's pointer arms decline a press inside it, so the
4
+ * gesture never doubles as a block range. A declaration, not a `stopPropagation`: pointer events
5
+ * are delegated at the app root, so a component's own handler runs after the editor's.
6
+ */
7
+ /** Declared on the gesture surface itself; any descendant press counts. */
8
+ export declare const POINTER_GESTURE_ATTR = "data-pointer-gesture";
9
+ export declare function claimsPointerGesture(target: EventTarget | null): boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The door a widget declares over an element whose pointer drags belong to its own gesture (a
3
+ * diagram's pan, a canvas's brush): the editor's pointer arms decline a press inside it, so the
4
+ * gesture never doubles as a block range. A declaration, not a `stopPropagation`: pointer events
5
+ * are delegated at the app root, so a component's own handler runs after the editor's.
6
+ */
7
+ /** Declared on the gesture surface itself; any descendant press counts. */
8
+ export const POINTER_GESTURE_ATTR = 'data-pointer-gesture';
9
+ export function claimsPointerGesture(target) {
10
+ return target instanceof Element && target.closest(`[${POINTER_GESTURE_ATTR}]`) !== null;
11
+ }
@@ -73,3 +73,10 @@ export type BlockSelectionClass = 'outside' | 'start' | 'middle' | 'end' | 'sing
73
73
  * tells the caller to delegate to the browser instead of painting.
74
74
  */
75
75
  export declare function classifyBlockForSelection(path: number[], selection: EditorSelection): BlockSelectionClass;
76
+ /**
77
+ * Whether this block paints the range over its whole box: the range holds its entire subtree and
78
+ * no ancestor's box already covers it, or it is the whole unit a single-block range holds. Chrome
79
+ * a container derives from its markers (a GitHub alert's badge) has no child host to paint it, so
80
+ * the covering block takes the box in one piece and its children paint nothing.
81
+ */
82
+ export declare function blockPaintsWholeBox(path: readonly number[], selection: EditorSelection, wholeUnitPath: readonly number[] | null): boolean;
@@ -3,7 +3,7 @@
3
3
  * classification, the delete-commit snapshot rule. No DOM, no state. Path-level predicates
4
4
  * live in `./path-math`.
5
5
  */
6
- import { comparePaths, isPathBetween, pathHasPrefix } from './path-math';
6
+ import { comparePaths, isPathBetween, isStrictAncestorOf, pathHasPrefix, pathsEqual } from './path-math';
7
7
  import { asCellIndex, asRawOffset, docPathFrom } from '../cursor/coordinate-spaces';
8
8
  import { devWarn } from '../dev-warn';
9
9
  export function isWholeBlockEndpoint(endpoint) {
@@ -89,3 +89,21 @@ export function classifyBlockForSelection(path, selection) {
89
89
  return 'middle';
90
90
  return 'outside';
91
91
  }
92
+ /**
93
+ * Whether this block paints the range over its whole box: the range holds its entire subtree and
94
+ * no ancestor's box already covers it, or it is the whole unit a single-block range holds. Chrome
95
+ * a container derives from its markers (a GitHub alert's badge) has no child host to paint it, so
96
+ * the covering block takes the box in one piece and its children paint nothing.
97
+ */
98
+ export function blockPaintsWholeBox(path, selection, wholeUnitPath) {
99
+ if (wholeUnitPath)
100
+ return pathsEqual(path, wholeUnitPath);
101
+ const { start, end } = normalize(selection);
102
+ return (holdsSubtree(path, start.path, end.path) &&
103
+ !holdsSubtree(path.slice(0, -1), start.path, end.path));
104
+ }
105
+ /** The range holds this block's whole subtree: inside it in document order, and not an ancestor
106
+ * of the end endpoint, whose own descendants the range cuts through. */
107
+ function holdsSubtree(path, start, end) {
108
+ return isPathBetween(path, start, end) && !isStrictAncestorOf(path, end);
109
+ }
@@ -10,8 +10,10 @@ import { charOffsetOf, walkBetween } from './primitives';
10
10
  import { comparePaths, isStrictAncestorOf, isPathSubtreeBetween, lowestCommonAncestor, pathHasPrefix, pathsEqual } from './path-math';
11
11
  import { cascadeCleanupEmptyAncestors } from '../tree-operations/cleanup';
12
12
  import { deleteAtPath, replaceAtPath } from '../tree-operations/path-mutate';
13
- import { blockNodeAt, cleanJoinedRaw, emptyParagraph, nodeAt, normalizeOwnRaw } from '../tree-operations/node-ops';
14
- import { ensureUnsharedPath, rebuildUnsharedAncestry, rebuildUnsharedChain } from '../tree-operations/unshare';
13
+ import { blockNodeAt, emptyParagraph, nodeAt, normalizeOwnRaw } from '../tree-operations/node-primitives';
14
+ import { cleanJoinedRaw } from '../tree-operations/node-ops';
15
+ import { ensureUnsharedPath } from '../tree-operations/unshare';
16
+ import { rebuildUnsharedAncestry, rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
15
17
  // Wall primitives live with the chrome branch, which imports the atoms below back. The
16
18
  // resulting cycle is function-body-only (resolved at call time, never at module load), so it
17
19
  // is safe.
@@ -7,7 +7,8 @@
7
7
  import { displayLength } from '../core/lines';
8
8
  import { comparePaths, pathsEqual } from './path-math';
9
9
  import { resolveEndWall, planCrossBlockDeletion, applyPlannedDeletion, truncateEndInPlace, truncateStartInPlace } from './range-delete-ceremony';
10
- import { ensureUnsharedPath, rebuildUnsharedChain } from '../tree-operations/unshare';
10
+ import { ensureUnsharedPath } from '../tree-operations/unshare';
11
+ import { rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
11
12
  import { reservedChromeKindOf, isReservedChromeChild } from '../schema/reserved-chrome';
12
13
  // ── Public API ──────────────────────────────────────────────────────────────
13
14
  /**
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import { deleteSnapshot } from './primitives';
7
7
  import { metadataOf } from '../core/nodes';
8
- import { deleteNode, emptyParagraph } from '../tree-operations/node-ops';
8
+ import { emptyParagraph } from '../tree-operations/node-primitives';
9
+ import { deleteNode } from '../tree-operations/settle';
9
10
  import { trailingLineEnding } from '../core/lines';
10
11
  import { expectStateForNode, getStateForNode } from '../reactivity/state-registry';
11
12
  import { deleteRow as mutDeleteRow, deleteColumn as mutDeleteColumn, canDeleteRow, canDeleteColumn } from '../tree-operations/table-mutations';
@@ -11,8 +11,9 @@ import { cellRowCol } from '../cursor/coordinate-spaces';
11
11
  import { cellIndexOf } from './primitives';
12
12
  import { resolveEndWall, planCrossBlockDeletion, applyPlannedDeletion, rebuildSharedAncestries, truncateEndInPlace, truncateStartInPlace } from './range-delete-ceremony';
13
13
  import { comparePaths } from './path-math';
14
- import { blockNodeAt, emptyParagraph } from '../tree-operations/node-ops';
15
- import { ensureUnsharedNode, ensureUnsharedPath, ensureUnsharedSubtree, rebuildOwnedContainer, rebuildUnsharedAncestry } from '../tree-operations/unshare';
14
+ import { blockNodeAt, emptyParagraph } from '../tree-operations/node-primitives';
15
+ import { ensureUnsharedNode, ensureUnsharedPath, ensureUnsharedSubtree, rebuildOwnedContainer } from '../tree-operations/unshare';
16
+ import { rebuildUnsharedAncestry } from '../tree-operations/chain-rebuild';
16
17
  import { rebuildTableRowRaw } from '../schema/container-rebuilders';
17
18
  import { isCollapsedContainer } from '../schema/reserved-chrome';
18
19
  import { nearestChromeContainer, isChromeChild } from './range-delete-chrome';
@@ -31,3 +31,7 @@ export interface RangeDeleteResult {
31
31
  * post-unshare references. Caller pre-normalizes and keeps endpoints on focusable blocks.
32
32
  */
33
33
  export declare function rangeDelete(doc: Document, start: SelectionPoint, end: SelectionPoint, sharing: SharingState, grammar: GrammarView | undefined, presentationMode: PresentationMode | undefined, linkRef: InlineResolverRef | undefined): RangeDeleteResult;
34
+ /** The container prefix the survivor renders under, so the join seam can read its candidate back
35
+ * through it (live-mode.md § 4.5). An ambient marker rides the container's FIRST child only, the
36
+ * way `BlockList` forwards it, and a list item is the one built-in container that paints one. */
37
+ export declare function containerAmbientPrefix(doc: Document, path: readonly number[]): string;
@@ -7,11 +7,30 @@ import { metadataOf } from '../core/nodes';
7
7
  import { walkBetween, charOffsetOf } from './primitives';
8
8
  import { comparePaths, lowestCommonAncestor, isPathSubtreeBetween } from './path-math';
9
9
  import { firstLeafAtOrAfter } from './path-lookup';
10
- import { blockNodeAt, cleanJoinedRaw, nodeAt, normalizeBodyWrite, normalizeOwnRaw, settleSeparatorOnBlank, writeOwnRaw } from '../tree-operations/node-ops';
10
+ import { blockNodeAt, nodeAt, normalizeBodyWrite, normalizeOwnRaw, writeOwnRaw } from '../tree-operations/node-primitives';
11
+ import { settleSeparatorOnBlank } from '../tree-operations/settle';
12
+ import { isBlankParagraph } from '../core/parser';
13
+ import { displayLength } from '../core/lines';
14
+ import { deleteAtPath } from '../tree-operations/path-mutate';
15
+ import { cleanJoinedRaw } from '../tree-operations/node-ops';
11
16
  import { deleteSubtreesIdentityGated, installTruncatedEndpoint, reparseTruncatedEndpoint } from './range-delete-ceremony';
12
- import { ensureUnsharedNode, ensureUnsharedPath, rebuildUnsharedAncestry, rebuildUnsharedChain } from '../tree-operations/unshare';
17
+ import { ensureUnsharedNode, ensureUnsharedPath } from '../tree-operations/unshare';
18
+ import { rebuildUnsharedAncestry, rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
13
19
  import { involvesTable, tableAwareRangeDelete } from './range-delete-table';
14
20
  import { involvesReservedChrome, chromeAwareRangeDelete } from './range-delete-chrome';
21
+ /** Whoever takes the slot gets the caret, at its start; the block above when nothing does. */
22
+ function deleteWholeUnit(doc, path, sharing, grammar) {
23
+ const parentPath = path.slice(0, -1);
24
+ const index = path[path.length - 1];
25
+ // The path-addressed door, so the commit's id ledger sees the slot go.
26
+ const chain = ensureUnsharedPath(doc, parentPath, sharing);
27
+ deleteAtPath(doc, path, sharing);
28
+ if (chain.length > 0)
29
+ rebuildUnsharedChain(doc, chain, sharing, null, grammar);
30
+ const survivors = (chain.length > 0 ? chain[chain.length - 1] : doc).children ?? [];
31
+ const landing = Math.max(0, Math.min(index, survivors.length - 1));
32
+ return { newDoc: doc, collapsedCaret: { path: [...parentPath, landing], offset: 0 } };
33
+ }
15
34
  /**
16
35
  * Delete [start, end] in place, merge at start's position with its container context preserved,
17
36
  * cascade-clean empty ancestors, rebuild container raws. Copy-path-on-write: every spliced or
@@ -38,6 +57,15 @@ export function rangeDelete(doc, start, end, sharing, grammar, presentationMode,
38
57
  const endRaw = endBlock.raw;
39
58
  const startOffset = charOffsetOf(start, 'rangeDelete:prose-merge-start');
40
59
  const endOffset = charOffsetOf(end, 'rangeDelete:prose-merge-end');
60
+ // The range holds one block whole, so none of its own bytes survive: the byte arm below would
61
+ // keep a husk holding a bare line ending, which no reload reads as that kind. A paragraph is
62
+ // the one kind that survives empty, because a blank one IS the separating line below it.
63
+ if (sameBlock &&
64
+ startOffset === 0 &&
65
+ endOffset >= displayLength(startRaw) &&
66
+ !isBlankParagraph({ kind: startBlock.kind, raw: '' })) {
67
+ return deleteWholeUnit(doc, start.path, sharing, grammar);
68
+ }
41
69
  // The end slice answers to the END block's rule before the join: start's rule below speaks
42
70
  // only for start's bytes, so a truncation from the end block's head strands its closer. A
43
71
  // same-block merge is one block's bytes and takes that rule once, whole, on the arm below.
@@ -110,7 +138,7 @@ export function rangeDelete(doc, start, end, sharing, grammar, presentationMode,
110
138
  /** The container prefix the survivor renders under, so the join seam can read its candidate back
111
139
  * through it (live-mode.md § 4.5). An ambient marker rides the container's FIRST child only, the
112
140
  * way `BlockList` forwards it, and a list item is the one built-in container that paints one. */
113
- function containerAmbientPrefix(doc, path) {
141
+ export function containerAmbientPrefix(doc, path) {
114
142
  if (path.length < 2 || path[path.length - 1] !== 0)
115
143
  return '';
116
144
  const parent = blockNodeAt(doc, path.slice(0, -1));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Dragging a selection and dropping it. The browser's own version is two native edits committed
3
+ * apart — `deleteByDrag` on the source, `insertFromDrop` on the target — so undo takes two presses
4
+ * over a document that lost bytes in between, and inside one block the source commit rebuilds the
5
+ * surface under the drop. Owned here instead: one snapshot over two raw writes, which splice
6
+ * nothing, so no path moves under the second.
7
+ */
8
+ import type { Document } from '../core/nodes';
9
+ import type { GrammarView } from '../schema/block-openers';
10
+ import type { LinkReferenceResolverRef, PresentationModeGetter } from '../editor-keys';
11
+ import type { PluginActivation } from '../schema/plugin-activation';
12
+ import type { CommitController } from '../action-contracts';
13
+ import type { PasteCommitCoordinator } from '../tree-operations/paste/paste-deps';
14
+ import { type EditorEvents } from '../editor-events';
15
+ export interface SelectionDropDeps {
16
+ editorRoot: HTMLElement;
17
+ getDoc(): Document;
18
+ controller: CommitController;
19
+ coordinator: PasteCommitCoordinator;
20
+ getPresentationMode: PresentationModeGetter | undefined;
21
+ linkRef: LinkReferenceResolverRef | undefined;
22
+ grammar: GrammarView | undefined;
23
+ activePlugins: PluginActivation | undefined;
24
+ /** The instance event surface: a ceremony that throws mid-move has nowhere else to report. */
25
+ events: EditorEvents;
26
+ isReadOnly(): boolean;
27
+ }
28
+ export declare function installSelectionDrop(deps: SelectionDropDeps): () => void;
29
+ /** Where a splice of `delta` blocks at `at` inside `parent` leaves `target`: what the drop's
30
+ * second write addresses once the first one's reparse changed its slot's block count. */
31
+ export declare function shiftPathAfterSplice(target: number[], parent: number[], at: number, delta: number): number[];
32
+ /** Where a drop at `offset` lands once `[start, end)` left the same block, `shrunkBy` bytes
33
+ * shorter than the range was wide once the join seam cleaned up. Null inside the range. */
34
+ export declare function dropOffsetAfterCut(offset: number, start: number, end: number, shrunkBy: number): number | null;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Dragging a selection and dropping it. The browser's own version is two native edits committed
3
+ * apart — `deleteByDrag` on the source, `insertFromDrop` on the target — so undo takes two presses
4
+ * over a document that lost bytes in between, and inside one block the source commit rebuilds the
5
+ * surface under the drop. Owned here instead: one snapshot over two raw writes, which splice
6
+ * nothing, so no path moves under the second.
7
+ */
8
+ import { emitClipboardError } from '../editor-events';
9
+ import { ambientLengthOf } from '../ambient/ambient-dom';
10
+ import { toClampedRawOffset } from '../cursor/coordinate-spaces';
11
+ import { domTextOffsetAtNode } from '../cursor/widget-offset';
12
+ import { trailingLineEnding, trimTrailingLineEnding } from '../core/lines';
13
+ import { replaceRangeRaw } from '../components/blocks/text/live-selection-edit';
14
+ import { blockNodeAt, emptyParagraph, writeOwnRaw } from '../tree-operations/node-primitives';
15
+ import { cloneNode } from '../tree-operations/clone';
16
+ import { cutRangeFromDisplay } from '../tree-operations/node-ops';
17
+ import { rebuildAncestryRaw } from '../schema/container-raw';
18
+ import { applyPasteTransforms } from '../tree-operations/paste/paste-transforms';
19
+ import { replaceBlockAtParent } from '../tree-operations/paste/replace-block-at-parent';
20
+ import { parseReplacement } from '../tree-operations/paste/replacement-parse';
21
+ import { blockNearPoint } from './nearest-block';
22
+ import { findSurfaceForElement } from './path-lookup';
23
+ import { containerAmbientPrefix } from './range-delete';
24
+ /** A drag that IS this gesture but whose shape the seam does not move — a range leaving its
25
+ * surface, an empty one. The drop cancels it rather than handing it back. */
26
+ const DECLINED = 'declined';
27
+ // ── Public entry ───────────────────────────────────────────────────────────
28
+ export function installSelectionDrop(deps) {
29
+ let source = null;
30
+ const onDragStart = (e) => {
31
+ source = readSelectionSource(deps.editorRoot, e.target);
32
+ };
33
+ const onDragEnd = () => {
34
+ source = null;
35
+ };
36
+ // The editor is the drop target for every drag this seam recognized, declined ones included:
37
+ // the drop below must RUN to cancel the browser's pair of native edits.
38
+ const onDragOver = (e) => {
39
+ if (source)
40
+ e.preventDefault();
41
+ };
42
+ const onDrop = (e) => {
43
+ const from = source;
44
+ source = null;
45
+ if (!from)
46
+ return;
47
+ // Before every decline below, not after: the browser's two edits land apart and its undo
48
+ // is not ours, so a shape this seam declines mutates nothing rather than half of it.
49
+ e.preventDefault();
50
+ if (from === DECLINED || deps.isReadOnly())
51
+ return;
52
+ const target = dropTarget(deps, e.clientX, e.clientY);
53
+ if (!target)
54
+ return;
55
+ const text = movedText(deps, from);
56
+ if (text === null)
57
+ return;
58
+ void runDrop(deps, from, target, text, e.ctrlKey || e.altKey).catch((error) => {
59
+ // A throw between the two writes leaves a snapshot pushed and half the move applied;
60
+ // the host hears it on the channel the paste route reports through.
61
+ emitClipboardError(deps.events, { error, path: from.path });
62
+ });
63
+ };
64
+ deps.editorRoot.addEventListener('dragstart', onDragStart);
65
+ deps.editorRoot.addEventListener('dragend', onDragEnd);
66
+ deps.editorRoot.addEventListener('dragover', onDragOver);
67
+ deps.editorRoot.addEventListener('drop', onDrop);
68
+ return () => {
69
+ deps.editorRoot.removeEventListener('dragstart', onDragStart);
70
+ deps.editorRoot.removeEventListener('dragend', onDragEnd);
71
+ deps.editorRoot.removeEventListener('dragover', onDragOver);
72
+ deps.editorRoot.removeEventListener('drop', onDrop);
73
+ };
74
+ }
75
+ /** Where a splice of `delta` blocks at `at` inside `parent` leaves `target`: what the drop's
76
+ * second write addresses once the first one's reparse changed its slot's block count. */
77
+ export function shiftPathAfterSplice(target, parent, at, delta) {
78
+ if (delta === 0 || target.length <= parent.length)
79
+ return target;
80
+ for (let i = 0; i < parent.length; i++)
81
+ if (target[i] !== parent[i])
82
+ return target;
83
+ if (target[parent.length] <= at)
84
+ return target;
85
+ const shifted = target.slice();
86
+ shifted[parent.length] += delta;
87
+ return shifted;
88
+ }
89
+ /** Where a drop at `offset` lands once `[start, end)` left the same block, `shrunkBy` bytes
90
+ * shorter than the range was wide once the join seam cleaned up. Null inside the range. */
91
+ export function dropOffsetAfterCut(offset, start, end, shrunkBy) {
92
+ if (offset > start && offset < end)
93
+ return null;
94
+ return offset <= start ? offset : Math.max(0, offset - shrunkBy);
95
+ }
96
+ // ── Reading the gesture ────────────────────────────────────────────────────
97
+ /**
98
+ * The native range the drag carries, in its surface's raw offsets. `null` is "not this gesture" and
99
+ * leaves the drag to the browser; {@link DECLINED} is this gesture in a shape the seam does not
100
+ * move, which the drop cancels. A cross-block selection reaches neither: it paints through the
101
+ * overlay and leaves no native range for the browser to drag.
102
+ */
103
+ function readSelectionSource(editorRoot, dragged) {
104
+ const sel = window.getSelection();
105
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0)
106
+ return null;
107
+ const range = sel.getRangeAt(0);
108
+ // A draggable of its own (a rendered link, an image) is not this gesture, even with a range
109
+ // painted elsewhere: a selection drag grips a node the range covers.
110
+ if (!(dragged instanceof Node) || !range.intersectsNode(dragged))
111
+ return null;
112
+ const surface = surfaceOf(range.startContainer);
113
+ if (!surface || !editorRoot.contains(surface))
114
+ return null;
115
+ // Past this point the drag IS the editor's selection, so every remaining shape declines.
116
+ if (!surface.contains(range.endContainer))
117
+ return DECLINED;
118
+ const found = findSurfaceForElement(surface);
119
+ if (!found)
120
+ return DECLINED;
121
+ const ambient = ambientLengthOf(surface);
122
+ const start = toClampedRawOffset(domTextOffsetAtNode(surface, range.startContainer, range.startOffset), ambient);
123
+ const end = toClampedRawOffset(domTextOffsetAtNode(surface, range.endContainer, range.endOffset), ambient);
124
+ return start < end ? { ...found, start, end } : DECLINED;
125
+ }
126
+ /** The block and offset a drop point addresses, as a single-click caret would land. Null where
127
+ * the point names no character position (a whole-block unit, a table cell). */
128
+ function dropTarget(deps, clientX, clientY) {
129
+ const near = blockNearPoint(deps.editorRoot, clientX, clientY);
130
+ const point = near?.endpointHere();
131
+ if (!point || !('offset' in point) || point.cellCoordinate)
132
+ return null;
133
+ return blockNodeAt(deps.getDoc(), point.path) ? { path: point.path, offset: point.offset } : null;
134
+ }
135
+ /** The source surface's own bytes for the range, past the paste transforms. Null for a payload
136
+ * this seam does not own: a line break needs the structural paste route. */
137
+ function movedText(deps, from) {
138
+ const node = blockNodeAt(deps.getDoc(), from.path);
139
+ if (!node)
140
+ return null;
141
+ const raw = trimTrailingLineEnding(node.raw).slice(from.start, from.end);
142
+ const text = applyPasteTransforms(raw, deps.activePlugins);
143
+ return text && !/[\r\n]/.test(text) ? text : null;
144
+ }
145
+ // ── The commit ceremony ────────────────────────────────────────────────────
146
+ async function runDrop(deps, from, to, text, copy) {
147
+ if (copy) {
148
+ await writeBlockRaw(deps, to.path, insert(to.offset, text), to.offset + text.length, true);
149
+ return;
150
+ }
151
+ const cut = cutFrom(deps, from);
152
+ if (!cut)
153
+ return;
154
+ if (pathsEqual(cut.path, to.path)) {
155
+ const offset = dropOffsetAfterCut(to.offset, from.start, from.end, cut.shrunkBy);
156
+ if (offset === null)
157
+ return;
158
+ const merged = spliceAt(cut.raw, offset, text);
159
+ await writeBlockRaw(deps, cut.path, () => merged, offset + text.length, true);
160
+ return;
161
+ }
162
+ deps.controller.pushUndoSnapshotPath(from.path, from.start);
163
+ // A table's caret door reads a cell landing, never a character offset (G1.29), so the
164
+ // transient caret the second write moves off is its first cell.
165
+ const sourceCaret = from.inCell ? 0 : from.start;
166
+ const spliced = await writeBlockRaw(deps, cut.path, () => cut.raw, sourceCaret, false);
167
+ const at = cut.path[cut.path.length - 1];
168
+ const target = shiftPathAfterSplice(to.path, cut.path.slice(0, -1), at, spliced);
169
+ await writeBlockRaw(deps, target, insert(to.offset, text), to.offset + text.length, false);
170
+ }
171
+ function insert(offset, text) {
172
+ return (raw) => spliceAt(raw, offset, text);
173
+ }
174
+ /** The source surface's display bytes with the dragged range gone, through the delete seam so a
175
+ * live-mode join cleans up after itself. */
176
+ function cutFrom(deps, from) {
177
+ const node = blockNodeAt(deps.getDoc(), from.path);
178
+ if (!node)
179
+ return null;
180
+ if (from.inCell)
181
+ return cutFromCell(deps, from, node);
182
+ const before = trimTrailingLineEnding(node.raw);
183
+ const edit = replaceRangeRaw(node, { start: from.start, end: from.end }, '', deps.getPresentationMode?.(), deps.linkRef, containerAmbientPrefix(deps.getDoc(), from.path));
184
+ const raw = trimTrailingLineEnding(edit.raw);
185
+ return { path: from.path, raw, shrunkBy: before.length - raw.length };
186
+ }
187
+ /** A cell's bytes are joined into its row, so the TABLE is the block the cut rewrites: the cell's
188
+ * own range-delete on a copy, then the kind's escape and the ancestry rebuild around it. */
189
+ function cutFromCell(deps, from, cell) {
190
+ const tablePath = from.path.slice(0, -2);
191
+ // The cell path is resolved from a DOM selector contract, so the kind is read, not assumed.
192
+ const table = blockNodeAt(deps.getDoc(), tablePath);
193
+ if (!table || table.kind !== 'table')
194
+ return null;
195
+ const cut = cutRangeFromDisplay(cell, cell.raw, { start: from.start, end: from.end }, deps.getPresentationMode?.(), deps.linkRef);
196
+ const rebuilt = cloneNode(table);
197
+ const inner = from.path.slice(-2);
198
+ const [rowIdx, colIdx] = inner;
199
+ const written = rebuilt.children?.[rowIdx]?.children?.[colIdx];
200
+ if (!written)
201
+ return null;
202
+ writeOwnRaw(written, cut.display, deps.grammar);
203
+ rebuildAncestryRaw(rebuilt, inner);
204
+ const raw = trimTrailingLineEnding(rebuilt.raw);
205
+ return { path: tablePath, raw, shrunkBy: trimTrailingLineEnding(table.raw).length - raw.length };
206
+ }
207
+ /**
208
+ * Replace the block at `path` with the reparse of the bytes `rewrite` returns, at its parent
209
+ * scope. Answers how many blocks the slot grew or shrank by, which is what keeps a second
210
+ * write's path honest. `own` pushes this write's own undo entry.
211
+ */
212
+ async function writeBlockRaw(deps, path, rewrite, caret, own) {
213
+ const doc = deps.getDoc();
214
+ const node = blockNodeAt(doc, path);
215
+ if (!node)
216
+ return 0;
217
+ const written = rewrite(trimTrailingLineEnding(node.raw));
218
+ // A block emptied by the cut keeps its slot as a blank paragraph: no splice, so the second
219
+ // write's path is still the one this gesture resolved.
220
+ const parsed = parseReplacement(node, written, deps.grammar, () => [
221
+ emptyParagraph(node.leadingTrivia ?? '', trailingLineEnding(node.raw))
222
+ ]);
223
+ if (!parsed)
224
+ return 0;
225
+ const landed = await replaceBlockAtParent({
226
+ doc,
227
+ blockPath: path,
228
+ replacement: parsed.replacement,
229
+ controller: deps.coordinator,
230
+ undoEntry: own ? 'own' : 'join',
231
+ focusReplacementIndex: parsed.replacement.length - 1,
232
+ focusOffset: caret,
233
+ source: 'selection-drop',
234
+ ...(deps.grammar ? { grammar: deps.grammar } : {})
235
+ });
236
+ // The LANDED count, not the parse's: a body rule inside the scope can rewrite the list. Zero
237
+ // is the unmounted-scope decline, which wrote nothing and so moved nothing.
238
+ return Math.max(0, landed - 1);
239
+ }
240
+ // ── Small helpers ──────────────────────────────────────────────────────────
241
+ function spliceAt(raw, offset, text) {
242
+ const at = Math.max(0, Math.min(offset, raw.length));
243
+ return raw.slice(0, at) + text + raw.slice(at);
244
+ }
245
+ function pathsEqual(a, b) {
246
+ return a.length === b.length && a.every((v, i) => v === b[i]);
247
+ }
248
+ function surfaceOf(node) {
249
+ let el = node instanceof HTMLElement ? node : node.parentElement;
250
+ while (el && el.contentEditable !== 'true')
251
+ el = el.parentElement;
252
+ return el;
253
+ }
@@ -5,7 +5,7 @@
5
5
  * can't be carried by only one. A gap caret takes {@link restoreGapCaret}, the same road minus
6
6
  * the endpoint pair.
7
7
  */
8
- import { isBlockNode, nodeAt } from '../tree-operations/node-ops';
8
+ import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
9
9
  import { applySelectionToDom } from './native-bridge';
10
10
  import { placeGapCaret } from './caret-doors';
11
11
  import { gapScopeChildren } from './gap-caret';
@@ -34,6 +34,12 @@ export interface SelectionState {
34
34
  readonly selectAllCount: number;
35
35
  /** The third mode: a collapsed caret in a between-blocks boundary (`gap-caret.ts`). */
36
36
  readonly gapCaret: GapCaretPosition | null;
37
+ /**
38
+ * One block taken whole as the entire range — a press on a surface-less leaf (an equation)
39
+ * dragged inside it. The stored pair spans the block's bytes on a shared path, which the
40
+ * same-path guard would otherwise refuse, so the overlay reads this to paint it as a unit.
41
+ */
42
+ readonly wholeUnitPath: number[] | null;
37
43
  enterCrossBlock(anchor: SelectionEndpoint, focus: SelectionEndpoint): void;
38
44
  extendFocus(point: SelectionEndpoint): void;
39
45
  collapse(): void;
@@ -4,11 +4,12 @@
4
4
  * native browser selection rules, and the two are mutually exclusive here rather than at any
5
5
  * call site. Transitions: `docs/design/editor.md` § Cross-block selection.
6
6
  */
7
- import { nodeAt } from '../tree-operations/node-ops';
7
+ import { nodeAt } from '../tree-operations/node-primitives';
8
8
  import { isWholeBlockEndpoint, normalize } from './primitives';
9
9
  import { cellEndpointDeepPath, normalizeTableEndpoint, snapCrossBlockTableEndpoints } from './table-endpoint-snap';
10
10
  import { normalizeCharEndpoint } from './char-endpoint-snap';
11
11
  import { pathsEqual } from './path-math';
12
+ import { displayLength } from '../core/lines';
12
13
  import { assertInvariant } from '../assert';
13
14
  import { checkCrossBlockEndpointCoordinates } from '../invariants/selection-endpoints';
14
15
  export function createSelectionState(options) {
@@ -19,6 +20,7 @@ class SelectionStateImpl {
19
20
  #anchor = $state(null);
20
21
  #focus = $state(null);
21
22
  #gapCaret = $state(null);
23
+ #wholeUnit = $state(null);
22
24
  #selectAllCount = $state(0);
23
25
  #onChange;
24
26
  #getDoc;
@@ -63,6 +65,9 @@ class SelectionStateImpl {
63
65
  get isCrossBlock() {
64
66
  return this.#anchor !== null && this.#focus !== null;
65
67
  }
68
+ get wholeUnitPath() {
69
+ return this.#wholeUnit?.slice() ?? null;
70
+ }
66
71
  get isCustomRendered() {
67
72
  const getDoc = this.#getDoc;
68
73
  if (!getDoc)
@@ -71,6 +76,8 @@ class SelectionStateImpl {
71
76
  const focus = this.#focus;
72
77
  if (!anchor || !focus)
73
78
  return false;
79
+ if (this.#wholeUnit)
80
+ return true;
74
81
  if (!pathsEqual(anchor.path, focus.path))
75
82
  return true;
76
83
  if (anchor.offset === focus.offset)
@@ -102,6 +109,19 @@ class SelectionStateImpl {
102
109
  }
103
110
  enterCrossBlock(anchor, focus) {
104
111
  this.#gapCaret = null;
112
+ // Both ends the same surface-less block: the unit itself is the range, stored as its full
113
+ // byte span and flagged, since below a same-path pair is refused as prose.
114
+ if (isWholeBlockEndpoint(anchor) &&
115
+ isWholeBlockEndpoint(focus) &&
116
+ pathsEqual(anchor.path, focus.path)) {
117
+ const path = anchor.path.slice();
118
+ this.#anchor = { path, offset: 0 };
119
+ this.#focus = { path: path.slice(), offset: this.#byteLengthAt(path) };
120
+ this.#wholeUnit = path.slice();
121
+ this.#notify();
122
+ return;
123
+ }
124
+ this.#wholeUnit = null;
105
125
  const a = this.#normalizePoint(anchor, focus.path);
106
126
  const f = this.#normalizePoint(focus, anchor.path);
107
127
  // A same-path prose pair is a single-block range the browser owns; storing it mints an
@@ -124,6 +144,13 @@ class SelectionStateImpl {
124
144
  throw new Error('SelectionState.extendFocus called without an anchor');
125
145
  }
126
146
  this.#gapCaret = null;
147
+ // Leaving a whole unit: its anchor is the block as a whole again, so the side it means
148
+ // (start below the new focus, end above it) is resolved afresh rather than kept at 0.
149
+ if (this.#wholeUnit) {
150
+ const unit = { path: this.#wholeUnit, wholeBlock: true };
151
+ this.#anchor = this.#normalizePoint(unit, point.path);
152
+ this.#wholeUnit = null;
153
+ }
127
154
  const f = this.#normalizePoint(point, this.#anchor.path);
128
155
  // A focus back on the anchor's prose leaf contracts to a single-block range. No
129
156
  // `offset !== offset` guard, unlike #isSamePathProseRange: extendFocus never seeds, so
@@ -142,6 +169,11 @@ class SelectionStateImpl {
142
169
  }
143
170
  // G1.29 at the storing seam, the belt behind #normalizePoint: both entries carry it because
144
171
  // both store an endpoint pair.
172
+ #byteLengthAt(path) {
173
+ const doc = this.#getDoc?.();
174
+ const node = doc ? nodeAt(doc, path) : null;
175
+ return node && 'raw' in node ? displayLength(node.raw) : 0;
176
+ }
145
177
  #assertEndpointCoordinates(anchor, focus) {
146
178
  const getDoc = this.#getDoc;
147
179
  if (!getDoc)
@@ -177,6 +209,7 @@ class SelectionStateImpl {
177
209
  this.#anchor = null;
178
210
  this.#focus = null;
179
211
  this.#gapCaret = null;
212
+ this.#wholeUnit = null;
180
213
  this.#notify();
181
214
  }
182
215
  clear() {
@@ -185,6 +218,7 @@ class SelectionStateImpl {
185
218
  this.#anchor = null;
186
219
  this.#focus = null;
187
220
  this.#gapCaret = null;
221
+ this.#wholeUnit = null;
188
222
  this.#selectAllCount = 0;
189
223
  this.#notify();
190
224
  }
@@ -198,6 +232,7 @@ class SelectionStateImpl {
198
232
  setGapCaret(pos) {
199
233
  this.#anchor = null;
200
234
  this.#focus = null;
235
+ this.#wholeUnit = null;
201
236
  this.#gapCaret = { parentPath: pos.parentPath.slice(), index: pos.index };
202
237
  this.#notify();
203
238
  }
@@ -5,7 +5,7 @@
5
5
  * An intra-table pair is deliberately left unsnapped, so rectangular sub-cell selection survives.
6
6
  */
7
7
  import { metadataOf } from '../core/nodes';
8
- import { isBlockNode, nodeAt } from '../tree-operations/node-ops';
8
+ import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
9
9
  import { cellIndexOf } from './primitives';
10
10
  import { asCellIndex, cellRowCol } from '../cursor/coordinate-spaces';
11
11
  import { comparePaths, pathHasPrefix } from './path-math';