@voithos-labs/aragonite 0.10.2 → 0.10.3

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 (255) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +18 -0
  4. package/dist/a11y-strings.js +18 -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 +35 -24
  9. package/dist/components/BlockHost.svelte +16 -9
  10. package/dist/components/Editor.svelte +381 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +17 -3
  13. package/dist/components/TailInsert.svelte +107 -0
  14. package/dist/components/TailInsert.svelte.d.ts +17 -0
  15. package/dist/components/block-content-selector.d.ts +6 -2
  16. package/dist/components/block-content-selector.js +6 -2
  17. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  18. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  19. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  20. package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
  21. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  22. package/dist/components/blocks/code/code-bootstrap.js +4 -0
  23. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  24. package/dist/components/blocks/code/code-context-actions.js +24 -0
  25. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  26. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  27. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  28. package/dist/components/blocks/code/code-languages.js +11 -0
  29. package/dist/components/blocks/code/code-renderer.js +11 -0
  30. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  31. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  32. package/dist/components/blocks/editable-leaf.js +243 -29
  33. package/dist/components/blocks/editable-surface.d.ts +9 -0
  34. package/dist/components/blocks/editable-surface.js +22 -3
  35. package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
  36. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  37. package/dist/components/blocks/list/task-checkbox.js +11 -2
  38. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  39. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  40. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  41. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  42. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  43. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  44. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  45. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  46. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  47. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  48. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  49. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  50. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  51. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  52. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  53. package/dist/components/blocks/table/table-menu-model.js +36 -17
  54. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  55. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  56. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  57. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  58. package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
  59. package/dist/components/blocks/text/live-selection-edit.js +27 -0
  60. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  61. package/dist/components/blocks/text/text-keydown.js +11 -1
  62. package/dist/components/blocks/text/text-render.d.ts +1 -1
  63. package/dist/components/blocks/text/text-render.js +3 -1
  64. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  65. package/dist/components/blocks/text/widget-interaction.js +144 -22
  66. package/dist/components/drag-handle.d.ts +35 -0
  67. package/dist/components/drag-handle.js +126 -0
  68. package/dist/components/editor-root-focus.d.ts +19 -0
  69. package/dist/components/editor-root-focus.js +67 -0
  70. package/dist/components/editor-root-geometry.d.ts +39 -0
  71. package/dist/components/editor-root-geometry.js +91 -0
  72. package/dist/components/editor-root-keydown.d.ts +1 -1
  73. package/dist/components/editor-root-keydown.js +12 -3
  74. package/dist/components/editor-root-listeners.d.ts +10 -6
  75. package/dist/components/editor-root-listeners.js +23 -22
  76. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  77. package/dist/components/editor-root-mode-flip.js +92 -0
  78. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  79. package/dist/components/image/ImageProperties.svelte +512 -70
  80. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  81. package/dist/components/image/ImageResizeHandles.svelte +59 -34
  82. package/dist/components/image/image-crop.d.ts +39 -0
  83. package/dist/components/image/image-crop.js +74 -0
  84. package/dist/components/image/image-edit-commit.d.ts +1 -0
  85. package/dist/components/image/image-edit-commit.js +21 -5
  86. package/dist/components/image/image-source-bytes.js +10 -3
  87. package/dist/components/image/image-widget-editing.js +1 -0
  88. package/dist/components/image/widget-dom.js +5 -1
  89. package/dist/components/link-card/link-card-commit.js +1 -1
  90. package/dist/components/lrd-map-gate.js +1 -1
  91. package/dist/components/menu/BlockMenu.svelte +315 -0
  92. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  93. package/dist/components/menu/MenuIcon.svelte +153 -0
  94. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  95. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  96. package/dist/components/menu/clipboard-actions.js +42 -0
  97. package/dist/components/menu/default-context-actions.d.ts +15 -0
  98. package/dist/components/menu/default-context-actions.js +76 -0
  99. package/dist/components/menu/flyout-placement.d.ts +6 -0
  100. package/dist/components/menu/flyout-placement.js +25 -0
  101. package/dist/core/inline/format-toggle.d.ts +12 -4
  102. package/dist/core/inline/format-toggle.js +94 -40
  103. package/dist/core/inline/image-dimensions.d.ts +3 -0
  104. package/dist/core/inline/image-dimensions.js +46 -10
  105. package/dist/core/inline/inline-widgets.d.ts +11 -0
  106. package/dist/core/inline/scan/brackets.js +1 -0
  107. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  108. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  109. package/dist/core/inline-render.d.ts +6 -0
  110. package/dist/core/inline-render.js +32 -0
  111. package/dist/core/nodes.d.ts +14 -0
  112. package/dist/cursor/edge-affinity.js +2 -1
  113. package/dist/cursor/overlay-remeasure.js +8 -0
  114. package/dist/cursor/reveal-source.js +7 -2
  115. package/dist/cursor/widget-offset.d.ts +6 -0
  116. package/dist/cursor/widget-offset.js +61 -4
  117. package/dist/debug/interaction-trace.d.ts +4 -0
  118. package/dist/debug/interaction-trace.js +15 -0
  119. package/dist/decorations/decoration-state.svelte.js +1 -1
  120. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  121. package/dist/editor-actions/ancestry-folds.js +1 -1
  122. package/dist/editor-actions/block-edit-scope.js +1 -1
  123. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  124. package/dist/editor-actions/commit/text-batch.js +1 -1
  125. package/dist/editor-actions/commit/undo-controller.js +4 -2
  126. package/dist/editor-actions/container-edit.js +2 -1
  127. package/dist/editor-actions/enter-completion.d.ts +2 -0
  128. package/dist/editor-actions/enter-completion.js +23 -2
  129. package/dist/editor-actions/inline-range-commit.js +1 -1
  130. package/dist/editor-actions/reorder-action.js +19 -10
  131. package/dist/editor-actions/reorder-drag.js +29 -1
  132. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  133. package/dist/editor-actions/replacement-focus.js +1 -1
  134. package/dist/editor-actions/search-replace.js +1 -1
  135. package/dist/editor-actions/table-context.d.ts +4 -1
  136. package/dist/editor-actions/table-context.js +57 -1
  137. package/dist/editor-events.d.ts +3 -0
  138. package/dist/editor-keys.d.ts +33 -0
  139. package/dist/editor-props.d.ts +21 -12
  140. package/dist/index.d.ts +1 -1
  141. package/dist/plugin.d.ts +6 -0
  142. package/dist/plugin.js +11 -0
  143. package/dist/plugins/latex/BlockMath.svelte +264 -29
  144. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  145. package/dist/plugins/latex/index.d.ts +2 -1
  146. package/dist/plugins/latex/latex-kind.js +36 -3
  147. package/dist/plugins/latex/math-completion.js +4 -1
  148. package/dist/plugins/latex/math-layout.d.ts +15 -0
  149. package/dist/plugins/latex/math-layout.js +13 -0
  150. package/dist/plugins/latex/math-source.d.ts +19 -0
  151. package/dist/plugins/latex/math-source.js +97 -0
  152. package/dist/plugins/latex/register.d.ts +11 -2
  153. package/dist/plugins/latex/register.js +3 -1
  154. package/dist/plugins/latex/renderer.d.ts +3 -3
  155. package/dist/plugins/latex/renderer.js +13 -6
  156. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  157. package/dist/reactivity/list-windowing.svelte.js +49 -24
  158. package/dist/schema/block-completions.d.ts +8 -0
  159. package/dist/schema/block-completions.js +11 -0
  160. package/dist/schema/context-actions.d.ts +31 -0
  161. package/dist/schema/context-actions.js +23 -0
  162. package/dist/schema/fenced-code-raw.js +31 -1
  163. package/dist/schema/operations.d.ts +8 -1
  164. package/dist/schema/reserved-chords.js +25 -4
  165. package/dist/schema/table-cell-raw.d.ts +1 -1
  166. package/dist/schema/table-cell-raw.js +1 -1
  167. package/dist/selection/block-hit-test.js +3 -2
  168. package/dist/selection/char-endpoint-snap.js +1 -1
  169. package/dist/selection/clipboard-text.js +6 -1
  170. package/dist/selection/covered-block.d.ts +10 -0
  171. package/dist/selection/covered-block.js +24 -0
  172. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  173. package/dist/selection/cross-block/dispatch.js +13 -1
  174. package/dist/selection/cross-block/format-range.d.ts +1 -1
  175. package/dist/selection/cross-block/format-range.js +4 -15
  176. package/dist/selection/cross-block/format-toggle.js +1 -1
  177. package/dist/selection/cross-block/keydown.js +1 -1
  178. package/dist/selection/cross-block/ops.js +1 -1
  179. package/dist/selection/cross-block/paste.js +32 -30
  180. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  181. package/dist/selection/cross-block/type-replace.js +62 -13
  182. package/dist/selection/dead-space-caret.d.ts +10 -0
  183. package/dist/selection/dead-space-caret.js +47 -1
  184. package/dist/selection/double-click-trim.d.ts +17 -0
  185. package/dist/selection/double-click-trim.js +57 -0
  186. package/dist/selection/drag-pointer.d.ts +7 -2
  187. package/dist/selection/drag-pointer.js +61 -2
  188. package/dist/selection/gap-caret.js +1 -1
  189. package/dist/selection/keyboard-extend.js +1 -1
  190. package/dist/selection/path-lookup.js +1 -1
  191. package/dist/selection/range-delete-ceremony.js +4 -2
  192. package/dist/selection/range-delete-chrome.js +2 -1
  193. package/dist/selection/range-delete-table-coverage.js +2 -1
  194. package/dist/selection/range-delete-table.js +3 -2
  195. package/dist/selection/range-delete.js +30 -2
  196. package/dist/selection/selection-restore.js +1 -1
  197. package/dist/selection/selection-state.svelte.d.ts +6 -0
  198. package/dist/selection/selection-state.svelte.js +36 -1
  199. package/dist/selection/table-endpoint-snap.js +1 -1
  200. package/dist/selection/table-rect-extend.js +1 -1
  201. package/dist/styles/editor-theme.css +57 -28
  202. package/dist/styles/editor.css +217 -18
  203. package/dist/testing/container-conformance.js +2 -2
  204. package/dist/testing/inline-conformance.js +2 -1
  205. package/dist/tree-operations/blockquote.js +1 -1
  206. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  207. package/dist/tree-operations/chain-rebuild.js +142 -0
  208. package/dist/tree-operations/children.d.ts +1 -1
  209. package/dist/tree-operations/children.js +1 -1
  210. package/dist/tree-operations/cleanup.js +1 -1
  211. package/dist/tree-operations/content-write.d.ts +50 -0
  212. package/dist/tree-operations/content-write.js +263 -0
  213. package/dist/tree-operations/index.d.ts +8 -3
  214. package/dist/tree-operations/index.js +6 -2
  215. package/dist/tree-operations/list/exit-replacement.js +1 -1
  216. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  217. package/dist/tree-operations/node-ops.d.ts +17 -234
  218. package/dist/tree-operations/node-ops.js +47 -1113
  219. package/dist/tree-operations/node-primitives.d.ts +75 -0
  220. package/dist/tree-operations/node-primitives.js +117 -0
  221. package/dist/tree-operations/paste/apply.js +1 -1
  222. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  223. package/dist/tree-operations/paste/body-write.js +2 -2
  224. package/dist/tree-operations/paste/container-match.js +4 -2
  225. package/dist/tree-operations/paste/dispatch.js +2 -1
  226. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  227. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  228. package/dist/tree-operations/paste/list-absorb.js +1 -1
  229. package/dist/tree-operations/paste/list-break-out.js +1 -1
  230. package/dist/tree-operations/paste/parent-scope.js +1 -1
  231. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  232. package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
  233. package/dist/tree-operations/path-mutate.d.ts +1 -1
  234. package/dist/tree-operations/path-mutate.js +2 -1
  235. package/dist/tree-operations/reorder-unit.js +1 -1
  236. package/dist/tree-operations/reorder.d.ts +5 -2
  237. package/dist/tree-operations/reorder.js +55 -2
  238. package/dist/tree-operations/settle.d.ts +105 -0
  239. package/dist/tree-operations/settle.js +660 -0
  240. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  241. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  242. package/dist/tree-operations/unshare.d.ts +15 -77
  243. package/dist/tree-operations/unshare.js +15 -166
  244. package/docs/guide/consumer-guide.md +126 -96
  245. package/docs/guide/plugin-api.md +31 -3
  246. package/docs/guide/plugin-guide.md +26 -3
  247. package/package.json +4 -2
  248. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  249. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  250. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  251. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  252. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  253. package/dist/components/blocks/table/table-drop-target.js +0 -16
  254. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  255. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { makeBlockNode, metadataOf } from '../core/nodes';
5
5
  import { cloneMetadata } from '../tree-operations/clone';
6
- import { isBlockNode, nodeAt } from '../tree-operations/node-ops';
6
+ import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
7
7
  import { walkBetween, normalize, charOffsetOf, cellIndexOf } from './primitives';
8
8
  import { snapCrossBlockTableEndpoints } from './table-endpoint-snap';
9
9
  import { isStrictAncestorOf, pathHasPrefix, pathsEqual, sharedPrefixLength } from './path-math';
@@ -39,6 +39,11 @@ export function collectCrossBlockText(doc, anchor, focus) {
39
39
  }
40
40
  const startRaw = isBlockNode(startNode) ? startNode.raw : '';
41
41
  const endRaw = isBlockNode(endNode) ? endNode.raw : '';
42
+ // One leaf taken whole (`SelectionState.wholeUnitPath`): the only same-path prose pair the
43
+ // state stores, and the head/tail split below would emit its bytes twice.
44
+ if (pathsEqual(start.path, end.path) && !start.cellCoordinate) {
45
+ return startRaw.slice(start.offset, end.offset);
46
+ }
42
47
  let effectiveStartPath = start.path;
43
48
  let chromeStart = null;
44
49
  let startTail = '';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The block a range holds whole, for the gestures that insert over it. Such a range has no seam
3
+ * inside it to merge at: the block leaves the tree entirely, so bytes that follow must land in
4
+ * its SLOT rather than at a caret in whatever took the slot.
5
+ */
6
+ import type { DocumentView } from '../core/node-views';
7
+ import type { SelectionPoint } from './primitives';
8
+ /** The block both endpoints address and jointly span, or null. Cell coordinates index a grid
9
+ * rather than characters: the table's own coverage rule is `cross-block/paste.ts`'s. */
10
+ export declare function blockCoveredWhole(doc: DocumentView, anchor: SelectionPoint | null, focus: SelectionPoint | null): number[] | null;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The block a range holds whole, for the gestures that insert over it. Such a range has no seam
3
+ * inside it to merge at: the block leaves the tree entirely, so bytes that follow must land in
4
+ * its SLOT rather than at a caret in whatever took the slot.
5
+ */
6
+ import { displayLength } from '../core/lines';
7
+ import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
8
+ import { pathsEqual } from './path-math';
9
+ /** The block both endpoints address and jointly span, or null. Cell coordinates index a grid
10
+ * rather than characters: the table's own coverage rule is `cross-block/paste.ts`'s. */
11
+ export function blockCoveredWhole(doc, anchor, focus) {
12
+ if (!anchor || !focus)
13
+ return null;
14
+ if (!pathsEqual(anchor.path, focus.path))
15
+ return null;
16
+ if (anchor.cellCoordinate || focus.cellCoordinate)
17
+ return null;
18
+ const node = nodeAt(doc, anchor.path);
19
+ if (!node || !isBlockNode(node))
20
+ return null;
21
+ const low = Math.min(anchor.offset, focus.offset);
22
+ const high = Math.max(anchor.offset, focus.offset);
23
+ return low === 0 && high >= displayLength(node.raw) ? anchor.path.slice() : null;
24
+ }
@@ -71,6 +71,9 @@ export interface CrossBlockHandlers {
71
71
  * programmatic insertion: no gesture to consume, so `replacement` carries the payload. */
72
72
  handlePaste(e: ClipboardEvent | null, replacement?: string): Promise<boolean>;
73
73
  handleBeforeInput(e: InputEvent): Promise<boolean>;
74
+ /** Type-replace from a door that carries no InputEvent: the editor root, where a range over a
75
+ * block with no character position leaves no editing surface for `beforeinput` to fire on. */
76
+ insertText(text: string): Promise<boolean>;
74
77
  handleCompositionStart(): boolean;
75
78
  /** Cross-block range delete for Cut handlers, after they synchronously wrote the clipboard. */
76
79
  performCrossBlockDeleteFromEvent(): Promise<void>;
@@ -28,6 +28,14 @@ export function createCrossBlockHandlers(ctx) {
28
28
  // (each editable surface, the editor root) inherits them. Keydown gates its own destructive
29
29
  // branches, since it also carries navigation, which stays live.
30
30
  const reading = () => isReadingMode(ctx.getPresentationMode);
31
+ const insertText = async (text) => {
32
+ if (reading())
33
+ return true;
34
+ if (!ctx.selection.isCrossBlock)
35
+ return false;
36
+ await handleCrossBlockTypeReplace(ctx, mutationCtx, text);
37
+ return true;
38
+ };
31
39
  return {
32
40
  handleKeyDown: keydown.handleKeyDown,
33
41
  handleCompositionStart: keydown.handleCompositionStart,
@@ -44,8 +52,12 @@ export function createCrossBlockHandlers(ctx) {
44
52
  e.preventDefault();
45
53
  return true;
46
54
  }
47
- return handleCrossBlockTypeReplace(ctx, mutationCtx, e);
55
+ if (!ctx.selection.isCrossBlock || e.inputType !== 'insertText')
56
+ return false;
57
+ e.preventDefault();
58
+ return insertText(e.data ?? '');
48
59
  },
60
+ insertText,
49
61
  performCrossBlockDeleteFromEvent: async () => {
50
62
  // Reached from cut handlers after the clipboard write; declining the delete
51
63
  // degrades a reading-mode cut to a copy.
@@ -10,7 +10,7 @@ import type { DocumentView } from '../../core/node-views';
10
10
  import type { InlineMarkKind } from '../../schema/inline-construct-policy';
11
11
  import type { PresentationMode } from '../../presentation-mode';
12
12
  import type { GrammarView } from '../../schema/block-openers';
13
- import { type NodeParent } from '../../tree-operations/node-ops';
13
+ import { type NodeParent } from '../../tree-operations/node-primitives';
14
14
  import type { SharingState } from '../../tree-operations/sharing';
15
15
  import { type SelectionPoint } from '../primitives';
16
16
  export interface CrossBlockFormatWrite {
@@ -6,12 +6,13 @@
6
6
  * through the single-block seam, so its arms, candidates and mode verification are the same ones.
7
7
  * A grid joins by its cells, which the range covers whole.
8
8
  */
9
- import { activeInlineFormats, inlineFormatsCovering, isInlineFormatActive, isInlineFormatActiveAfter, toggleInlineFormat } from '../../core/inline/format-toggle';
9
+ import { activeInlineFormats, inlineFormatsCovering, isInlineFormatActive, isInlineFormatActiveAfter, toggleInlineFormat, withoutBoundaryWhitespace } from '../../core/inline/format-toggle';
10
10
  import { getContentRange } from '../../core/inline';
11
11
  import { ownTrailingLineEnding, trimTrailingLineEnding } from '../../core/lines';
12
12
  import { tryGetBlockKindDescriptor } from '../../schema/block-kind-descriptor';
13
- import { normalizeBodyWrite, writeOwnRaw } from '../../tree-operations/node-ops';
14
- import { ensureUnsharedPath, rebuildUnsharedChain } from '../../tree-operations/unshare';
13
+ import { normalizeBodyWrite, writeOwnRaw } from '../../tree-operations/node-primitives';
14
+ import { ensureUnsharedPath } from '../../tree-operations/unshare';
15
+ import { rebuildUnsharedChain } from '../../tree-operations/chain-rebuild';
15
16
  import { comparePaths } from '../path-math';
16
17
  import { charOffsetOf } from '../primitives';
17
18
  import { coveredGridCells, gridEndpointCellIndex } from '../table-endpoint-snap';
@@ -170,18 +171,6 @@ function contentSpan(node, path, from, to) {
170
171
  const selection = withoutBoundaryWhitespace(display, clampToContent(from ?? content.start, content), clampToContent(to ?? content.end, content));
171
172
  return selection && { path, edit: { display, content, selection } };
172
173
  }
173
- /** The span minus its boundary whitespace, null once nothing is left. Markdown opens and closes a
174
- * run against a word, never a space, so an untrimmed edge yields delimiters that form no
175
- * construct — which the modes that paint them write anyway (`wrapCandidates` is unverified there). */
176
- function withoutBoundaryWhitespace(display, from, to) {
177
- let start = from;
178
- let end = to;
179
- while (start < end && /\s/.test(display[start]))
180
- start++;
181
- while (end > start && /\s/.test(display[end - 1]))
182
- end--;
183
- return start === end ? null : { start, end };
184
- }
185
174
  const clampToContent = (offset, content) => Math.min(Math.max(offset, content.start), content.end);
186
175
  /**
187
176
  * Whether the span actually changed sides. The single-block seam decides its own arm from the
@@ -7,7 +7,7 @@
7
7
  import { ownTrailingLineEnding } from '../../core/lines';
8
8
  import { docPathFrom } from '../../cursor/coordinate-spaces';
9
9
  import { inlineMarkForCommand } from '../../schema/inline-construct-policy';
10
- import { blockNodeAt } from '../../tree-operations/node-ops';
10
+ import { blockNodeAt } from '../../tree-operations/node-primitives';
11
11
  import { comparePaths } from '../path-math';
12
12
  import { restoreSelection } from '../selection-restore';
13
13
  import { applyCrossBlockFormat, crossBlockActiveFormats, planCrossBlockFormat } from './format-range';
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { CURSOR_START } from '../../block-component';
6
6
  import { performCrossBlockDelete, performCrossBlockDeleteSync } from './ops';
7
- import { isBlockNode } from '../../tree-operations/node-ops';
7
+ import { isBlockNode } from '../../tree-operations/node-primitives';
8
8
  import { isReadingMode } from '../../presentation-mode';
9
9
  import { eventToChord } from '../../schema/keybindings';
10
10
  import { dispatchKeyCommand } from '../../schema/block-commands';
@@ -9,7 +9,7 @@ import { deleteSnapshot } from '../primitives';
9
9
  import { focusCollapsedCaret } from '../native-bridge';
10
10
  import { rangeDelete } from '../range-delete';
11
11
  import { trackChildIds } from '../../tree-operations/structural-change';
12
- import { isBlockNode, nodeAt } from '../../tree-operations/node-ops';
12
+ import { isBlockNode, nodeAt } from '../../tree-operations/node-primitives';
13
13
  import { pathsEqual } from '../path-math';
14
14
  import { docPathFrom } from '../../cursor/coordinate-spaces';
15
15
  import { getStateForNode } from '../../reactivity/state-registry';
@@ -5,14 +5,15 @@
5
5
  */
6
6
  import { tableCellCount } from '../table-endpoint-snap';
7
7
  import { CURSOR_END } from '../../block-component';
8
- import { normalizeLineEndings } from '../../core/lines';
8
+ import { normalizeLineEndings, terminateLine } from '../../core/lines';
9
9
  import { performCrossBlockDelete } from './ops';
10
10
  import { charOffsetOf } from '../primitives';
11
11
  import { focusCollapsedCaret } from '../native-bridge';
12
+ import { blockCoveredWhole } from '../covered-block';
12
13
  import { pasteDispatch } from '../../tree-operations/paste/dispatch';
13
14
  import { applyPasteTransforms } from '../../tree-operations/paste/paste-transforms';
14
15
  import { parse } from '../../core/parser';
15
- import { blockNodeAt, isBlockNode, nodeAt } from '../../tree-operations/node-ops';
16
+ import { blockNodeAt, isBlockNode, nodeAt } from '../../tree-operations/node-primitives';
16
17
  import { pathsEqual } from '../path-math';
17
18
  import { replaceBlockAtParent } from '../../tree-operations/paste/replace-block-at-parent';
18
19
  import { ensureEditableContainers, normalizeReplacementTrivia } from '../../tree-operations';
@@ -32,10 +33,12 @@ export async function handleCrossBlockPaste(ctx, mutCtx, e, replacement) {
32
33
  if (!pasted)
33
34
  return true;
34
35
  const doc = ctx.getDoc();
35
- // Whole-table selection (Ctrl+A 2nd press inside a cell): replace the table block at the
36
- // parent position, single undo. The sub-rectangle path only clears cells, leaving the table.
37
- if (isWholeTableSelection(ctx.selection, doc)) {
38
- await replaceTableWithPaste(ctx, mutCtx, pasted);
36
+ // The range holds one block whole, in either addressing: replace the block at its parent
37
+ // position, single undo. A sub-rectangle inside a table only clears cells, leaving the table.
38
+ const covered = wholeTablePath(ctx.selection, doc) ??
39
+ blockCoveredWhole(doc, ctx.selection.anchor, ctx.selection.focus);
40
+ if (covered) {
41
+ await replaceCoveredBlockWithPaste(ctx, mutCtx, pasted, covered);
39
42
  return true;
40
43
  }
41
44
  // Read before the delete collapses the selection: the only coordinate an error report on the
@@ -96,60 +99,59 @@ async function landCaretAfterPaste(ctx, caretPath, inlineCaretOffset) {
96
99
  ctx.getBlockElByPath(caretPath)?.focus();
97
100
  }
98
101
  }
99
- // ── Whole-table paste ──────────────────────────────────────────────────────
100
- function isWholeTableSelection(selection, doc) {
102
+ // ── Covered-block paste ────────────────────────────────────────────────────
103
+ /** The table a cell rectangle covers whole (Ctrl+A's 2nd press inside a cell), or null. */
104
+ function wholeTablePath(selection, doc) {
101
105
  const anchor = selection.anchor;
102
106
  const focus = selection.focus;
103
107
  if (!anchor || !focus)
104
- return false;
108
+ return null;
105
109
  if (!pathsEqual(anchor.path, focus.path))
106
- return false;
110
+ return null;
107
111
  const node = nodeAt(doc, anchor.path);
108
112
  if (!node || !isBlockNode(node) || node.kind !== 'table')
109
- return false;
113
+ return null;
110
114
  const cellCount = tableCellCount(node);
111
115
  if (cellCount === 0)
112
- return false;
116
+ return null;
113
117
  // Same-path intra-table selection: cell offsets are context-established, so read directly.
114
118
  const lo = Math.min(anchor.offset, focus.offset);
115
119
  const hi = Math.max(anchor.offset, focus.offset);
116
- return lo === 0 && hi === cellCount - 1;
120
+ return lo === 0 && hi === cellCount - 1 ? anchor.path.slice() : null;
117
121
  }
118
122
  /**
119
- * Replace the selected table block with the pasted content at the table's parent position.
120
- * Routes through replaceBlockAtParent so the splice lands at the doc/enclosing-container scope
121
- * rather than the row-level blockEdit TableRowBlock propagates. One snapshot covers the replace.
123
+ * Replace the covered block with the pasted content at its parent position. Routes through
124
+ * replaceBlockAtParent so the splice lands at the doc/enclosing-container scope rather than the
125
+ * row-level blockEdit TableRowBlock propagates. One snapshot covers the replace.
122
126
  */
123
- async function replaceTableWithPaste(ctx, mutCtx, pasted) {
124
- const tablePath = ctx.selection.anchor.path;
127
+ async function replaceCoveredBlockWithPaste(ctx, mutCtx, pasted, blockPath) {
125
128
  const doc = ctx.getDoc();
129
+ const covered = blockNodeAt(doc, blockPath);
130
+ if (!covered)
131
+ return;
126
132
  // This route never reaches pasteDispatch, so the paste transforms and the instance grammar
127
- // ride here too; both rules live in the helper, applied at both sites.
128
- const parsed = parse(applyPasteTransforms(pasted, ctx.activePlugins), {
129
- grammar: ctx.grammar,
130
- scope: 'fragment'
131
- });
133
+ // ride here too; both rules live in the helper, applied at both sites. Terminated in the
134
+ // covered block's OWN ending, or an unterminated clipboard line leaves the block below
135
+ // flowing into the last one pasted (G4.20).
136
+ const parsed = parse(terminateLine(applyPasteTransforms(pasted, ctx.activePlugins), covered.raw), { grammar: ctx.grammar, scope: 'fragment' });
132
137
  if (parsed.children.length === 0)
133
138
  return;
134
- const tableNode = blockNodeAt(doc, tablePath);
135
- if (!tableNode)
136
- return;
137
- const replacement = normalizeReplacementTrivia(tableNode, parsed.children);
139
+ const replacement = normalizeReplacementTrivia(covered, parsed.children);
138
140
  for (const node of replacement)
139
141
  ensureEditableContainers(node);
140
142
  mutCtx.pushUndoSnapshot();
141
143
  ctx.selection.collapse();
142
144
  await replaceBlockAtParent({
143
145
  doc,
144
- blockPath: tablePath,
146
+ blockPath,
145
147
  replacement,
146
148
  controller: ctx.pasteCoordinator,
147
149
  undoEntry: 'join',
148
150
  focusReplacementIndex: replacement.length - 1,
149
151
  focusOffset: CURSOR_END,
150
- source: 'cross-block-paste-whole-table',
152
+ source: 'cross-block-covered-block',
151
153
  ...(ctx.grammar ? { grammar: ctx.grammar } : {}),
152
- // Nothing is reattached behind the clipboard here — the table's whole slot is the target —
154
+ // Nothing is reattached behind the clipboard here — the block's whole slot is the target —
153
155
  // so the trailing blank rides in unfiltered (`paste/dispatch.ts` states the rule).
154
156
  trailingSeparator: parsed.suffix
155
157
  });
@@ -3,8 +3,9 @@
3
3
  * Delete the range, splice the character into the surviving leaf's raw, re-parse so a marker at
4
4
  * offset 0 re-derives the kind (parity with the single-block type path). Routed through
5
5
  * commitMultiScope so a kind change mints a fresh node, ids/refs stay synced, and the op is
6
- * `updateContent`.
6
+ * `updateContent`. A range holding its block whole has no surviving leaf and takes the
7
+ * replace arm at the foot of the file instead.
7
8
  */
8
9
  import type { CrossBlockDispatchContext } from './dispatch';
9
10
  import type { CrossBlockMutationContext } from './ops';
10
- export declare function handleCrossBlockTypeReplace(ctx: CrossBlockDispatchContext, mutCtx: CrossBlockMutationContext, e: InputEvent): Promise<boolean>;
11
+ export declare function handleCrossBlockTypeReplace(ctx: CrossBlockDispatchContext, mutCtx: CrossBlockMutationContext, typed: string): Promise<void>;
@@ -3,37 +3,51 @@
3
3
  * Delete the range, splice the character into the surviving leaf's raw, re-parse so a marker at
4
4
  * offset 0 re-derives the kind (parity with the single-block type path). Routed through
5
5
  * commitMultiScope so a kind change mints a fresh node, ids/refs stay synced, and the op is
6
- * `updateContent`.
6
+ * `updateContent`. A range holding its block whole has no surviving leaf and takes the
7
+ * replace arm at the foot of the file instead.
7
8
  */
8
9
  import { performCrossBlockDelete } from './ops';
9
10
  import { charOffsetOf } from '../primitives';
10
- import { blockNodeAt, normalizeBodyWrite, updateNodeContent, writeOwnRaw, settledCaretTarget } from '../../tree-operations/node-ops';
11
+ import { blockCoveredWhole } from '../covered-block';
12
+ import { CURSOR_END } from '../../block-component';
13
+ import { parse } from '../../core/parser';
14
+ import { terminateLine } from '../../core/lines';
15
+ import { replaceBlockAtParent } from '../../tree-operations/paste/replace-block-at-parent';
16
+ import { ensureEditableContainers, normalizeReplacementTrivia } from '../../tree-operations';
17
+ import { blockNodeAt, normalizeBodyWrite, writeOwnRaw } from '../../tree-operations/node-primitives';
18
+ import { updateNodeContent, settledCaretTarget } from '../../tree-operations/content-write';
11
19
  import { focusCollapsedCaret } from '../native-bridge';
12
- import { ensureUnsharedChild, ensureUnsharedNode, ensureUnsharedPath, rebuildUnsharedChain } from '../../tree-operations/unshare';
20
+ import { ensureUnsharedChild, ensureUnsharedNode, ensureUnsharedPath } from '../../tree-operations/unshare';
21
+ import { rebuildUnsharedChain } from '../../tree-operations/chain-rebuild';
13
22
  import { stampStructuralChange } from '../../tree-operations/structural-change';
14
23
  import { getStateForNode } from '../../reactivity/state-registry';
15
24
  import { docPathFrom } from '../../cursor/coordinate-spaces';
16
25
  import { devWarn } from '../../dev-warn';
17
- export async function handleCrossBlockTypeReplace(ctx, mutCtx, e) {
18
- if (!ctx.selection.isCrossBlock || e.inputType !== 'insertText')
19
- return false;
20
- e.preventDefault();
21
- const typed = e.data ?? '';
26
+ export async function handleCrossBlockTypeReplace(ctx, mutCtx, typed) {
27
+ // A range holding its block whole leaves no leaf to splice into, so the character replaces the
28
+ // block in its own slot. An empty insertion has nothing to stand there and takes the delete.
29
+ const covered = typed
30
+ ? blockCoveredWhole(ctx.getDoc(), ctx.selection.anchor, ctx.selection.focus)
31
+ : null;
32
+ if (covered) {
33
+ await replaceCoveredBlockWithText(ctx, mutCtx, typed, covered);
34
+ return;
35
+ }
22
36
  const caret = await performCrossBlockDelete(mutCtx, { skipCaretRestore: true });
23
37
  if (!caret)
24
- return true;
38
+ return;
25
39
  // All caret placements below target caret.path's top-level block; mount it once here so each
26
40
  // (the post-tick landing included) finds a live element.
27
41
  await ctx.revealPath(caret.path);
28
42
  if (!typed) {
29
43
  focusCollapsedCaret(ctx.getBlockElByPath, caret);
30
- return true;
44
+ return;
31
45
  }
32
46
  const doc = ctx.getDoc();
33
47
  const targetNode = blockNodeAt(doc, caret.path);
34
48
  if (!targetNode) {
35
49
  focusCollapsedCaret(ctx.getBlockElByPath, caret);
36
- return true;
50
+ return;
37
51
  }
38
52
  // `updateContent`, not `input`: symmetric with the single-block path's KIND-CHANGING branch
39
53
  // (block-edit.ts updateBlockContent). Only that path's kind-stable branch emits the debounced
@@ -42,7 +56,7 @@ export async function handleCrossBlockTypeReplace(ctx, mutCtx, e) {
42
56
  const scope = resolveTypedCharScope(ctx, caret.path);
43
57
  if (!scope) {
44
58
  focusCollapsedCaret(ctx.getBlockElByPath, caret);
45
- return true;
59
+ return;
46
60
  }
47
61
  // resolveTypedCharScope returns the leaf's IMMEDIATE parent: every mounted container registers
48
62
  // a BlockListState, and the document root stands in for a top-level leaf. The guard below is
@@ -97,7 +111,42 @@ export async function handleCrossBlockTypeReplace(ctx, mutCtx, e) {
97
111
  focusCollapsedCaret(ctx.getBlockElByPath, { path, offset: target.offset });
98
112
  }
99
113
  });
100
- return true;
114
+ }
115
+ /**
116
+ * Replace the covered block with the parse of the typed character, at the block's parent
117
+ * position: the same door the covered-block paste takes, so both gestures splice at one scope
118
+ * and land one undo entry. Re-parsed rather than spliced, so a marker typed over the block
119
+ * derives its kind exactly as the single-block type path does.
120
+ */
121
+ async function replaceCoveredBlockWithText(ctx, mutCtx, typed, blockPath) {
122
+ const doc = ctx.getDoc();
123
+ const covered = blockNodeAt(doc, blockPath);
124
+ if (!covered)
125
+ return;
126
+ // Terminated in the block's OWN ending: an unterminated line leaves the block below flowing
127
+ // into the one the character just minted (G4.20).
128
+ const parsed = parse(terminateLine(typed, covered.raw), {
129
+ grammar: ctx.grammar,
130
+ scope: 'fragment'
131
+ });
132
+ if (parsed.children.length === 0)
133
+ return;
134
+ const replacement = normalizeReplacementTrivia(covered, parsed.children);
135
+ for (const node of replacement)
136
+ ensureEditableContainers(node);
137
+ mutCtx.pushUndoSnapshot();
138
+ ctx.selection.collapse();
139
+ await replaceBlockAtParent({
140
+ doc,
141
+ blockPath,
142
+ replacement,
143
+ controller: ctx.pasteCoordinator,
144
+ undoEntry: 'join',
145
+ focusReplacementIndex: replacement.length - 1,
146
+ focusOffset: CURSOR_END,
147
+ source: 'cross-block-covered-block',
148
+ ...(ctx.grammar ? { grammar: ctx.grammar } : {})
149
+ });
101
150
  }
102
151
  /** The scope's children, re-read after the commit: the ceremony replaces the node it published. */
103
152
  function scopeChildrenOf(ctx, scopePath) {
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type BlockComponent } from '../block-component';
9
9
  import { type GapStopScope } from './gap-caret';
10
+ import type { SelectionEndpoint } from './primitives';
10
11
  export interface DeadSpaceCaretDeps {
11
12
  getBlockComponent(path: number[]): BlockComponent | null;
12
13
  /**
@@ -35,5 +36,14 @@ export interface DeadSpaceCaret {
35
36
  * was claimed; a point past a windowed-out tail claims it and lands after the reveal.
36
37
  */
37
38
  placeAtPoint(root: HTMLElement, x: number, y: number): boolean;
39
+ /** Whether a press target is the editor's dead space (the root, or a block list inside it). */
40
+ isDeadSpaceTarget(root: HTMLElement, target: EventTarget | null): boolean;
41
+ /**
42
+ * The endpoint a drag STARTING in dead space (or on a block's rendered face) anchors at: the
43
+ * landing a click there would take, without taking it. A character surface anchors at that
44
+ * offset; a rendered leaf (an equation) anchors as a WHOLE block, so the range takes it entire
45
+ * whichever way the drag goes. A coordinate-addressed kind (a table) anchors nothing.
46
+ */
47
+ anchorAtPoint(root: HTMLElement, x: number, y: number): SelectionEndpoint | null;
38
48
  }
39
49
  export declare function createDeadSpaceCaret(deps: DeadSpaceCaretDeps): DeadSpaceCaret;
@@ -55,6 +55,13 @@ export function createDeadSpaceCaret(deps) {
55
55
  const hit = blockAtPoint(root, probeX, probeY);
56
56
  if (!hit)
57
57
  return false;
58
+ // Prose lands the caret on the line the click is level with, as any editor does. A block
59
+ // with no character surface (an equation, a table, a rule) has no such line, and a revealed
60
+ // source is one only while it is being edited: a click that was not ON the block is a click
61
+ // on nothing, and focuses nothing (the click's owner then blurs what was being edited).
62
+ const transient = hit.charSurface?.classList.contains('md-source-surface') ?? false;
63
+ if ((!hit.charSurface || transient) && !pressedOnBlockContent(root, x, y))
64
+ return false;
58
65
  const landing = landingFor(hit, probeX, probeY);
59
66
  if (!landing)
60
67
  return false;
@@ -81,7 +88,33 @@ export function createDeadSpaceCaret(deps) {
81
88
  leafOf(component, landing.path)?.snapCaretToPoint?.(probeX, probeY);
82
89
  return true;
83
90
  }
91
+ function anchorAtPoint(root, x, y) {
92
+ const blocks = measureBlocks(root);
93
+ const band = nearestBand(blocks.map((b) => b.rect), y);
94
+ if (!band)
95
+ return null;
96
+ if (band.belowAll && lastMountedTopLevel(blocks) !== deps.lastBlockIndex())
97
+ return null;
98
+ const { x: probeX, y: probeY } = probePointIn(blocks[band.index].rect, x, y, band.belowAll);
99
+ const hit = blockAtPoint(root, probeX, probeY);
100
+ if (!hit)
101
+ return null;
102
+ // No character surface to measure against — a rendered equation, a rule, a table — so the
103
+ // block is the unit, and the funnel picks the side by the drag's direction. An offset would
104
+ // put a range END at the block's start and leave it out; a cell would make the drag a caret
105
+ // placement in the nearest column. A rule names no landing at all, and is a unit all the same.
106
+ if (!hit.charSurface)
107
+ return { path: hit.path.slice(), wholeBlock: true };
108
+ const landing = landingFor(hit, probeX, probeY);
109
+ if (!landing)
110
+ return null;
111
+ if (landing.path.length > 0)
112
+ return { path: hit.path.slice(), wholeBlock: true };
113
+ return { path: hit.path.slice(), offset: landing.offset };
114
+ }
84
115
  return {
116
+ isDeadSpaceTarget: isDeadSpace,
117
+ anchorAtPoint,
85
118
  notePress(root, event) {
86
119
  pressedOnDeadSpace =
87
120
  isDeadSpace(root, event.target) &&
@@ -155,6 +188,15 @@ function isDeadSpace(root, target) {
155
188
  return true;
156
189
  return target instanceof Element && target.classList.contains('block-list');
157
190
  }
191
+ /** Whether the ORIGINAL point (not the clamped probe) sits inside some block's content: on a
192
+ * descendant of a host, not on the host's own box or the dead space around it. */
193
+ function pressedOnBlockContent(root, x, y) {
194
+ const direct = document.elementFromPoint(x, y);
195
+ if (!(direct instanceof Element) || isDeadSpace(root, direct))
196
+ return false;
197
+ const host = direct.closest('[data-block-path]');
198
+ return host !== null && host !== direct;
199
+ }
158
200
  /**
159
201
  * Where the caret goes for a resolved hit: an internal child path (empty for a
160
202
  * character-addressed surface) plus the offset within that leaf. Null declines the click.
@@ -165,8 +207,12 @@ function landingFor(hit, probeX, probeY) {
165
207
  // A kind with only the drag hook addresses cells and named no caret landing.
166
208
  if (hit.foreignDragHitTest)
167
209
  return null;
210
+ // A leaf with no character surface (a rule, a folded equation) reached here was pressed on
211
+ // its own box: the block itself is the landing, and its `focus` ignores the offset.
212
+ if (!hit.charSurface)
213
+ return { path: [], offset: 0 };
168
214
  // Reading mode flips contenteditable off, and a non-editable leaf has no character position.
169
- if (!hit.charSurface?.matches('[contenteditable="true"]'))
215
+ if (!hit.charSurface.matches('[contenteditable="true"]'))
170
216
  return null;
171
217
  const offset = offsetFromViewportPoint(hit.charSurface, probeX, probeY);
172
218
  return offset === null ? null : { path: [], offset };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Word selection without its trailing space. Windows Chromium's double-click takes the whitespace
3
+ * after a word (the desktop-Word convention); a formatting toggle over that range would wrap the
4
+ * space too, and every other platform selects the bare word. The browser paints its own range
5
+ * before any `dblclick` handler runs, so the word is selected HERE, on the second press, with
6
+ * the native selection suppressed: the trimmed range is the first one ever painted.
7
+ */
8
+ /** Trim trailing whitespace off the document's current selection when it ends in a text node.
9
+ * A selection that is only whitespace is left alone: collapsing it would undo the click. */
10
+ export declare function trimDoubleClickSelection(doc: Document): boolean;
11
+ /**
12
+ * Select the word under the point of a double-click's second press, trimmed, synchronously.
13
+ * Word edges come from the engine's own `modify` walk so they match what its double-click would
14
+ * have chosen. False when the point names no text position; the caller then leaves the press to
15
+ * the browser.
16
+ */
17
+ export declare function selectWordAtPoint(doc: Document, x: number, y: number): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Word selection without its trailing space. Windows Chromium's double-click takes the whitespace
3
+ * after a word (the desktop-Word convention); a formatting toggle over that range would wrap the
4
+ * space too, and every other platform selects the bare word. The browser paints its own range
5
+ * before any `dblclick` handler runs, so the word is selected HERE, on the second press, with
6
+ * the native selection suppressed: the trimmed range is the first one ever painted.
7
+ */
8
+ /** Trim trailing whitespace off the document's current selection when it ends in a text node.
9
+ * A selection that is only whitespace is left alone: collapsing it would undo the click. */
10
+ export function trimDoubleClickSelection(doc) {
11
+ const sel = doc.getSelection();
12
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed)
13
+ return false;
14
+ const range = sel.getRangeAt(0);
15
+ const endNode = range.endContainer;
16
+ if (!(endNode instanceof Text))
17
+ return false;
18
+ const text = range.toString();
19
+ const trimmed = text.trimEnd().length;
20
+ if (trimmed === 0 || trimmed === text.length)
21
+ return false;
22
+ const cut = text.length - trimmed;
23
+ if (cut > range.endOffset)
24
+ return false;
25
+ sel.setBaseAndExtent(range.startContainer, range.startOffset, endNode, range.endOffset - cut);
26
+ return true;
27
+ }
28
+ /**
29
+ * Select the word under the point of a double-click's second press, trimmed, synchronously.
30
+ * Word edges come from the engine's own `modify` walk so they match what its double-click would
31
+ * have chosen. False when the point names no text position; the caller then leaves the press to
32
+ * the browser.
33
+ */
34
+ export function selectWordAtPoint(doc, x, y) {
35
+ const sel = doc.getSelection();
36
+ const hit = textPositionAt(doc, x, y);
37
+ if (!sel || !hit || typeof sel.modify !== 'function')
38
+ return false;
39
+ sel.setBaseAndExtent(hit.node, hit.offset, hit.node, hit.offset);
40
+ sel.modify('move', 'backward', 'word');
41
+ sel.modify('extend', 'forward', 'word');
42
+ if (sel.isCollapsed)
43
+ return false;
44
+ trimDoubleClickSelection(doc);
45
+ return true;
46
+ }
47
+ function textPositionAt(doc, x, y) {
48
+ const position = doc.caretPositionFromPoint?.(x, y);
49
+ if (position?.offsetNode instanceof Text) {
50
+ return { node: position.offsetNode, offset: position.offset };
51
+ }
52
+ const range = doc.caretRangeFromPoint?.(x, y);
53
+ if (range?.startContainer instanceof Text) {
54
+ return { node: range.startContainer, offset: range.startOffset };
55
+ }
56
+ return null;
57
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { UserScrollport } from '../cursor/scroll-ancestors';
7
7
  import type { SelectionState } from './selection-state.svelte';
8
- import type { SelectionPoint } from './primitives';
8
+ import type { SelectionEndpoint } from './primitives';
9
9
  import type { BlockElLookup } from '../editor-keys';
10
10
  export interface DragContext {
11
11
  editorRoot: HTMLElement;
@@ -15,8 +15,13 @@ export interface DragContext {
15
15
  getBlockElByPath: BlockElLookup;
16
16
  /** Aborted on editor unmount; forwarded to the session's teardown. */
17
17
  lifetimeSignal?: AbortSignal;
18
+ /**
19
+ * The press began outside every editable surface (the editor's margin), so no native drag
20
+ * is extending a selection underneath: the session paints the same-block range itself.
21
+ */
22
+ paintSameBlock?: boolean;
18
23
  }
19
24
  /** Document-level pointer listeners for a cross-block drag started at `down`, plus a disposer. */
20
- export declare function installDragListener(ctx: DragContext, anchorPoint: SelectionPoint, down: PointerEvent): {
25
+ export declare function installDragListener(ctx: DragContext, anchorPoint: SelectionEndpoint, down: PointerEvent): {
21
26
  dispose(): void;
22
27
  };