@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
@@ -1,20 +1,26 @@
1
1
  /**
2
- * The editable-leaf seam a plugin block component builds on: a text-editing block surface with
3
- * native caret/IME/undo/cross-block-selection parity, in one factory so a plugin never touches an
4
- * editor context key. Two modes: `plain` commits per keystroke, `render-primary` reveals its
5
- * source and commits on blur. Call synchronously during init. Contract: plugin-guide § The
6
- * editable leaf.
2
+ * The editable-leaf seam a plugin block component builds on: native caret/IME/undo/selection
3
+ * parity in one factory, so a plugin never touches an editor context key. `plain` commits per
4
+ * keystroke; `render-primary` reveals its source and commits on blur. A painted source
5
+ * (`renderSource`) edits only its own DOM: every byte still enters the CST through
6
+ * `commitReveal`'s one `updateBlockContent`. Call synchronously during init. Contract:
7
+ * plugin-guide § The editable leaf.
7
8
  */
8
9
  import { getContext } from 'svelte';
9
10
  import { createAttachmentKey } from 'svelte/attachments';
10
11
  import { EDITOR_POLICIES_KEY, EDITOR_SERVICES_KEY } from '../../editor-keys';
11
12
  import { asDomTextOffset } from '../../cursor/coordinate-spaces';
12
- import { setCursorOffset, getCursorOffset, getSelectionOffsets } from '../../cursor/content-offsets';
13
+ import { setCursorOffset, getCursorOffset, getSelectionOffsets, getRangeOffsets } from '../../cursor/content-offsets';
13
14
  import { handleSharedKeydown } from '../../selection/shared-keydown';
14
- import { createEditableSurface, createClipboardHandlers, consumePendingRestore } from './editable-surface';
15
+ import { createEditableSurface, createClipboardHandlers, consumePendingRestore, withKeydownVerdict } from './editable-surface';
15
16
  import { wireSurfaceContexts } from './surface-wiring.svelte';
16
17
  import { createContentOffsetBackend, anchorTrailingNewline } from './plain-text-backend';
18
+ import { CONTENT_EMPTY_ATTR, chromeFreeText, clampToLandableRaw, holdsOnlyMarkerChrome } from '../../cursor/widget-offset';
17
19
  import { parkFocusOnEditorRoot } from '../../selection/native-bridge';
20
+ import { assertInvariant } from '../../assert';
21
+ import { checkRenderedTextFidelity } from '../../invariants/render-fidelity';
22
+ import { resolveBinding } from '../../schema/commands';
23
+ import { eventToChord } from '../../schema/keybindings';
18
24
  import { resetForPointerDown } from '../../selection/cross-block/pointer';
19
25
  import { placeCaret } from '../../selection/caret-doors';
20
26
  import { createSourceReveal } from '../../cursor/reveal-source';
@@ -25,6 +31,7 @@ import {} from '../../schema/commands';
25
31
  import {} from '../../schema/block-commands';
26
32
  import { owningPluginEditor } from '../../schema/plugin-install';
27
33
  import { reorderRunCommand } from '../../editor-actions/reorder-action';
34
+ import { createTextBatch } from '../../editor-actions/commit/text-batch';
28
35
  /**
29
36
  * The node → metadata bridge (plus `commandHooks`) a minted block command resolves
30
37
  * against on the leaf tier — the container factory's `buildContainerKindTarget` sibling.
@@ -48,9 +55,9 @@ export function createEditableLeaf(deps) {
48
55
  // Plain mode's source is always the editable view.
49
56
  const isRevealed = mode === 'render-primary' ? deps.isRevealed : () => true;
50
57
  const wiring = wireSurfaceContexts();
51
- const { blockEdit, stickyColumn, edgeAffinity, selection, getDoc, getBlockElByPath, getEditorRoot, pluginEditor, events: editorEvents } = wiring.deps;
58
+ const { blockEdit, focusActions, stickyColumn, edgeAffinity, selection, getDoc, getBlockElByPath, getEditorRoot, pluginEditor, activePlugins, events: editorEvents } = wiring.deps;
52
59
  const { reorder } = getContext(EDITOR_SERVICES_KEY);
53
- const { presentationMode: getPresentationModeCtx, theme: getThemeCtx, onPasteImage } = getContext(EDITOR_POLICIES_KEY);
60
+ const { presentationMode: getPresentationModeCtx, theme: getThemeCtx, keybindingOverrides, onPasteImage } = getContext(EDITOR_POLICIES_KEY);
54
61
  const getPresentationMode = () => getPresentationModeCtx?.() ?? 'source';
55
62
  const getTheme = () => getThemeCtx?.() ?? 'dark';
56
63
  // Resolved by the kind's recorded owner, like the command context's `editor`.
@@ -123,13 +130,29 @@ export function createEditableLeaf(deps) {
123
130
  showSource: () => {
124
131
  traceRevealOpen('leaf');
125
132
  revealedBase = sourceText();
133
+ clearSourceHistory();
126
134
  deps.setRevealed?.(true);
127
135
  },
128
136
  showRendered: () => {
129
137
  revealedBase = null;
138
+ clearSourceHistory();
130
139
  deps.setRevealed?.(false);
131
140
  }
132
141
  });
142
+ // Every open goes through here rather than the kernel directly, so a chrome-only source is
143
+ // completed (see `EditableLeafDeps.completeBareSource`) whatever door revealed it.
144
+ async function revealSource(atSourceOffset = 0) {
145
+ await revealKernel.reveal(atSourceOffset);
146
+ const el = deps.getEl();
147
+ if (!el || !deps.completeBareSource || !isRevealed() || isReading())
148
+ return;
149
+ const completed = deps.completeBareSource(el.textContent ?? '');
150
+ if (!completed)
151
+ return;
152
+ paintSource(el, completed.text);
153
+ deps.onSourceEdit?.(completed.text);
154
+ setCursorOffset(el, asDomTextOffset(completed.caret));
155
+ }
133
156
  // ── Commit ─────────────────────────────────────────────────────────────────
134
157
  // Returns the commit's own promise, so a caller that has to act on the committed bytes
135
158
  // (a single-line Enter's split) can wait for the write to land.
@@ -138,14 +161,29 @@ export function createEditableLeaf(deps) {
138
161
  // caret follows the edit position.
139
162
  return Promise.resolve(blockEdit.updateBlockContent(deps.getIndex(), edited + trailingLineEnding(deps.getNode().raw), preEditOffset, edited.length));
140
163
  }
141
- async function commitReveal() {
164
+ // A blur that arrives with a cross-block range live is the release of a drag that began in
165
+ // this source and left it: the fold waits one frame, so the range's rects measured real text
166
+ // under the pointer, then folds unless focus came back — else the source stays open with
167
+ // nothing focused in it and no further blur to close it.
168
+ let foldFrame = 0;
169
+ function foldAfterRange() {
170
+ if (foldFrame)
171
+ return;
172
+ foldFrame = requestAnimationFrame(() => {
173
+ foldFrame = 0;
174
+ const el = deps.getEl();
175
+ if (!isRevealed() || (el && el.contains(document.activeElement)))
176
+ return;
177
+ void commitReveal(true);
178
+ });
179
+ }
180
+ async function commitReveal(force = false) {
142
181
  if (mode !== 'render-primary' || !isRevealed())
143
182
  return;
144
- // A cross-block selection sweeping through keeps the source revealed so its rects
145
- // measure real text, and focusout is this fold's only entry — so the leaf stays in
146
- // source view until the user focuses it and leaves again.
147
- if (selection.isCrossBlock)
183
+ if (selection.isCrossBlock && !force) {
184
+ foldAfterRange();
148
185
  return;
186
+ }
149
187
  // Only wired to onFocusOut — the block leaf folds on blur, never Escape-cancel.
150
188
  traceRevealFold('blur');
151
189
  const edited = deps.getEl()?.textContent ?? sourceText();
@@ -169,7 +207,7 @@ export function createEditableLeaf(deps) {
169
207
  // and block-level traversal passes over.
170
208
  if (isReading())
171
209
  return;
172
- void revealKernel.reveal(offset);
210
+ void revealSource(offset);
173
211
  return;
174
212
  }
175
213
  surface.parkCaret(offset);
@@ -184,7 +222,7 @@ export function createEditableLeaf(deps) {
184
222
  return;
185
223
  // Through the kernel like every other open, so this entry gets the same trace pair,
186
224
  // length assert and reveal base; it seats a caret at 0, which the column re-seats.
187
- await revealKernel.reveal();
225
+ await revealSource();
188
226
  }
189
227
  if (!deps.getEl())
190
228
  return;
@@ -196,6 +234,30 @@ export function createEditableLeaf(deps) {
196
234
  }
197
235
  const getCommandContext = () => buildLeafCommandContext(deps, blockEdit, pluginEditor);
198
236
  // ── View sync ──────────────────────────────────────────────────────────────
237
+ // The one place source bytes become DOM, so G1.28 is asserted here for every painter rather
238
+ // than trusted per plugin. A painter's chrome-only case (a fence with no body line) takes the
239
+ // same stamp a code block does, so its markers paint while focused.
240
+ function paintSource(el, text) {
241
+ if (deps.renderSource) {
242
+ const painted = deps.renderSource(text);
243
+ assertInvariant('rendered-text-fidelity', () => checkRenderedTextFidelity(painted.textContent ?? '', text));
244
+ el.replaceChildren(painted);
245
+ el.toggleAttribute(CONTENT_EMPTY_ATTR, holdsOnlyMarkerChrome(el));
246
+ }
247
+ else {
248
+ el.textContent = text;
249
+ }
250
+ anchorTrailingNewline(el);
251
+ }
252
+ function repaintSource() {
253
+ const el = deps.getEl();
254
+ if (!el || !deps.renderSource || composing)
255
+ return;
256
+ const offset = getCursorOffset(el);
257
+ paintSource(el, el.textContent ?? '');
258
+ if (offset !== null)
259
+ setCursorOffset(el, asDomTextOffset(offset));
260
+ }
199
261
  function syncSource() {
200
262
  const text = sourceText();
201
263
  const el = deps.getEl();
@@ -204,23 +266,73 @@ export function createEditableLeaf(deps) {
204
266
  const pending = pendingCursor;
205
267
  pendingCursor = null;
206
268
  if ((el.textContent ?? '') !== text) {
207
- el.textContent = text;
208
- anchorTrailingNewline(el);
269
+ paintSource(el, text);
270
+ // The local entries were taken against bytes an external rewrite just replaced.
271
+ clearSourceHistory();
209
272
  // Restore only under a live caret — an external rewrite (undo, structural
210
273
  // replace) must not steal focus.
211
274
  consumePendingRestore(el, pending, (offset) => setCursorOffset(el, asDomTextOffset(offset)));
212
275
  }
213
276
  }
214
- // ── Event handlers ─────────────────────────────────────────────────────────
277
+ let sourceUndo = [];
278
+ let sourceRedo = [];
279
+ // The document's own keystroke batch, not a second timer: a burst of typing inside the
280
+ // reveal takes one entry there, so it takes one here.
281
+ let batchBaseText = '';
282
+ const sourceBatch = createTextBatch({
283
+ pushSnapshot: (_leafPath, offset) => {
284
+ sourceUndo.push({ text: batchBaseText, caret: offset });
285
+ sourceRedo = [];
286
+ }
287
+ });
288
+ function clearSourceHistory() {
289
+ sourceBatch.interrupt();
290
+ sourceUndo = [];
291
+ sourceRedo = [];
292
+ }
293
+ function restoreSourceEntry(el, from, to) {
294
+ const entry = from.pop();
295
+ if (!entry)
296
+ return;
297
+ // The restored text is what the next keystroke must snapshot, so it opens its own batch.
298
+ sourceBatch.interrupt();
299
+ to.push({ text: el.textContent ?? '', caret: getCursorOffset(el) ?? 0 });
300
+ paintSource(el, entry.text);
301
+ setCursorOffset(el, asDomTextOffset(entry.caret));
302
+ deps.onSourceEdit?.(entry.text);
303
+ }
304
+ /**
305
+ * The one entry every reveal edit crosses, so the batching rule lives here rather than at
306
+ * each gesture: one character replaced by at most one non-newline character is the shape a
307
+ * keystroke has, and nothing else coalesces. Enter, a paste, a cut and a selection replace
308
+ * each take an entry of their own and end the burst before them.
309
+ */
310
+ function recordSourceEdit(text, caret, keystroke) {
311
+ if (!keystroke) {
312
+ sourceBatch.interrupt();
313
+ sourceUndo.push({ text, caret });
314
+ sourceRedo = [];
315
+ return;
316
+ }
317
+ batchBaseText = text;
318
+ sourceBatch.keystroke(deps.getPath(), caret);
319
+ }
215
320
  // Splice `insert` over the source text node's [start, end) and reseat the caret. A
216
321
  // DOM-text mutation keeps the offset walk exact where a native Enter/cut/paste would
217
322
  // inject <div>/<br> that vanish from textContent.
218
323
  function spliceSourceText(el, start, end, insert) {
219
324
  const text = el.textContent ?? '';
220
- el.textContent = text.slice(0, start) + insert + text.slice(end);
221
- anchorTrailingNewline(el);
325
+ const keystroke = end - start <= 1 && insert.length <= 1 && insert !== '\n';
326
+ if (deps.renderSource)
327
+ recordSourceEdit(text, getCursorOffset(el) ?? start, keystroke);
328
+ const next = text.slice(0, start) + insert + text.slice(end);
329
+ paintSource(el, next);
330
+ deps.onSourceEdit?.(next);
222
331
  preEditOffset = start;
223
332
  setCursorOffset(el, asDomTextOffset(start + insert.length));
333
+ // Armed once the edit has settled, so the window measures the gap the user leaves.
334
+ if (deps.renderSource && keystroke)
335
+ sourceBatch.armPause();
224
336
  if (mode === 'plain')
225
337
  editableSurface.onInput();
226
338
  }
@@ -267,6 +379,44 @@ export function createEditableLeaf(deps) {
267
379
  if (composing || !el)
268
380
  return;
269
381
  preEditOffset = getCursorOffset(el) ?? 0;
382
+ // Undo INSIDE an open painted reveal steps back through this session's own edits: the
383
+ // document's history sees the session as one entry written on blur, so until the local
384
+ // stack is spent the chord has nothing else to mean. Resolved through the keymap like every
385
+ // chord, so a host's rebinding or disable reaches it.
386
+ if (deps.renderSource && isRevealed()) {
387
+ const command = historyCommandFor(e);
388
+ if (command === 'history.undo' && sourceUndo.length > 0) {
389
+ e.preventDefault();
390
+ restoreSourceEntry(el, sourceUndo, sourceRedo);
391
+ return;
392
+ }
393
+ if (command === 'history.redo' && sourceRedo.length > 0) {
394
+ e.preventDefault();
395
+ restoreSourceEntry(el, sourceRedo, sourceUndo);
396
+ return;
397
+ }
398
+ }
399
+ // Backspace in a painted source that holds nothing but its own chrome deletes the block, as
400
+ // it does in a code block: an empty body has no byte the press could mean, and a seat that
401
+ // only steps out (or eats the one blank line) leaves a block the user just asked to be rid
402
+ // of. In every mode — the gate is the byte BEFORE the caret: whitespace, or nothing, so a
403
+ // press that sits right after a visible marker still edits that marker in source mode.
404
+ if (e.key === 'Backspace' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
405
+ const offset = deps.renderSource ? getCursorOffset(el) : null;
406
+ const text = el.textContent ?? '';
407
+ if (offset !== null &&
408
+ (offset === 0 || /\s/.test(text[offset - 1] ?? '')) &&
409
+ !hasSelectionIn(el) &&
410
+ chromeFreeText(el).trim() === '') {
411
+ e.preventDefault();
412
+ revealedBase = null;
413
+ deps.setRevealed?.(false);
414
+ const index = deps.getIndex();
415
+ await blockEdit.deleteBlock(index);
416
+ void focusActions.moveFocus(index - 1, 'end');
417
+ return;
418
+ }
419
+ }
270
420
  if ((await handleSharedKeydown(e, editableSurface.sharedCtx)) || editableSurface.isDetached())
271
421
  return;
272
422
  if (dispatchChord(e))
@@ -290,6 +440,56 @@ export function createEditableLeaf(deps) {
290
440
  spliceSourceText(el, offset, offset, '\n');
291
441
  }
292
442
  }
443
+ /**
444
+ * A painted source takes plain text edits here, not from the engine: Chromium treats a lone
445
+ * `\n` text node as a placeholder and replaces it on insert (a fresh `$$` block lost the line
446
+ * before its closer), and a native delete has no notion of hidden chrome. The edit range is
447
+ * clamped to the landable span, so nothing reaches a fence line. Composition stays native.
448
+ */
449
+ function onBeforeInput(e) {
450
+ if (!deps.renderSource || composing || e.isComposing)
451
+ return;
452
+ const el = deps.getEl();
453
+ if (!el)
454
+ return;
455
+ let insert;
456
+ switch (e.inputType) {
457
+ case 'insertText':
458
+ insert = e.data ?? '';
459
+ break;
460
+ case 'insertLineBreak':
461
+ case 'insertParagraph':
462
+ insert = '\n';
463
+ break;
464
+ case 'deleteContentBackward':
465
+ case 'deleteContentForward':
466
+ case 'deleteWordBackward':
467
+ case 'deleteWordForward':
468
+ insert = '';
469
+ break;
470
+ default:
471
+ return;
472
+ }
473
+ const target = e.getTargetRanges()[0];
474
+ const range = target ? getRangeOffsets(el, target) : null;
475
+ if (!range)
476
+ return;
477
+ e.preventDefault();
478
+ const start = clampToLandableRaw(el, range.start, 0);
479
+ const end = Math.max(start, clampToLandableRaw(el, range.end, 0));
480
+ spliceSourceText(el, start, end, insert);
481
+ }
482
+ function historyCommandFor(e) {
483
+ const chord = eventToChord(e);
484
+ if (!chord)
485
+ return undefined;
486
+ return resolveBinding(chord, deps.getNode().kind, keybindingOverrides(), activePlugins)
487
+ ?.command;
488
+ }
489
+ function hasSelectionIn(el) {
490
+ const sel = window.getSelection();
491
+ return Boolean(sel && !sel.isCollapsed && el.contains(sel.anchorNode));
492
+ }
293
493
  function onPointerDown(e) {
294
494
  void crossBlock.handlePointerDown(e);
295
495
  }
@@ -303,17 +503,25 @@ export function createEditableLeaf(deps) {
303
503
  const descriptor = tryGetBlockKindDescriptor(deps.getNode().kind);
304
504
  return descriptor?.caretTargetAtPoint?.(host, e.clientX, e.clientY)?.offset ?? 0;
305
505
  }
506
+ // The press only notes where it landed: revealing on the press swallowed every drag that
507
+ // began on a rendered equation. The editor's own drag runs from such a press (its root
508
+ // handler), and the CLICK — a release that did not move — is what reveals.
509
+ let renderPress = null;
306
510
  function onRenderPointerDown(e) {
307
- // Shift-click extends a selection; a plain click reveals. Reading mode: no reveal
308
- // and no preventDefault, so native selection over the rendered view stays live.
309
- if (e.shiftKey || isReading())
511
+ renderPress = e.shiftKey || isReading() ? null : { x: e.clientX, y: e.clientY };
512
+ }
513
+ function onRenderClick(e) {
514
+ const press = renderPress;
515
+ renderPress = null;
516
+ if (!press || e.shiftKey || isReading())
517
+ return;
518
+ if (Math.abs(e.clientX - press.x) > 3 || Math.abs(e.clientY - press.y) > 3)
310
519
  return;
311
- e.preventDefault();
312
520
  // The reveal lands a caret, so this owes the shared preamble. NOT through
313
521
  // crossBlock.handlePointerDown: that hit-tests against the SOURCE text, which the
314
522
  // rendered view is not.
315
- resetForPointerDown(selection, stickyColumn, edgeAffinity, e.shiftKey);
316
- void revealKernel.reveal(revealOffsetAt(e));
523
+ resetForPointerDown(selection, stickyColumn, edgeAffinity, false);
524
+ void revealSource(revealOffsetAt(e));
317
525
  }
318
526
  // The revealed source owns the press and has already spent the chord, so a spread the fold
319
527
  // does not unmount must not re-run either on the way up.
@@ -322,6 +530,10 @@ export function createEditableLeaf(deps) {
322
530
  if (!isRevealed())
323
531
  onRenderPointerDown(e);
324
532
  },
533
+ onclick: (e) => {
534
+ if (!isRevealed())
535
+ onRenderClick(e);
536
+ },
325
537
  onkeydown: (e) => {
326
538
  if (!isRevealed())
327
539
  void dispatchChord(e);
@@ -333,7 +545,8 @@ export function createEditableLeaf(deps) {
333
545
  role: 'textbox',
334
546
  spellcheck: 'false',
335
547
  oninput: editableSurface.onInput,
336
- onkeydown: handleKeydown,
548
+ onbeforeinput: onBeforeInput,
549
+ onkeydown: withKeydownVerdict(handleKeydown),
337
550
  oncopy: clipboard.onCopy,
338
551
  oncut: clipboard.onCut,
339
552
  onpaste: clipboard.onPaste,
@@ -373,6 +586,7 @@ export function createEditableLeaf(deps) {
373
586
  get sourceText() {
374
587
  return sourceText();
375
588
  },
589
+ repaintSource,
376
590
  surfaceProps,
377
591
  renderProps,
378
592
  getPresentationMode,
@@ -403,7 +617,7 @@ export function createEditableLeaf(deps) {
403
617
  reveal: (offset = 0) => {
404
618
  if (mode !== 'render-primary')
405
619
  return Promise.resolve(surface.focus(offset));
406
- return isReading() ? Promise.resolve() : revealKernel.reveal(offset);
620
+ return isReading() ? Promise.resolve() : revealSource(offset);
407
621
  },
408
622
  commitSource: (edited) => void commitSource(edited)
409
623
  };
@@ -22,6 +22,12 @@ import type { SelectionState } from '../../selection/selection-state.svelte';
22
22
  import { type RawOffset } from '../../cursor/coordinate-spaces';
23
23
  import { type CrossBlockHandlers } from '../../selection/cross-block/dispatch';
24
24
  import type { SharedKeydownContext } from '../../selection/shared-keydown';
25
+ /**
26
+ * Binds a surface's keydown handler so the interaction trace records ONE verdict per event,
27
+ * after the handler's own await chain settles. That record is the e2e harness's only positive
28
+ * signal that a gesture which must change nothing has finished. Disabled, one boolean read.
29
+ */
30
+ export declare function withKeydownVerdict(handle: (e: KeyboardEvent) => Promise<void>): (e: KeyboardEvent) => void;
25
31
  /**
26
32
  * Per-surface cursor I/O in raw-content coordinates (ambient marker excluded).
27
33
  * `buildRange` maps a raw-offset span to a DOM Range for selection writes.
@@ -186,6 +192,9 @@ export interface ClipboardSurfaceDeps {
186
192
  copyPreHook?: (e: ClipboardEvent) => boolean;
187
193
  /** Pre-cross-block cut arm (selected-widget splice, intra-table rect cut). */
188
194
  cutPreHook?: (e: ClipboardEvent) => boolean | Promise<boolean>;
195
+ /** Pre-cross-block paste arm, handed the normalized text while a live rectangle is still
196
+ * readable (a grid into a table's cells). True when it consumed the paste. */
197
+ pastePreHook?: (text: string) => boolean | Promise<boolean>;
189
198
  /** The intra-block copy payload; owns its preventDefault. Omit to write the
190
199
  * visible selection string (code, leaf); text and the cell slice their raw. */
191
200
  copyTail?: (e: ClipboardEvent) => void;
@@ -17,9 +17,24 @@ import { createCrossBlockHandlers } from '../../selection/cross-block/dispatch';
17
17
  import { writeCrossBlockCopy, writeCrossBlockCut } from '../../selection/cross-block/clipboard';
18
18
  import { createImagePasteArm } from '../paste-image-arm';
19
19
  import { clampToLandableRaw, revealsNoMarkers } from '../../cursor/widget-offset';
20
- import { traceCompositionStart, traceCompositionEnd } from '../../debug/interaction-trace';
20
+ import { isInteractionTraceEnabled, traceCompositionEnd, traceCompositionStart, traceKeydownVerdict } from '../../debug/interaction-trace';
21
21
  import { assertInvariant } from '../../assert';
22
22
  import { checkCompositionEndPaired } from '../../invariants/inline-transitions';
23
+ // ── Keydown verdict ─────────────────────────────────────────────────────────
24
+ /**
25
+ * Binds a surface's keydown handler so the interaction trace records ONE verdict per event,
26
+ * after the handler's own await chain settles. That record is the e2e harness's only positive
27
+ * signal that a gesture which must change nothing has finished. Disabled, one boolean read.
28
+ */
29
+ export function withKeydownVerdict(handle) {
30
+ return (e) => {
31
+ if (!isInteractionTraceEnabled()) {
32
+ void handle(e);
33
+ return;
34
+ }
35
+ void handle(e).then(() => traceKeydownVerdict(e.key, e.defaultPrevented));
36
+ };
37
+ }
23
38
  /**
24
39
  * Guarded pending-caret restore. Applies only while `el` still holds focus, so a blur
25
40
  * between arming the restore and the render drops it instead of yanking the global
@@ -92,7 +107,9 @@ export function createEditableSurface(deps) {
92
107
  const el = deps.getEl();
93
108
  if (!el)
94
109
  return;
95
- el.focus();
110
+ // `preventScroll`: seating a caret must not move the page — the reveal path scrolls when a
111
+ // target is off-screen; an implicit focus scroll jumped the document on every table edit.
112
+ el.focus({ preventScroll: true });
96
113
  if (offset === CURSOR_EXACT_START) {
97
114
  deps.backend.setRaw(asRawOffset(0));
98
115
  return;
@@ -107,7 +124,7 @@ export function createEditableSurface(deps) {
107
124
  const el = deps.getEl();
108
125
  if (!el)
109
126
  return;
110
- el.focus();
127
+ el.focus({ preventScroll: true });
111
128
  const ambientLength = deps.getAmbientLength();
112
129
  // minOffset = the walk position of raw 0 keeps the scan out of the marker region.
113
130
  const minOffset = toDomTextOffset(asRawOffset(0), ambientLength);
@@ -283,6 +300,8 @@ export function createClipboardHandlers(deps) {
283
300
  async function insertPastedText(text, e) {
284
301
  const fold = deps.foldReveal?.() ?? null;
285
302
  await fold?.settled;
303
+ if (text && deps.pastePreHook && (await deps.pastePreHook(text)))
304
+ return;
286
305
  if (await deps.crossBlock.handlePaste(e, text))
287
306
  return;
288
307
  deps.stickyColumn.reset();
@@ -260,6 +260,7 @@
260
260
  <div
261
261
  class="list-item-block"
262
262
  class:reorder-host={dragHandles}
263
+ class:handle-host={dragHandles}
263
264
  data-task-checked={taskCheckedAttr}
264
265
  data-list-marker={presentationMarkerKind}
265
266
  bind:this={boxEl}
@@ -289,7 +290,7 @@
289
290
  align-items: flex-start;
290
291
  }
291
292
 
292
- /* Hover reveal is the shared global `.reorder-host` rule in BlockHost; it reveals
293
+ /* Hover reveal is the shared global `.handle-host` rule in BlockHost; it reveals
293
294
  only the innermost hovered unit, so a sub-item's hover never lights the parent. */
294
295
 
295
296
  .list-item-content {
@@ -318,7 +319,6 @@
318
319
  > :global(.block-list)
319
320
  > :global(.block-host:first-child)
320
321
  > :global(:not(.list-block)) {
321
- text-decoration: line-through;
322
- color: var(--syntax-task-done, rgba(128, 128, 128, 0.7));
322
+ color: var(--syntax-task-done, var(--color-text-muted, rgba(128, 128, 128, 0.7)));
323
323
  }
324
324
  </style>
@@ -1,3 +1,5 @@
1
1
  import type { AmbientPrefix } from '../../../block-component';
2
2
  import type { ListItemMetadata } from '../../../core/nodes';
3
+ /** Matches the `.task-checkbox` slot and gap in editor.css; the two move together. */
4
+ export declare const TASK_HANGING_INDENT = "2.1em";
3
5
  export declare function buildTaskItemAmbient(metadata: ListItemMetadata | undefined, onToggle: () => void): AmbientPrefix;
@@ -1,4 +1,6 @@
1
1
  import { devWarn } from '../../../dev-warn';
2
+ /** Matches the `.task-checkbox` slot and gap in editor.css; the two move together. */
3
+ export const TASK_HANGING_INDENT = '2.1em';
2
4
  function isTaskMarkerChecked(taskMarker) {
3
5
  const c = taskMarker[1];
4
6
  return c === 'x' || c === 'X';
@@ -26,8 +28,15 @@ export function buildTaskItemAmbient(metadata, onToggle) {
26
28
  role: 'checkbox',
27
29
  // single source of truth: derive from the keyed marker (in the render memo key), not parallel taskChecked
28
30
  ariaChecked: isTaskMarkerChecked(metadata.taskMarker),
31
+ // The painted box is taller than the text beside it; the grip centres on the box.
32
+ dragAnchor: true,
29
33
  onClick: onToggle
30
- }
31
- ]
34
+ },
35
+ // The list marker gets a span of its own so the rendered modes can collapse its width:
36
+ // GitHub draws the box at the margin, not indented under a bullet that is not there.
37
+ { start: 0, end: boxStart, className: 'task-list-marker', onClick: () => { } }
38
+ ],
39
+ // The painted box (editor.css): slot + gap + the marker's trailing space.
40
+ indent: TASK_HANGING_INDENT
32
41
  };
33
42
  }
@@ -5,6 +5,8 @@
5
5
  * `createEditableSurface` itself stays context-free (the jsdom harness constructs it bare).
6
6
  */
7
7
  import { type KindCommandTarget } from '../../schema/block-commands';
8
+ import type { AnyBlockKind } from '../../core/nodes';
9
+ import type { AnyCommandId } from '../../schema/command-id';
8
10
  import type { EditableSurfaceDeps } from './editable-surface';
9
11
  /** The context-threaded half of `EditableSurfaceDeps` — the fields every surface passes verbatim. */
10
12
  export type SharedSurfaceDeps = Pick<EditableSurfaceDeps, 'selection' | 'getDoc' | 'getBlockElByPath' | 'focusActions' | 'getEditorRoot' | 'getScrollHost' | 'getEditorLifetime' | 'stickyColumn' | 'edgeAffinity' | 'blockEdit' | 'controller' | 'history' | 'pluginEditor' | 'getKeybindingOverrides' | 'pasteCoordinator' | 'grammar' | 'activePlugins' | 'events' | 'linkRef' | 'onCommandError' | 'crossBlockCommands'>;
@@ -13,6 +15,8 @@ export interface SurfaceWiring {
13
15
  deps: SharedSurfaceDeps;
14
16
  /** Resolve a chord at `target` through the shared gates; consumes the event when spent. */
15
17
  dispatchChord(e: KeyboardEvent, target: KindCommandTarget): boolean;
18
+ /** The command a press names at `kind`, overrides included, without running it. */
19
+ resolveChord(e: KeyboardEvent, kind: AnyBlockKind): AnyCommandId | null;
16
20
  }
17
21
  export declare function wireSurfaceContexts(): SurfaceWiring;
18
22
  export declare function useParkFocusOnUnmount(getEl: () => HTMLElement | null, getEditorRoot: () => HTMLElement | null): void;
@@ -9,6 +9,7 @@ import { BLOCK_EDIT_KEY, EDITOR_DOC_KEY, EDITOR_POLICIES_KEY, EDITOR_SERVICES_KE
9
9
  import { emitCommandError } from '../../editor-events';
10
10
  import { eventToChord } from '../../schema/keybindings';
11
11
  import { dispatchKeyCommand } from '../../schema/block-commands';
12
+ import { resolveBinding } from '../../schema/commands';
12
13
  import { parkFocusOnEditorRoot } from '../../selection/native-bridge';
13
14
  export function wireSurfaceContexts() {
14
15
  const blockEdit = getContext(BLOCK_EDIT_KEY);
@@ -56,7 +57,13 @@ export function wireSurfaceContexts() {
56
57
  e.preventDefault();
57
58
  return true;
58
59
  };
59
- return { deps, dispatchChord };
60
+ const resolveChord = (e, kind) => {
61
+ const chord = eventToChord(e);
62
+ if (!chord)
63
+ return null;
64
+ return resolveBinding(chord, kind, keybindingOverrides(), activePlugins)?.command ?? null;
65
+ };
66
+ return { deps, dispatchChord, resolveChord };
60
67
  }
61
68
  // Windowed out while focused: hand focus to the editor root so the next keystroke
62
69
  // routes through its document-level listener instead of falling to `<body>`.